Implement WebView via Playwright (#1434)

* Implement Android's Looper

Looper handles thread messaging. This is used by extensions when they
want to enqueue actions e.g. for sleeping while WebView does someting

* Stub WebView

* Continue stubbing ViewGroup for WebView

* Implement WebView via Playwright

* Lint

* Implement request interception

Supports Yidan

* Support WebChromeClient

For Bokugen

* Fix onPageStarted

* Make Playwright configurable

* Subscribe to config changes

* Fix exposing of functions

* Support data urls

* Looper: Fix infinite sleep

* Looper: Avoid killing the loop on exception

Just log it and continue

* Pump playwright's message queue periodically

https://playwright.dev/java/docs/multithreading#pagewaitfortimeout-vs-threadsleep

* Update server/src/main/kotlin/suwayomi/tachidesk/graphql/types/SettingsType.kt

Co-authored-by: Mitchell Syer <Syer10@users.noreply.github.com>

* Stub a KCef WebViewProvider

* Initial Kcef Webview implementation

Still buggy, on the second call it just seems to fall over

* Format, restructure to create browser on load

This is much more consistent, before we would sometimes see errors from
about:blank, which block the actual page

* Implement some small useful properties

* Move inline objects to class

* Handle requests in Kcef

* Move Playwright implementation

* Document Playwright settings, fix deprecated warnings

* Inject default user agent from NetworkHelper

* Move playwright to libs.versions.toml

* Lint

* Fix missing imports after lint

* Update server/src/main/kotlin/suwayomi/tachidesk/server/ServerSetup.kt

Co-authored-by: Mitchell Syer <Syer10@users.noreply.github.com>

* Fix default user agent set/get

Use System.getProperty instead of SystemProperties.get

* Configurable WebView provider implementation

* Simplify Playwright settings init

* Minor cleanup and improvements

* Remove playwright WebView impl

* Document WebView for Linux

---------

Co-authored-by: Mitchell Syer <Syer10@users.noreply.github.com>
This commit is contained in:
Constantin Piber
2025-06-12 17:38:54 +02:00
committed by GitHub
parent dee61e191c
commit a2fadbe513
30 changed files with 18694 additions and 4 deletions
@@ -16,6 +16,7 @@ import io.github.oshai.kotlinlogging.KotlinLogging
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@@ -54,6 +55,7 @@ class NetworkHelper(
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) " +
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
)
val userAgentFlow = userAgent.asStateFlow()
fun defaultUserAgentProvider(): String = userAgent.value
@@ -117,6 +117,14 @@ class ServerConfig(
// extensions
val extensionRepos: MutableStateFlow<List<String>> by OverrideConfigValues(StringConfigAdapter)
// playwright webview
val playwrightBrowser: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter)
val playwrightWsEndpoint: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter)
val playwrightSandbox: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
// webview
val webviewImpl: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter)
// requests
val maxSourcesInParallel: MutableStateFlow<Int> by OverrideConfigValue(IntConfigAdapter)
@@ -7,8 +7,10 @@ package suwayomi.tachidesk.server
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import android.os.Looper
import ch.qos.logback.classic.Level
import com.typesafe.config.ConfigRenderOptions
import dev.datlag.kcef.KCEF
import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.createAppModule
import eu.kanade.tachiyomi.network.NetworkHelper
@@ -16,9 +18,14 @@ import eu.kanade.tachiyomi.source.local.LocalSource
import io.github.oshai.kotlinlogging.KotlinLogging
import io.javalin.json.JavalinJackson
import io.javalin.json.JsonMapper
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.koin.core.context.startKoin
import org.koin.core.module.Module
@@ -50,6 +57,10 @@ import java.net.Authenticator
import java.net.PasswordAuthentication
import java.security.Security
import java.util.Locale
import kotlin.io.path.Path
import kotlin.io.path.createDirectories
import kotlin.io.path.div
import kotlin.math.roundToInt
private val logger = KotlinLogging.logger {}
@@ -70,6 +81,15 @@ class ApplicationDirs(
val mangaDownloadsRoot get() = "$downloadsRoot/mangas"
}
@Suppress("DEPRECATION")
class LooperThread : Thread() {
override fun run() {
logger.info { "Starting Android Main Loop" }
Looper.prepareMainLooper()
Looper.loop()
}
}
data class ProxySettings(
val proxyEnabled: Boolean,
val socksProxyVersion: Int,
@@ -100,11 +120,15 @@ fun serverModule(applicationDirs: ApplicationDirs): Module =
single<JsonMapper> { JavalinJackson() }
}
@OptIn(DelicateCoroutinesApi::class)
fun applicationSetup() {
Thread.setDefaultUncaughtExceptionHandler { _, throwable ->
KotlinLogging.logger { }.error(throwable) { "unhandled exception" }
}
val mainLoop = LooperThread()
mainLoop.start()
// register Tachidesk's config which is dubbed "ServerConfig"
GlobalConfigManager.registerModule(
ServerConfig.register { GlobalConfigManager.config },
@@ -187,7 +211,12 @@ fun applicationSetup() {
androidCompat.startApp(app)
// Initialize NetworkHelper early
Injekt.get<NetworkHelper>()
Injekt
.get<NetworkHelper>()
.userAgentFlow
.onEach {
System.setProperty("http.agent", it)
}.launchIn(GlobalScope)
// create or update conf file if doesn't exist
try {
@@ -312,4 +341,29 @@ fun applicationSetup() {
// start DownloadManager and restore + resume downloads
DownloadManager.restoreAndResumeDownloads()
GlobalScope.launch {
val logger = KotlinLogging.logger("KCEF")
KCEF.init(
builder = {
progress {
var lastNum = -1
onDownloading {
val num = it.roundToInt()
if (num > lastNum) {
lastNum = num
logger.info { "KCEF download progress: $num%" }
}
}
}
download { github() }
val kcefDir = Path(applicationDirs.dataRoot) / "bin/kcef"
kcefDir.createDirectories()
installDir(kcefDir.toFile())
},
onError = {
it?.printStackTrace()
},
)
}
}