diff --git a/CHANGELOG.md b/CHANGELOG.md index e382479fd..8dffc3e83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)) diff --git a/app/src/main/java/eu/kanade/presentation/webview/WebViewScreenContent.kt b/app/src/main/java/eu/kanade/presentation/webview/WebViewScreenContent.kt index 529dc1d2d..d11316f79 100644 --- a/app/src/main/java/eu/kanade/presentation/webview/WebViewScreenContent.kt +++ b/app/src/main/java/eu/kanade/presentation/webview/WebViewScreenContent.kt @@ -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 = 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) + } + } + }, + ) + } } } diff --git a/core/common/src/main/kotlin/eu/kanade/tachiyomi/util/system/WebViewUtil.kt b/core/common/src/main/kotlin/eu/kanade/tachiyomi/util/system/WebViewUtil.kt index a66ca6994..0ca4361f8 100644 --- a/core/common/src/main/kotlin/eu/kanade/tachiyomi/util/system/WebViewUtil.kt +++ b/core/common/src/main/kotlin/eu/kanade/tachiyomi/util/system/WebViewUtil.kt @@ -83,6 +83,9 @@ fun WebView.setDefaultSettings() { loadWithOverviewMode = true cacheMode = WebSettings.LOAD_DEFAULT + // Handle popups properly + setSupportMultipleWindows(true) + // Allow zooming setSupportZoom(true) builtInZoomControls = true