Browser Webview (#1486)

* WebView: Add initial controller

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

* WebView: Prepare page

* WebView: Basic HTML setup

* WebView: Improve navigation

* WebView: Refactor message class deserialization

* WebView: Refactor event message serialization

* WebView: Handle click events

* WebView: Fix events after refactor

* WebView: Fix normalizing of URLs

* WebView: HTML remove navigation buttons

* WebView: Handle more events

* WebView: Handle document change in events

* WebView: Refactor to send mutation events

* WebView: More mouse events

* WebView: Include bubbles, cancelable in event

Those seem to be important

* WebView: Attempt to support nested iframe

* WebView: Handle long titles

* WebView: Avoid setting invalid url

* WebView: Send mousemove

* WebView: Start switch to canvas-based render

* WebView: Send on every render

* WebView: Dynamic size

* WebView: Keyboard events

* WebView: Handle mouse events in CEF

This is important because JS can't click into iFrames, meaning the
previous solution doesn't work for captchas

* WebView: Cleanup

* WebView: Cleanup 2

* WebView: Document title

* WebView: Also send title on address change

* WebView: Load and flush cookies from store

* WebView: remove outdated TODOs

* Offline WebView: Load cookies from store

* Cleanup

* Add KcefCookieManager, need to figure out how to inject it

* ktLintFormat

* Fix a few cookie bugs

* Fix Webview on Windows

* Minor cleanup

* WebView: Remove /tmp image write, lint

* Remove custom cookie manager

* Multiple cookie fixes

* Minor fix

* Minor cleanup and add support for MacOS meta key

* Get enter working

* WebView HTML: Make responsive for mobile pages

* WebView: Translate touch events to mouse scroll

* WebView: Overlay an actual input to allow typing on mobile

Browsers will only show the keyboard if an input is focused. This also
removes the `tabstop` hack.

* WebView: Protect against occasional NullPointerException

* WebView: Use float for clientX/Y

* WebView: Fix ChromeAndroid being a pain

* Simplify enter fix

* NetworkHelper: Fix cache

* Improve CookieStore url matching, fix another cookie conversion issue

* Move distinctBy

* WebView: Mouse direction toggle

* Remove accidentally copied comment

---------

Co-authored-by: Mitchell Syer <Syer10@users.noreply.github.com>
This commit is contained in:
Constantin Piber
2025-07-01 23:28:41 +02:00
committed by GitHub
parent 8a62c6295d
commit a79dc580a5
11 changed files with 1361 additions and 118 deletions

View File

@@ -0,0 +1,105 @@
package suwayomi.tachidesk.global.impl
import io.github.oshai.kotlinlogging.KotlinLogging
import io.javalin.websocket.WsContext
import io.javalin.websocket.WsMessageContext
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import org.eclipse.jetty.websocket.api.CloseStatus
import suwayomi.tachidesk.manga.impl.update.Websocket
object WebView : Websocket<String>() {
private val logger = KotlinLogging.logger {}
private var driver: KcefWebView? = null
override fun addClient(ctx: WsContext) {
if (clients.isNotEmpty()) {
// TODO: allow multiple concurrent accesses?
clients.forEach { it.value.closeSession(CloseStatus(1001, "Other client connected")) }
clients.clear()
}
if (driver == null) {
driver = KcefWebView()
}
super.addClient(ctx)
ctx.enableAutomaticPings()
}
override fun removeClient(ctx: WsContext) {
super.removeClient(ctx)
if (clients.isEmpty()) {
driver?.destroy()
driver = null
}
}
override fun notifyClient(
ctx: WsContext,
value: String?,
) {
if (value != null) {
ctx.send(value)
}
}
@Serializable public sealed class TypeObject
@Serializable
@SerialName("loadUrl")
private data class LoadUrlMessage(
val url: String,
val width: Int,
val height: Int,
) : TypeObject()
@Serializable
@SerialName("resize")
private data class ResizeMessage(
val width: Int,
val height: Int,
) : TypeObject()
@Serializable
@SerialName("event")
public data class JsEventMessage(
val eventType: String,
val clickX: Float,
val clickY: Float,
val button: Int? = null,
val ctrlKey: Boolean? = null,
val shiftKey: Boolean? = null,
val altKey: Boolean? = null,
val metaKey: Boolean? = null,
val key: String? = null,
val code: String? = null,
val clientX: Float? = null,
val clientY: Float? = null,
val deltaY: Float? = null,
) : TypeObject()
override fun handleRequest(ctx: WsMessageContext) {
val dr = driver ?: return
try {
val event = Json.decodeFromString<TypeObject>(ctx.message())
when (event) {
is LoadUrlMessage -> {
val url = event.url
dr.loadUrl(url)
dr.resize(event.width, event.height)
logger.info { "Loading URL $url" }
}
is ResizeMessage -> {
dr.resize(event.width, event.height)
logger.info { "Resize browser" }
}
is JsEventMessage -> {
val type = event.eventType
dr.event(event)
}
}
} catch (e: Exception) {
logger.warn(e) { "Failed to deserialize client request: ${ctx.message()}" }
}
}
}