@@ -2,11 +2,130 @@ package org.ooni.probe.ui.shared
2
2
3
3
import androidx.compose.runtime.Composable
4
4
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
5
14
6
15
@Composable
7
16
actual fun OoniWebView (
8
17
controller : OoniWebViewController ,
9
18
modifier : Modifier ,
10
19
allowedDomains : List <String >,
11
20
) {
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
+ )
12
131
}
0 commit comments