Skip to content

Commit 044ef8a

Browse files
authored
Merge pull request #667 from ooni/javafx
Desktop webview support with JavaFX
2 parents d67afa7 + 8b1723c commit 044ef8a

File tree

4 files changed

+139
-1
lines changed

4 files changed

+139
-1
lines changed

composeApp/build.gradle.kts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import com.android.build.api.variant.FilterConfiguration.FilterType.ABI
2+
import org.gradle.internal.os.OperatingSystem
23
import org.jetbrains.compose.ExperimentalComposeLibrary
34
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
45
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
@@ -123,6 +124,14 @@ kotlin {
123124
implementation(files("./src/desktopMain/libs/oonimkall.jar"))
124125
implementation(compose.desktop.currentOs)
125126
implementation(libs.bundles.desktop)
127+
128+
// As JavaFX have platform-specific dependencies, we need to add them manually
129+
val fxParts = listOf("base", "graphics", "controls", "media", "web", "swing")
130+
val jvmVersion = 17
131+
val fxSuffix = getJavaFxSuffix()
132+
fxParts.forEach {
133+
implementation("org.openjfx:javafx-$it:$jvmVersion:$fxSuffix")
134+
}
126135
}
127136
}
128137
// Testing
@@ -542,3 +551,14 @@ fun isFdroidTaskRequested(): Boolean {
542551
fun isDebugTaskRequested(): Boolean {
543552
return gradle.startParameter.taskRequests.flatMap { it.args }.any { it.contains("Debug") }
544553
}
554+
555+
fun getJavaFxSuffix(): String {
556+
val os = OperatingSystem.current()
557+
val arch = System.getProperty("os.arch")
558+
return when {
559+
os.isMacOsX -> if (arch == "aarch64") "mac-aarch64" else "mac"
560+
os.isWindows -> "win"
561+
os.isLinux -> if (arch == "aarch64") "linux-aarch64" else "linux"
562+
else -> throw IllegalStateException("Unknown OS: $os")
563+
}
564+
}

composeApp/src/androidMain/kotlin/org/ooni/probe/ui/shared/OoniWebView.kt renamed to composeApp/src/androidMain/kotlin/org/ooni/probe/ui/shared/OoniWebView.android.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import android.webkit.WebView
1212
import android.webkit.WebViewClient
1313
import androidx.activity.compose.BackHandler
1414
import androidx.compose.runtime.Composable
15-
import androidx.compose.runtime.getValue
1615
import androidx.compose.ui.Modifier
1716
import androidx.compose.ui.viewinterop.AndroidView
1817

composeApp/src/desktopMain/kotlin/org/ooni/probe/ui/shared/OoniWebView.desktop.kt

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,130 @@ package org.ooni.probe.ui.shared
22

33
import androidx.compose.runtime.Composable
44
import androidx.compose.ui.Modifier
5+
import androidx.compose.ui.awt.SwingPanel
6+
import javafx.application.Platform
7+
import javafx.concurrent.Worker
8+
import javafx.embed.swing.JFXPanel
9+
import javafx.scene.Scene
10+
import javafx.scene.layout.StackPane
11+
import javafx.scene.web.WebView
12+
import java.net.URL
13+
import kotlin.io.encoding.Base64
514

615
@Composable
716
actual fun OoniWebView(
817
controller: OoniWebViewController,
918
modifier: Modifier,
1019
allowedDomains: List<String>,
1120
) {
21+
val event = controller.rememberNextEvent()
22+
23+
SwingPanel(
24+
factory = {
25+
controller.state = OoniWebViewController.State.Initializing
26+
27+
JFXPanel().apply {
28+
Platform.setImplicitExit(false) // Otherwise, webView will not show the second time
29+
Platform.runLater {
30+
31+
val webView = WebView().apply {
32+
isVisible = true
33+
@Suppress("SetJavaScriptEnabled")
34+
engine.isJavaScriptEnabled = true
35+
36+
// Set up load listeners
37+
engine.loadWorker.stateProperty().addListener { _, _, newValue ->
38+
when (newValue) {
39+
Worker.State.SCHEDULED -> {
40+
controller.state = OoniWebViewController.State.Loading(0f)
41+
}
42+
43+
Worker.State.RUNNING -> {
44+
val progress = engine.loadWorker.progress
45+
controller.state =
46+
OoniWebViewController.State.Loading(progress.toFloat())
47+
}
48+
49+
Worker.State.SUCCEEDED -> {
50+
controller.state = OoniWebViewController.State.Successful
51+
controller.canGoBack = engine.history.currentIndex > 0
52+
}
53+
54+
Worker.State.FAILED -> {
55+
controller.state = OoniWebViewController.State.Failure
56+
controller.canGoBack = engine.history.currentIndex > 0
57+
}
58+
59+
else -> {}
60+
}
61+
}
62+
63+
// Domain restriction
64+
engine.locationProperty().addListener { _, _, newLocation ->
65+
try {
66+
val host = URL(newLocation).host
67+
val allowed = allowedDomains.any { domain ->
68+
host.matches(Regex("^(.*\\.)?$domain$"))
69+
}
70+
71+
if (!allowed) {
72+
engine.load("about:blank")
73+
}
74+
} catch (e: Exception) {
75+
// Invalid URL, ignore
76+
}
77+
controller.canGoBack = engine.history.currentIndex > 0
78+
}
79+
80+
val css = """
81+
body {
82+
-ms-overflow-style: none; /* Internet Explorer 10+ */
83+
scrollbar-width: none; /* Firefox */
84+
}
85+
body::-webkit-scrollbar {
86+
display: none; /* Safari and Chrome */
87+
}
88+
""".trimIndent()
89+
val cssData = Base64.encode(css.encodeToByteArray())
90+
engine.userStyleSheetLocation =
91+
"data:text/css;charset=utf-8;base64,$cssData"
92+
}
93+
94+
val root = StackPane()
95+
root.children.add(webView)
96+
this.scene = Scene(root)
97+
}
98+
}
99+
},
100+
modifier = modifier,
101+
update = { jfxPanel ->
102+
Platform.runLater {
103+
val root = jfxPanel.scene?.root as? StackPane
104+
val webView = (root?.children?.get(0) as? WebView) ?: return@runLater
105+
when (event) {
106+
is OoniWebViewController.Event.Load -> {
107+
val headers = event.additionalHttpHeaders.entries.joinToString {
108+
"\n${it.key}: it.value"
109+
}
110+
// Hack to send HTTP headers by taking advantage of userAgent
111+
webView.engine.userAgent = "ooni$headers"
112+
webView.engine.load(event.url)
113+
}
114+
115+
OoniWebViewController.Event.Reload -> {
116+
webView.engine.reload()
117+
}
118+
119+
OoniWebViewController.Event.Back -> {
120+
if (webView.engine.history.currentIndex > 0) {
121+
webView.engine.history.go(-1)
122+
}
123+
}
124+
125+
null -> Unit
126+
}
127+
event?.let(controller::onEventHandled)
128+
}
129+
},
130+
)
12131
}
File renamed without changes.

0 commit comments

Comments
 (0)