Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ sealed class PlatformWebSettings {

data class DesktopWebSettings(
var transparent: Boolean = true,
var dataDirectory: String? = null,
var initScript: String? = null,
var enableClipboard: Boolean = true,
var enableDevtools: Boolean = false,
var enableNavigationGestures: Boolean = true,
var incognito: Boolean = false,
var autoplayWithoutUserInteraction: Boolean = false,
var focused: Boolean = true,
) : PlatformWebSettings()

data class IOSWebSettings(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,34 +1,56 @@
package io.github.kdroidfilter.webview.web

import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.awt.SwingPanel
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import io.github.kdroidfilter.webview.cookie.WryCookieManager
import io.github.kdroidfilter.webview.jsbridge.WebViewJsBridge
import io.github.kdroidfilter.webview.jsbridge.parseJsMessage
import io.github.kdroidfilter.webview.request.WebRequest
import io.github.kdroidfilter.webview.request.WebRequestInterceptResult
import io.github.kdroidfilter.webview.wry.Rgba
import kotlinx.coroutines.delay

actual class WebViewFactoryParam(
val state: WebViewState,
val fileContent: String = "",
val userAgent: String? = null,
)

actual fun defaultWebViewFactory(param: WebViewFactoryParam): NativeWebView =
when (val content = param.state.content) {
is WebContent.Url -> NativeWebView(content.url, param.userAgent ?: param.state.webSettings.customUserAgentString)
else -> NativeWebView("about:blank", param.userAgent ?: param.state.webSettings.customUserAgentString)
is WebContent.Url -> NativeWebView(
initialUrl = content.url,
customUserAgent = param.state.webSettings.customUserAgentString,
dataDirectory = param.state.webSettings.desktopWebSettings.dataDirectory,
supportZoom = param.state.webSettings.supportZoom,
backgroundColor = param.state.webSettings.backgroundColor.toRgba(),
transparent = param.state.webSettings.desktopWebSettings.transparent,
initScript = param.state.webSettings.desktopWebSettings.initScript,
enableClipboard = param.state.webSettings.desktopWebSettings.enableClipboard,
enableDevtools = param.state.webSettings.desktopWebSettings.enableDevtools,
enableNavigationGestures = param.state.webSettings.desktopWebSettings.enableNavigationGestures,
incognito = param.state.webSettings.desktopWebSettings.incognito,
autoplayWithoutUserInteraction = param.state.webSettings.desktopWebSettings.autoplayWithoutUserInteraction,
focused = param.state.webSettings.desktopWebSettings.focused
)

else -> NativeWebView(
initialUrl = "about:blank",
customUserAgent = param.state.webSettings.customUserAgentString,
dataDirectory = param.state.webSettings.desktopWebSettings.dataDirectory,
supportZoom = param.state.webSettings.supportZoom,
backgroundColor = param.state.webSettings.backgroundColor.toRgba(),
transparent = param.state.webSettings.desktopWebSettings.transparent,
initScript = param.state.webSettings.desktopWebSettings.initScript,
enableClipboard = param.state.webSettings.desktopWebSettings.enableClipboard,
enableDevtools = param.state.webSettings.desktopWebSettings.enableDevtools,
enableNavigationGestures = param.state.webSettings.desktopWebSettings.enableNavigationGestures,
incognito = param.state.webSettings.desktopWebSettings.incognito,
autoplayWithoutUserInteraction = param.state.webSettings.desktopWebSettings.autoplayWithoutUserInteraction,
focused = param.state.webSettings.desktopWebSettings.focused
)
}

@Composable
Expand All @@ -44,18 +66,25 @@ actual fun ActualWebView(
val currentOnDispose by rememberUpdatedState(onDispose)
val scope = rememberCoroutineScope()

val desiredUserAgent = state.webSettings.customUserAgentString?.trim()?.takeIf { it.isNotEmpty() }
var effectiveUserAgent by remember { mutableStateOf(desiredUserAgent) }
val desiredSettingsKey = state.webSettings.let {
listOf(
it.customUserAgentString?.trim()?.takeIf(String::isNotEmpty),
it.supportZoom,
it.backgroundColor,
)
}

var effectiveSettingsKey by remember { mutableStateOf(desiredSettingsKey) }

LaunchedEffect(desiredUserAgent) {
if (desiredUserAgent == effectiveUserAgent) return@LaunchedEffect
// Wry applies user-agent at creation time, so recreate the webview after a small debounce.
delay(400)
effectiveUserAgent = desiredUserAgent
LaunchedEffect(desiredSettingsKey) {
if (desiredSettingsKey != effectiveSettingsKey) {
delay(400)
effectiveSettingsKey = desiredSettingsKey
}
}

key(effectiveUserAgent) {
val nativeWebView = remember(state, factory) { factory(WebViewFactoryParam(state, userAgent = effectiveUserAgent)) }
key(effectiveSettingsKey) {
val nativeWebView = remember(state, factory) { factory(WebViewFactoryParam(state)) }

val desktopWebView =
remember(nativeWebView, scope, webViewJsBridge) {
Expand All @@ -73,52 +102,58 @@ actual fun ActualWebView(
}

// Poll native state (URL/loading/title/nav) and drain IPC messages for JS bridge.
LaunchedEffect(nativeWebView, state, navigator, webViewJsBridge) {
while (true) {
if (!nativeWebView.isReady()) {
if (state.loadingState !is LoadingState.Initializing) {
state.loadingState = LoadingState.Initializing
listOf(nativeWebView, state, navigator, webViewJsBridge).let {
LaunchedEffect(it) {
while (true) {
if (!nativeWebView.isReady()) {
if (state.loadingState !is LoadingState.Initializing) {
state.loadingState = LoadingState.Initializing
}
delay(50)
continue
}
delay(50)
continue
}

val isLoading = nativeWebView.isLoading()
state.loadingState =
if (isLoading) {
val current = state.loadingState
val next =
when (current) {
is LoadingState.Loading -> (current.progress + 0.02f).coerceAtMost(0.9f)
else -> 0.1f
}
LoadingState.Loading(next)
} else {
LoadingState.Finished
val isLoading = nativeWebView.isLoading()
state.loadingState =
if (isLoading) {
val next =
when (val current = state.loadingState) {
is LoadingState.Loading -> (current.progress + 0.02f).coerceAtMost(0.9f)
else -> 0.1f
}
LoadingState.Loading(next)
} else {
LoadingState.Finished
}

val url = nativeWebView.getCurrentUrl()
if (!url.isNullOrBlank()) {
if (!isLoading || state.lastLoadedUrl.isNullOrBlank()) {
state.lastLoadedUrl = url
}
}

val url = nativeWebView.getCurrentUrl()
if (!url.isNullOrBlank()) {
if (!isLoading || state.lastLoadedUrl.isNullOrBlank()) {
state.lastLoadedUrl = url
val title = nativeWebView.getTitle()
if (!title.isNullOrBlank()) {
state.pageTitle = title
}
}

val title = nativeWebView.getTitle()
if (!title.isNullOrBlank()) {
state.pageTitle = title
}
navigator.canGoBack = nativeWebView.canGoBack()
navigator.canGoForward = nativeWebView.canGoForward()

navigator.canGoBack = nativeWebView.canGoBack()
navigator.canGoForward = nativeWebView.canGoForward()
delay(250)
}
}

if (webViewJsBridge != null) {
for (raw in nativeWebView.drainIpcMessages()) {
parseJsMessage(raw)?.let { webViewJsBridge.dispatch(it) }
LaunchedEffect(it) {
while (true) {
if (webViewJsBridge != null) {
for (raw in nativeWebView.drainIpcMessages()) {
parseJsMessage(raw)?.let { webViewJsBridge.dispatch(it) }
}
}
delay(50)
}

delay(250)
}
}

Expand All @@ -137,7 +172,8 @@ actual fun ActualWebView(
method = "GET",
)

return@a when (val interceptResult = navigator.requestInterceptor.onInterceptUrlRequest(webRequest, navigator)) {
return@a when (val interceptResult =
navigator.requestInterceptor.onInterceptUrlRequest(webRequest, navigator)) {
WebRequestInterceptResult.Allow -> true

WebRequestInterceptResult.Reject -> false
Expand Down Expand Up @@ -174,3 +210,12 @@ actual fun ActualWebView(
}
}
}

private fun Color.toRgba(): Rgba {
val argb: Int = this.toArgb() // 0xAARRGGBB (sRGB)
val a: UByte = ((argb ushr 24) and 0xFF).toUByte()
val r: UByte = ((argb ushr 16) and 0xFF).toUByte()
val g: UByte = ((argb ushr 8) and 0xFF).toUByte()
val b: UByte = (argb and 0xFF).toUByte()
return Rgba(r = r, g = g, b = b, a = a)
}
12 changes: 6 additions & 6 deletions wrywebview/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion wrywebview/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ path = "src/main/rust/lib.rs"
[dependencies]
thiserror = "2.0.11"
uniffi = "0.29.4"
wry = "0.53.5"
wry = { version = "0.54.2", features = ["devtools"] }

[profile.release]
opt-level = "z"
Expand Down
Loading