mirror of
https://github.com/Suwayomi/Suwayomi-Server.git
synced 2026-07-03 19:04:39 -05:00
* Switch to JCEF This is a full implementation, but it does not yet include downloading CEF as KCEF did * Download CEF automatically * Handle and propagate CEF init errors * Lint * Simplify jcef version extract * CEF: Download async * Copy StartupAsync to support handling errors Startup failures are simply swallowed, since they are recorded in the future, but there is no way to get that exception * CEF: Search for release file recursively On Mac, the file is buried a bit deeper than first level, like on Win and Linux * KcefWebViewProvider: Suppress deprecation We need to send those events, even if they are deprecated * Update readme * Optimize imports * Suggestion Co-authored-by: Mitchell Syer <syer10@users.noreply.github.com> * Refactor: stick to `Path` instead of `File` Also extracts the downloading of CEF to a separate method * Lint * Support disabling CEF Co-authored-by: Kolby Moroz Liebl <31669092+kolbyml@users.noreply.github.com> * Move JBR version to build constants Allows embedding into Manifest so docker can later extract the proper version * Create test to verify JCEF dependency matches downloaded JBR * Update server/src/main/kotlin/suwayomi/tachidesk/server/util/CEFManager.kt Co-authored-by: Mitchell Syer <Syer10@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Mitchell Syer <Syer10@users.noreply.github.com> * Fix compile, apply Path suggestions * Download progress * Lint * Fix exception on non-posix * Delete recursively Others can be non-empty * Support disabling CEF at will Not really functional, but nice * Fix test * Exclude masstest unless explicitly requested * PR-CI: Run tests * Add Changelog entry --------- Co-authored-by: Mitchell Syer <syer10@users.noreply.github.com> Co-authored-by: Kolby Moroz Liebl <31669092+kolbyml@users.noreply.github.com>
740 lines
23 KiB
Kotlin
740 lines
23 KiB
Kotlin
package suwayomi.tachidesk.global.impl
|
|
|
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
|
import kotlinx.coroutines.runBlocking
|
|
import kotlinx.serialization.SerialName
|
|
import kotlinx.serialization.Serializable
|
|
import kotlinx.serialization.json.Json
|
|
import okhttp3.Cookie
|
|
import okhttp3.HttpUrl
|
|
import org.cef.CefClient
|
|
import org.cef.CefSettings
|
|
import org.cef.browser.CefBrowser
|
|
import org.cef.browser.CefFrame
|
|
import org.cef.browser.CefRendering
|
|
import org.cef.handler.CefDisplayHandlerAdapter
|
|
import org.cef.handler.CefLoadHandler
|
|
import org.cef.handler.CefLoadHandlerAdapter
|
|
import org.cef.handler.CefRenderHandlerAdapter
|
|
import org.cef.handler.CefRequestHandlerAdapter
|
|
import org.cef.handler.CefResourceRequestHandler
|
|
import org.cef.input.CefTouchEvent
|
|
import org.cef.misc.BoolRef
|
|
import org.cef.network.CefCookie
|
|
import org.cef.network.CefCookieManager
|
|
import org.cef.network.CefRequest
|
|
import uy.kohesive.injekt.injectLazy
|
|
import xyz.nulldev.androidcompat.webkit.CefHelper
|
|
import xyz.nulldev.androidcompat.webkit.dispose
|
|
import xyz.nulldev.androidcompat.webkit.evaluateJavaScript
|
|
import java.awt.Component
|
|
import java.awt.HeadlessException
|
|
import java.awt.Rectangle
|
|
import java.awt.Toolkit
|
|
import java.awt.datatransfer.DataFlavor
|
|
import java.awt.event.InputEvent
|
|
import java.awt.event.KeyEvent
|
|
import java.awt.event.MouseEvent
|
|
import java.awt.event.MouseWheelEvent
|
|
import java.awt.image.BufferedImage
|
|
import java.awt.image.DataBufferInt
|
|
import java.io.ByteArrayOutputStream
|
|
import java.nio.ByteBuffer
|
|
import java.nio.ByteOrder
|
|
import java.util.Date
|
|
import javax.imageio.ImageIO
|
|
import javax.swing.JPanel
|
|
|
|
class KcefWebView {
|
|
private val logger = KotlinLogging.logger {}
|
|
private val renderHandler = RenderHandler()
|
|
private var kcefClient: CefClient? = null
|
|
private var browser: CefBrowser? = null
|
|
private var width = 1000
|
|
private var height = 1000
|
|
|
|
companion object {
|
|
private val networkHelper: NetworkHelper by injectLazy()
|
|
|
|
fun Cookie.toCefCookie(): CefCookie {
|
|
val cookie = this
|
|
return CefCookie(
|
|
cookie.name,
|
|
cookie.value,
|
|
if (cookie.hostOnly) {
|
|
cookie.domain
|
|
} else {
|
|
"." + cookie.domain
|
|
},
|
|
cookie.path,
|
|
cookie.secure,
|
|
cookie.httpOnly,
|
|
Date(),
|
|
null,
|
|
cookie.expiresAt < 253402300799999L, // okhttp3.internal.http.MAX_DATE
|
|
Date(cookie.expiresAt),
|
|
)
|
|
}
|
|
}
|
|
|
|
@Serializable
|
|
sealed class Event
|
|
|
|
@Serializable
|
|
@SerialName("consoleMessage")
|
|
private data class ConsoleEvent(
|
|
val severity: Int,
|
|
val message: String,
|
|
val source: String,
|
|
val line: Int,
|
|
) : Event()
|
|
|
|
@Serializable
|
|
@SerialName("addressChange")
|
|
private data class AddressEvent(
|
|
val url: String,
|
|
val title: String,
|
|
) : Event()
|
|
|
|
@Serializable
|
|
@SerialName("statusChange")
|
|
private data class StatusEvent(
|
|
val message: String,
|
|
) : Event()
|
|
|
|
@Suppress("ArrayInDataClass")
|
|
@Serializable
|
|
@SerialName("render")
|
|
private data class RenderEvent(
|
|
val image: ByteArray,
|
|
) : Event()
|
|
|
|
@Serializable
|
|
@SerialName("load")
|
|
private data class LoadEvent(
|
|
val url: String,
|
|
val title: String,
|
|
val status: Int = 0,
|
|
val error: String? = null,
|
|
) : Event()
|
|
|
|
@Serializable
|
|
@SerialName("copy")
|
|
private data class CopyEvent(
|
|
val content: String,
|
|
) : Event()
|
|
|
|
private inner class DisplayHandler : CefDisplayHandlerAdapter() {
|
|
override fun onConsoleMessage(
|
|
browser: CefBrowser,
|
|
level: CefSettings.LogSeverity,
|
|
message: String,
|
|
source: String,
|
|
line: Int,
|
|
): Boolean {
|
|
WebView.notifyAllClients(
|
|
Json.encodeToString<Event>(
|
|
ConsoleEvent(level.ordinal, message, source, line),
|
|
),
|
|
)
|
|
logger.trace { "$source:$line: $message" }
|
|
return true
|
|
}
|
|
|
|
override fun onAddressChange(
|
|
browser: CefBrowser,
|
|
frame: CefFrame,
|
|
url: String,
|
|
) {
|
|
if (!frame.isMain) return
|
|
this@KcefWebView.browser!!.evaluateJavaScript("return document.title") {
|
|
WebView.notifyAllClients(
|
|
Json.encodeToString<Event>(
|
|
AddressEvent(url, it ?: ""),
|
|
),
|
|
)
|
|
}
|
|
flush()
|
|
}
|
|
|
|
override fun onStatusMessage(
|
|
browser: CefBrowser,
|
|
value: String,
|
|
) {
|
|
WebView.notifyAllClients(
|
|
Json.encodeToString<Event>(
|
|
StatusEvent(value),
|
|
),
|
|
)
|
|
}
|
|
}
|
|
|
|
private inner class LoadHandler : CefLoadHandlerAdapter() {
|
|
override fun onLoadEnd(
|
|
browser: CefBrowser,
|
|
frame: CefFrame,
|
|
httpStatusCode: Int,
|
|
) {
|
|
logger.trace { "Load event: ${frame.name} - ${frame.url}" }
|
|
if (httpStatusCode > 0 && frame.isMain) handleLoad(frame.url, httpStatusCode)
|
|
flush()
|
|
}
|
|
|
|
override fun onLoadError(
|
|
browser: CefBrowser,
|
|
frame: CefFrame,
|
|
errorCode: CefLoadHandler.ErrorCode,
|
|
errorText: String,
|
|
failedUrl: String,
|
|
) {
|
|
if (frame.isMain) handleLoad(failedUrl, 0, errorText)
|
|
}
|
|
}
|
|
|
|
private inner class RequestHandler : CefRequestHandlerAdapter() {
|
|
override fun getResourceRequestHandler(
|
|
browser: CefBrowser,
|
|
frame: CefFrame,
|
|
request: CefRequest,
|
|
isNavigation: Boolean,
|
|
isDownload: Boolean,
|
|
requestInitiator: String,
|
|
disableDefaultHandling: BoolRef,
|
|
): CefResourceRequestHandler? {
|
|
logger.trace { "Load resource: ${frame.name} - ${request.url}" }
|
|
return null
|
|
}
|
|
}
|
|
|
|
// Loosely based on
|
|
// https://github.com/JetBrains/jcef/blob/main/java/org/cef/browser/CefBrowserOsr.java
|
|
private inner class RenderHandler : CefRenderHandlerAdapter() {
|
|
var myImage: BufferedImage? = null
|
|
|
|
override fun getViewRect(browser: CefBrowser): Rectangle = Rectangle(0, 0, width, height)
|
|
|
|
override fun onPaint(
|
|
browser: CefBrowser,
|
|
popup: Boolean,
|
|
dirtyRects: Array<Rectangle>,
|
|
buffer: ByteBuffer,
|
|
width: Int,
|
|
height: Int,
|
|
) {
|
|
var image = myImage ?: BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB_PRE)
|
|
|
|
if (image.width != width || image.height != height) {
|
|
image = BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB_PRE)
|
|
}
|
|
|
|
val dst = (image.raster.getDataBuffer() as DataBufferInt).getData()
|
|
val src = buffer.order(ByteOrder.LITTLE_ENDIAN).asIntBuffer()
|
|
src.get(dst)
|
|
|
|
myImage = image
|
|
val stream = ByteArrayOutputStream()
|
|
val success = ImageIO.write(myImage, "png", stream)
|
|
if (!success) {
|
|
throw IllegalStateException("Failed to convert image to PNG")
|
|
}
|
|
|
|
WebView.notifyAllClients(
|
|
Json.encodeToString<Event>(
|
|
RenderEvent(stream.toByteArray()),
|
|
),
|
|
)
|
|
}
|
|
}
|
|
|
|
init {
|
|
destroy()
|
|
kcefClient =
|
|
runBlocking {
|
|
CefHelper.createClient().apply {
|
|
addDisplayHandler(DisplayHandler())
|
|
addLoadHandler(LoadHandler())
|
|
addRequestHandler(RequestHandler())
|
|
}
|
|
}
|
|
|
|
logger.debug { "Start loading cookies" }
|
|
CefCookieManager.getGlobalManager().apply {
|
|
val cookies = networkHelper.cookieStore.getStoredCookies()
|
|
for (cookie in cookies) {
|
|
try {
|
|
if (!setCookie(
|
|
"https://" + cookie.domain,
|
|
cookie.toCefCookie(),
|
|
)
|
|
) {
|
|
throw Exception()
|
|
}
|
|
} catch (e: Exception) {
|
|
logger.warn(e) { "Loading cookie ${cookie.name} failed" }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fun destroy() {
|
|
flush()
|
|
browser?.close(true)
|
|
browser?.dispose()
|
|
browser = null
|
|
kcefClient?.dispose()
|
|
kcefClient = null
|
|
}
|
|
|
|
fun loadUrl(url: String) {
|
|
browser?.close(true)
|
|
browser?.dispose()
|
|
browser =
|
|
kcefClient!!
|
|
.createBrowser(
|
|
url,
|
|
CefRendering.CefRenderingWithHandler(renderHandler, JPanel()),
|
|
false,
|
|
// NOTE: with a context, we don't seem to be getting any cookies
|
|
).apply {
|
|
// NOTE: Without this, we don't seem to be receiving any events
|
|
createImmediately()
|
|
}
|
|
}
|
|
|
|
fun resize(
|
|
width: Int,
|
|
height: Int,
|
|
) {
|
|
this.width = width
|
|
this.height = height
|
|
browser?.wasResized(width, height)
|
|
}
|
|
|
|
private fun flush() {
|
|
if (browser == null) return
|
|
logger.trace { "Start cookie flush" }
|
|
CefCookieManager.getGlobalManager().visitAllCookies { it, _, _, _ ->
|
|
try {
|
|
networkHelper.cookieStore.addAll(
|
|
HttpUrl
|
|
.Builder()
|
|
.scheme("http")
|
|
.host(it.domain.removePrefix("."))
|
|
.build(),
|
|
listOf(
|
|
Cookie
|
|
.Builder()
|
|
.name(it.name)
|
|
.value(it.value)
|
|
.path(if (it.path.startsWith('/')) it.path else "/" + it.path)
|
|
.domain(it.domain.removePrefix("."))
|
|
.apply {
|
|
if (it.hasExpires) {
|
|
expiresAt(it.expires.time)
|
|
} else {
|
|
expiresAt(Long.MAX_VALUE)
|
|
}
|
|
if (it.httponly) {
|
|
httpOnly()
|
|
}
|
|
if (it.secure) {
|
|
secure()
|
|
}
|
|
if (!it.domain.startsWith('.')) {
|
|
hostOnlyDomain(it.domain.removePrefix("."))
|
|
}
|
|
}.build(),
|
|
),
|
|
)
|
|
} catch (e: Exception) {
|
|
logger.warn(e) { "Writing cookie ${it.name} failed" }
|
|
}
|
|
return@visitAllCookies true
|
|
}
|
|
}
|
|
|
|
private fun keyEvent(
|
|
msg: WebView.JsEventMessage,
|
|
component: Component,
|
|
id: Int,
|
|
modifier: Int,
|
|
): KeyEvent? {
|
|
val char = if (msg.key?.length == 1) msg.key[0] else KeyEvent.CHAR_UNDEFINED
|
|
return keyEvent(char, component, id, modifier, msg.key)
|
|
}
|
|
|
|
private fun keyEvent(
|
|
char: Char,
|
|
component: Component,
|
|
id: Int,
|
|
modifier: Int,
|
|
strKey: String? = null,
|
|
): KeyEvent? {
|
|
val code =
|
|
when (char.uppercaseChar()) {
|
|
in 'A'..'Z', in '0'..'9' -> {
|
|
char.uppercaseChar().code
|
|
}
|
|
|
|
'&' -> {
|
|
KeyEvent.VK_AMPERSAND
|
|
}
|
|
|
|
'*' -> {
|
|
KeyEvent.VK_ASTERISK
|
|
}
|
|
|
|
'@' -> {
|
|
KeyEvent.VK_AT
|
|
}
|
|
|
|
'\\' -> {
|
|
KeyEvent.VK_BACK_SLASH
|
|
}
|
|
|
|
'{' -> {
|
|
KeyEvent.VK_BRACELEFT
|
|
}
|
|
|
|
'}' -> {
|
|
KeyEvent.VK_BRACERIGHT
|
|
}
|
|
|
|
'^' -> {
|
|
KeyEvent.VK_CIRCUMFLEX
|
|
}
|
|
|
|
']' -> {
|
|
KeyEvent.VK_CLOSE_BRACKET
|
|
}
|
|
|
|
':' -> {
|
|
KeyEvent.VK_COLON
|
|
}
|
|
|
|
',' -> {
|
|
KeyEvent.VK_COMMA
|
|
}
|
|
|
|
'$' -> {
|
|
KeyEvent.VK_DOLLAR
|
|
}
|
|
|
|
'=' -> {
|
|
KeyEvent.VK_EQUALS
|
|
}
|
|
|
|
'€' -> {
|
|
KeyEvent.VK_EURO_SIGN
|
|
}
|
|
|
|
'!' -> {
|
|
KeyEvent.VK_EXCLAMATION_MARK
|
|
}
|
|
|
|
'>' -> {
|
|
KeyEvent.VK_GREATER
|
|
}
|
|
|
|
'(' -> {
|
|
KeyEvent.VK_LEFT_PARENTHESIS
|
|
}
|
|
|
|
'<' -> {
|
|
KeyEvent.VK_LESS
|
|
}
|
|
|
|
'-' -> {
|
|
KeyEvent.VK_MINUS
|
|
}
|
|
|
|
'#' -> {
|
|
KeyEvent.VK_NUMBER_SIGN
|
|
}
|
|
|
|
'[' -> {
|
|
KeyEvent.VK_OPEN_BRACKET
|
|
}
|
|
|
|
'.' -> {
|
|
KeyEvent.VK_PERIOD
|
|
}
|
|
|
|
'+' -> {
|
|
KeyEvent.VK_PLUS
|
|
}
|
|
|
|
'\'' -> {
|
|
KeyEvent.VK_QUOTE
|
|
}
|
|
|
|
'"' -> {
|
|
KeyEvent.VK_QUOTEDBL
|
|
}
|
|
|
|
')' -> {
|
|
KeyEvent.VK_RIGHT_PARENTHESIS
|
|
}
|
|
|
|
';' -> {
|
|
KeyEvent.VK_SEMICOLON
|
|
}
|
|
|
|
'/' -> {
|
|
KeyEvent.VK_SLASH
|
|
}
|
|
|
|
' ' -> {
|
|
KeyEvent.VK_SPACE
|
|
}
|
|
|
|
'_' -> {
|
|
KeyEvent.VK_UNDERSCORE
|
|
}
|
|
|
|
else -> {
|
|
when (strKey) {
|
|
"Alt" -> KeyEvent.VK_ALT
|
|
"Backspace" -> KeyEvent.VK_BACK_SPACE
|
|
"Delete" -> KeyEvent.VK_DELETE
|
|
"CapsLock" -> KeyEvent.VK_CAPS_LOCK
|
|
"Control" -> KeyEvent.VK_CONTROL
|
|
"ArrowDown" -> KeyEvent.VK_DOWN
|
|
"End" -> KeyEvent.VK_END
|
|
"Enter" -> KeyEvent.VK_ENTER
|
|
"Escape" -> KeyEvent.VK_ESCAPE
|
|
"F1" -> KeyEvent.VK_F1
|
|
"F2" -> KeyEvent.VK_F2
|
|
"F3" -> KeyEvent.VK_F3
|
|
"F4" -> KeyEvent.VK_F4
|
|
"F5" -> KeyEvent.VK_F5
|
|
"F6" -> KeyEvent.VK_F6
|
|
"F7" -> KeyEvent.VK_F7
|
|
"F8" -> KeyEvent.VK_F8
|
|
"F9" -> KeyEvent.VK_F9
|
|
"F10" -> KeyEvent.VK_F10
|
|
"F11" -> KeyEvent.VK_F11
|
|
"F12" -> KeyEvent.VK_F12
|
|
"Home" -> KeyEvent.VK_HOME
|
|
"Insert" -> KeyEvent.VK_INSERT
|
|
"ArrowLeft" -> KeyEvent.VK_LEFT
|
|
"Meta" -> KeyEvent.VK_META
|
|
"NumLock" -> KeyEvent.VK_NUM_LOCK
|
|
"PageDown" -> KeyEvent.VK_PAGE_DOWN
|
|
"PageUp" -> KeyEvent.VK_PAGE_UP
|
|
"Pause" -> KeyEvent.VK_PAUSE
|
|
"ArrowRight" -> KeyEvent.VK_RIGHT
|
|
"ScrollLock" -> KeyEvent.VK_SCROLL_LOCK
|
|
"Shift" -> KeyEvent.VK_SHIFT
|
|
"Tab" -> KeyEvent.VK_TAB
|
|
"ArrowUp" -> KeyEvent.VK_UP
|
|
else -> KeyEvent.VK_UNDEFINED
|
|
}
|
|
}
|
|
}
|
|
if (id == KeyEvent.KEY_TYPED) {
|
|
if (char == KeyEvent.CHAR_UNDEFINED && code != KeyEvent.VK_ENTER) return null
|
|
return KeyEvent(
|
|
component,
|
|
id,
|
|
0L,
|
|
modifier,
|
|
KeyEvent.VK_UNDEFINED,
|
|
if (code == KeyEvent.VK_ENTER) code.toChar() else char,
|
|
KeyEvent.KEY_LOCATION_UNKNOWN,
|
|
)
|
|
}
|
|
return KeyEvent(
|
|
component,
|
|
id,
|
|
0L,
|
|
modifier,
|
|
code,
|
|
if (code == KeyEvent.VK_ENTER) code.toChar() else char,
|
|
KeyEvent.KEY_LOCATION_STANDARD,
|
|
)
|
|
}
|
|
|
|
fun event(msg: WebView.JsEventMessage) {
|
|
val component = browser?.uiComponent ?: return
|
|
val type = msg.eventType
|
|
val clickX = msg.clickX
|
|
val clickY = msg.clickY
|
|
val modifier =
|
|
(
|
|
// Alt support is removed, since browsers already translate, so this messes with translation, see #1575
|
|
// (if (msg.altKey == true) InputEvent.ALT_DOWN_MASK else 0) or
|
|
(if (msg.ctrlKey == true) InputEvent.CTRL_DOWN_MASK else 0) or
|
|
(if (msg.shiftKey == true) InputEvent.SHIFT_DOWN_MASK else 0) or
|
|
(if (msg.metaKey == true) InputEvent.META_DOWN_MASK else 0)
|
|
)
|
|
|
|
if (type == "wheel") {
|
|
val d = msg.deltaY?.toInt() ?: 1
|
|
val ev =
|
|
MouseWheelEvent(
|
|
component,
|
|
0,
|
|
0L,
|
|
modifier,
|
|
clickX.toInt(),
|
|
clickY.toInt(),
|
|
0,
|
|
false,
|
|
MouseWheelEvent.WHEEL_UNIT_SCROLL,
|
|
-d,
|
|
1,
|
|
)
|
|
browser!!.sendMouseWheelEvent(ev)
|
|
return
|
|
}
|
|
if (type == "keydown") {
|
|
browser!!.sendKeyEvent(keyEvent(msg, component, KeyEvent.KEY_PRESSED, modifier)!!)
|
|
keyEvent(msg, component, KeyEvent.KEY_TYPED, modifier)?.let { browser!!.sendKeyEvent(it) }
|
|
return
|
|
}
|
|
if (type == "keyup") {
|
|
browser!!.sendKeyEvent(keyEvent(msg, component, KeyEvent.KEY_RELEASED, modifier)!!)
|
|
return
|
|
}
|
|
if (type == "mousedown" || type == "mouseup" || type == "click") {
|
|
val id =
|
|
when (type) {
|
|
"mousedown" -> MouseEvent.MOUSE_PRESSED
|
|
"mouseup" -> MouseEvent.MOUSE_PRESSED
|
|
"click" -> MouseEvent.MOUSE_CLICKED
|
|
else -> 0
|
|
}
|
|
val mouseModifier =
|
|
when (msg.button ?: 0) {
|
|
0 -> MouseEvent.BUTTON1_DOWN_MASK
|
|
1 -> MouseEvent.BUTTON2_DOWN_MASK
|
|
2 -> MouseEvent.BUTTON3_DOWN_MASK
|
|
else -> 0
|
|
}
|
|
val button =
|
|
when (msg.button ?: 0) {
|
|
0 -> MouseEvent.BUTTON1
|
|
1 -> MouseEvent.BUTTON2
|
|
2 -> MouseEvent.BUTTON3
|
|
else -> 0
|
|
}
|
|
val ev =
|
|
MouseEvent(
|
|
component,
|
|
id,
|
|
0L,
|
|
modifier or mouseModifier,
|
|
clickX.toInt(),
|
|
clickY.toInt(),
|
|
msg.clientX?.toInt() ?: 0,
|
|
msg.clientY?.toInt() ?: 0,
|
|
1,
|
|
true,
|
|
button,
|
|
)
|
|
browser!!.sendMouseEvent(ev)
|
|
val evType =
|
|
when (type) {
|
|
"mousedown" -> CefTouchEvent.EventType.PRESSED
|
|
"mouseup" -> CefTouchEvent.EventType.RELEASED
|
|
else -> CefTouchEvent.EventType.MOVED
|
|
}
|
|
val ev2 =
|
|
CefTouchEvent(
|
|
0,
|
|
clickX,
|
|
clickY,
|
|
10.0f,
|
|
10.0f,
|
|
0.0f,
|
|
1.0f,
|
|
evType,
|
|
modifier,
|
|
CefTouchEvent.PointerType.MOUSE,
|
|
)
|
|
browser!!.sendTouchEvent(ev2)
|
|
return
|
|
}
|
|
if (type == "mousemove") {
|
|
val ev =
|
|
MouseEvent(
|
|
component,
|
|
MouseEvent.MOUSE_MOVED,
|
|
0L,
|
|
modifier,
|
|
clickX.toInt(),
|
|
clickY.toInt(),
|
|
msg.clientX?.toInt() ?: 0,
|
|
msg.clientY?.toInt() ?: 0,
|
|
0,
|
|
true,
|
|
0,
|
|
)
|
|
browser!!.sendMouseEvent(ev)
|
|
return
|
|
}
|
|
}
|
|
|
|
fun paste(msg: String) {
|
|
val component = browser?.uiComponent ?: return
|
|
for (c in msg) {
|
|
browser!!.sendKeyEvent(keyEvent(c, component, KeyEvent.KEY_PRESSED, 0)!!)
|
|
keyEvent(c, component, KeyEvent.KEY_TYPED, 0)?.let { browser!!.sendKeyEvent(it) }
|
|
browser!!.sendKeyEvent(keyEvent(c, component, KeyEvent.KEY_RELEASED, 0)!!)
|
|
}
|
|
}
|
|
|
|
fun copy() {
|
|
val frame = browser?.focusedFrame ?: return
|
|
frame.copy()
|
|
val clip =
|
|
try {
|
|
Toolkit.getDefaultToolkit().getSystemClipboard()
|
|
} catch (e: HeadlessException) {
|
|
logger.warn(e) { "Failed to get clipboard" }
|
|
return
|
|
}
|
|
val text =
|
|
try {
|
|
clip.getData(DataFlavor.stringFlavor) as String
|
|
} catch (e: Exception) {
|
|
logger.warn(e) { "Failed to get clipboard contents" }
|
|
return
|
|
}
|
|
WebView.notifyAllClients(
|
|
Json.encodeToString<Event>(
|
|
CopyEvent(text),
|
|
),
|
|
)
|
|
}
|
|
|
|
fun canGoBack(): Boolean = browser!!.canGoBack()
|
|
|
|
fun goBack() {
|
|
browser!!.goBack()
|
|
}
|
|
|
|
fun canGoForward(): Boolean = browser!!.canGoForward()
|
|
|
|
fun goForward() {
|
|
browser!!.goForward()
|
|
}
|
|
|
|
private fun handleLoad(
|
|
url: String,
|
|
status: Int = 0,
|
|
error: String? = null,
|
|
) {
|
|
browser!!.evaluateJavaScript("return document.title") {
|
|
logger.trace { "Load finished with title $it" }
|
|
WebView.notifyAllClients(
|
|
Json.encodeToString<Event>(
|
|
LoadEvent(url, it ?: "", status, error),
|
|
),
|
|
)
|
|
}
|
|
}
|
|
}
|