Added proper multi window support in WebView instead of treating everything as a redirect (#2584)

This commit is contained in:
Trevor Paley
2025-11-01 23:23:01 -07:00
committed by GitHub
parent 8662f80fbf
commit 6ab87c7931
3 changed files with 118 additions and 25 deletions

View File

@@ -23,6 +23,7 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
- Spoofing of `X-Requested-With` header to support newer WebView versions ([@Guzmazow](https://github.com/Guzmazow)) ([#2491](https://github.com/mihonapp/mihon/pull/2491))
- Download support for chapters with the same metadata. Now a hash based on chapter's url is appended to download filename to tell them apart, letting you download both. Existing downloaded chapters will continue to work normally ([@raxod502](https://github.com/radian-software)) ([#2305](https://github.com/mihonapp/mihon/pull/2305))
- Auto refresh extension list whenever a repository is added or removed ([@c2y5](https://github.com/c2y5)) ([#2483](https://github.com/mihonapp/mihon/pull/2483))
- Added proper multi window support in WebView instead of treating everything as a redirect ([@TheUnlocked](https://github.com/TheUnlocked)) ([#2584](https://github.com/mihonapp/mihon/pull/2584))
### Fixed
- Fix height of description not being calculated correctly if images are present ([@Secozzi](https://github.com/Secozzi)) ([#2382](https://github.com/mihonapp/mihon/pull/2382))

View File

@@ -2,6 +2,7 @@ package eu.kanade.presentation.webview
import android.content.pm.ApplicationInfo
import android.graphics.Bitmap
import android.os.Message
import android.webkit.WebResourceRequest
import android.webkit.WebView
import androidx.compose.foundation.clickable
@@ -19,6 +20,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
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
@@ -28,11 +30,14 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.core.stack.mutableStateStackOf
import com.kevinnzou.web.AccompanistWebChromeClient
import com.kevinnzou.web.AccompanistWebViewClient
import com.kevinnzou.web.LoadingState
import com.kevinnzou.web.WebContent
import com.kevinnzou.web.WebView
import com.kevinnzou.web.WebViewState
import com.kevinnzou.web.rememberWebViewNavigator
import com.kevinnzou.web.rememberWebViewState
import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions
import eu.kanade.presentation.components.WarningBanner
@@ -44,6 +49,18 @@ import kotlinx.coroutines.launch
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource
class WebViewWindow(webContent: WebContent) {
var state by mutableStateOf(WebViewState(webContent))
var popupMessage: Message? = null
private set
var webView: WebView? = null
constructor(popupMessage: Message) : this(WebContent.NavigatorOnly) {
this.popupMessage = popupMessage
}
}
@Composable
fun WebViewScreenContent(
onNavigateUp: () -> Unit,
@@ -55,7 +72,26 @@ fun WebViewScreenContent(
headers: Map<String, String> = emptyMap(),
onUrlChange: (String) -> Unit = {},
) {
val state = rememberWebViewState(url = url, additionalHttpHeaders = headers)
val windowStack = remember {
mutableStateStackOf(
WebViewWindow(
WebContent.Url(url = url, additionalHttpHeaders = headers),
),
)
}
val currentWindow = windowStack.lastItemOrNull!!
val popState: (() -> Unit) = remember {
{
if (windowStack.size == 1) {
onNavigateUp()
} else {
windowStack.pop()
}
}
}
val navigator = rememberWebViewNavigator()
val uriHandler = LocalUriHandler.current
val scope = rememberCoroutineScope()
@@ -116,14 +152,39 @@ fun WebViewScreenContent(
}
}
val webChromeClient = remember {
object : AccompanistWebChromeClient() {
override fun onCreateWindow(
view: WebView,
isDialog: Boolean,
isUserGesture: Boolean,
resultMsg: Message,
): Boolean {
// if it wasn't initiated by a user gesture, we should ignore it like a normal browser would
if (isUserGesture) {
windowStack.push(WebViewWindow(resultMsg))
return true
}
return false
}
}
}
fun initializePopup(webView: WebView, message: Message): WebView {
val transport = message.obj as WebView.WebViewTransport
transport.webView = webView
message.sendToTarget()
return webView
}
Scaffold(
topBar = {
Box {
Column {
AppBar(
title = state.pageTitle ?: initialTitle,
title = currentWindow.state.pageTitle ?: initialTitle,
subtitle = currentUrl,
navigateUp = onNavigateUp,
navigateUp = popState,
navigationIcon = Icons.Outlined.Close,
actions = {
AppBarActions(
@@ -186,7 +247,7 @@ fun WebViewScreenContent(
}
}
}
when (val loadingState = state.loadingState) {
when (val loadingState = currentWindow.state.loadingState) {
is LoadingState.Initializing -> LinearProgressIndicator(
modifier = Modifier
.fillMaxWidth()
@@ -203,27 +264,55 @@ fun WebViewScreenContent(
}
},
) { contentPadding ->
WebView(
state = state,
modifier = Modifier
.fillMaxSize()
.padding(contentPadding),
navigator = navigator,
onCreated = { webView ->
webView.setDefaultSettings()
// We need to key the WebView composable to the window object since simply updating the WebView composable will
// not cause it to re-invoke the WebView factory and render the new current window's WebView. This lets us
// completely reset the WebView composable when the current window switches.
key(currentWindow) {
WebView(
state = currentWindow.state,
modifier = Modifier
.fillMaxSize()
.padding(contentPadding),
navigator = navigator,
onCreated = { webView ->
webView.setDefaultSettings()
// Debug mode (chrome://inspect/#devices)
if (BuildConfig.DEBUG &&
0 != webView.context.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE
) {
WebView.setWebContentsDebuggingEnabled(true)
}
// Debug mode (chrome://inspect/#devices)
if (BuildConfig.DEBUG &&
0 != webView.context.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE
) {
WebView.setWebContentsDebuggingEnabled(true)
}
headers["user-agent"]?.let {
webView.settings.userAgentString = it
}
},
client = webClient,
)
headers["user-agent"]?.let {
webView.settings.userAgentString = it
}
},
onDispose = { webView ->
val window = windowStack.items.find { it.webView == webView }
if (window == null) {
// If we couldn't find any window on the stack that owns this WebView, it means that we can
// safely dispose of it because the window containing it has been closed.
webView.destroy()
} else {
// The composable is being disposed but the WebView object is not.
// When the WebView element is recomposed, we will want the WebView to resume from its state
// before it was unmounted, we won't want it to reset back to its original target.
window.state = WebViewState(WebContent.NavigatorOnly)
}
},
client = webClient,
chromeClient = webChromeClient,
factory = { context ->
currentWindow.webView
?: WebView(context).also { webView ->
currentWindow.webView = webView
currentWindow.popupMessage?.let {
initializePopup(webView, it)
}
}
},
)
}
}
}

View File

@@ -83,6 +83,9 @@ fun WebView.setDefaultSettings() {
loadWithOverviewMode = true
cacheMode = WebSettings.LOAD_DEFAULT
// Handle popups properly
setSupportMultipleWindows(true)
// Allow zooming
setSupportZoom(true)
builtInZoomControls = true