Improve WebView multi-window UX (#2662)

- Navigation history for lower windows is preserved when a popup is opened
- Back gesture will close a popup window rather than the entire WebView activity when there is no previous page
- The leftmost close button closes the entire activity as before
- When a popup window is shown, a new button appears to close just that window
This commit is contained in:
Trevor Paley
2025-11-07 01:26:04 -08:00
committed by GitHub
parent f4703ed83a
commit 855eea2ada
4 changed files with 51 additions and 18 deletions

View File

@@ -11,6 +11,8 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
- `Other` - for technical stuff. - `Other` - for technical stuff.
## [Unreleased] ## [Unreleased]
### Improved
- Improved various aspects of the WebView multi window support added in 0.19.2 ([@TheUnlocked](https://github.com/TheUnlocked)) ([#2662](https://github.com/mihonapp/mihon/pull/2662))
## [v0.19.3] - 2025-11-04 ## [v0.19.3] - 2025-11-04
### Fixed ### Fixed

View File

@@ -5,6 +5,7 @@ import android.graphics.Bitmap
import android.os.Message import android.os.Message
import android.webkit.WebResourceRequest import android.webkit.WebResourceRequest
import android.webkit.WebView import android.webkit.WebView
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -28,7 +29,9 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.core.stack.mutableStateStackOf import cafe.adriel.voyager.core.stack.mutableStateStackOf
import com.kevinnzou.web.AccompanistWebChromeClient import com.kevinnzou.web.AccompanistWebChromeClient
@@ -36,12 +39,13 @@ import com.kevinnzou.web.AccompanistWebViewClient
import com.kevinnzou.web.LoadingState import com.kevinnzou.web.LoadingState
import com.kevinnzou.web.WebContent import com.kevinnzou.web.WebContent
import com.kevinnzou.web.WebView import com.kevinnzou.web.WebView
import com.kevinnzou.web.WebViewNavigator
import com.kevinnzou.web.WebViewState import com.kevinnzou.web.WebViewState
import com.kevinnzou.web.rememberWebViewNavigator
import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions import eu.kanade.presentation.components.AppBarActions
import eu.kanade.presentation.components.WarningBanner import eu.kanade.presentation.components.WarningBanner
import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.system.getHtml import eu.kanade.tachiyomi.util.system.getHtml
import eu.kanade.tachiyomi.util.system.setDefaultSettings import eu.kanade.tachiyomi.util.system.setDefaultSettings
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@@ -50,13 +54,13 @@ import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource import tachiyomi.presentation.core.i18n.stringResource
class WebViewWindow(webContent: WebContent) { class WebViewWindow(webContent: WebContent, val navigator: WebViewNavigator) {
var state by mutableStateOf(WebViewState(webContent)) var state by mutableStateOf(WebViewState(webContent))
var popupMessage: Message? = null var popupMessage: Message? = null
private set private set
var webView: WebView? = null var webView: WebView? = null
constructor(popupMessage: Message) : this(WebContent.NavigatorOnly) { constructor(popupMessage: Message, navigator: WebViewNavigator) : this(WebContent.NavigatorOnly, navigator) {
this.popupMessage = popupMessage this.popupMessage = popupMessage
} }
} }
@@ -72,27 +76,20 @@ fun WebViewScreenContent(
headers: Map<String, String> = emptyMap(), headers: Map<String, String> = emptyMap(),
onUrlChange: (String) -> Unit = {}, onUrlChange: (String) -> Unit = {},
) { ) {
val coroutineScope = rememberCoroutineScope()
val windowStack = remember { val windowStack = remember {
mutableStateStackOf( mutableStateStackOf(
WebViewWindow( WebViewWindow(
WebContent.Url(url = url, additionalHttpHeaders = headers), WebContent.Url(url = url, additionalHttpHeaders = headers),
WebViewNavigator(coroutineScope),
), ),
) )
} }
val currentWindow = windowStack.lastItemOrNull!! val currentWindow = windowStack.lastItemOrNull!!
val navigator = currentWindow.navigator
val popState: (() -> Unit) = remember {
{
if (windowStack.size == 1) {
onNavigateUp()
} else {
windowStack.pop()
}
}
}
val navigator = rememberWebViewNavigator()
val uriHandler = LocalUriHandler.current val uriHandler = LocalUriHandler.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
@@ -161,7 +158,7 @@ fun WebViewScreenContent(
): Boolean { ): Boolean {
// if it wasn't initiated by a user gesture, we should ignore it like a normal browser would // if it wasn't initiated by a user gesture, we should ignore it like a normal browser would
if (isUserGesture) { if (isUserGesture) {
windowStack.push(WebViewWindow(resultMsg)) windowStack.push(WebViewWindow(resultMsg, WebViewNavigator(coroutineScope)))
return true return true
} }
return false return false
@@ -176,6 +173,18 @@ fun WebViewScreenContent(
return webView return webView
} }
val popState = remember<() -> Unit> {
{
if (windowStack.size == 1) {
onNavigateUp()
} else {
windowStack.pop()
}
}
}
BackHandler(windowStack.size > 1, popState)
Scaffold( Scaffold(
topBar = { topBar = {
Box { Box {
@@ -183,7 +192,7 @@ fun WebViewScreenContent(
AppBar( AppBar(
title = currentWindow.state.pageTitle ?: initialTitle, title = currentWindow.state.pageTitle ?: initialTitle,
subtitle = currentUrl, subtitle = currentUrl,
navigateUp = popState, navigateUp = onNavigateUp,
navigationIcon = Icons.Outlined.Close, navigationIcon = Icons.Outlined.Close,
actions = { actions = {
AppBarActions( AppBarActions(
@@ -224,7 +233,18 @@ fun WebViewScreenContent(
title = stringResource(MR.strings.pref_clear_cookies), title = stringResource(MR.strings.pref_clear_cookies),
onClick = { onClearCookies(currentUrl) }, onClick = { onClearCookies(currentUrl) },
), ),
), ).builder().apply {
if (windowStack.size > 1) {
add(
0,
AppBar.Action(
title = stringResource(MR.strings.action_webview_close_tab),
icon = ImageVector.vectorResource(R.drawable.ic_tab_close_24px),
onClick = popState,
),
)
}
}.build(),
) )
}, },
) )
@@ -297,7 +317,7 @@ fun WebViewScreenContent(
// The composable is being disposed but the WebView object is not. // 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 // 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. // before it was unmounted, we won't want it to reset back to its original target.
window.state = WebViewState(WebContent.NavigatorOnly) window.state.content = WebContent.NavigatorOnly
} }
}, },
client = webClient, client = webClient,

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M476,540L560,456L644,540L700,484L616,400L700,316L644,260L560,344L476,260L420,316L504,400L420,484L476,540ZM320,720Q287,720 263.5,696.5Q240,673 240,640L240,160Q240,127 263.5,103.5Q287,80 320,80L800,80Q833,80 856.5,103.5Q880,127 880,160L880,640Q880,673 856.5,696.5Q833,720 800,720L320,720ZM320,640L800,640Q800,640 800,640Q800,640 800,640L800,160Q800,160 800,160Q800,160 800,160L320,160Q320,160 320,160Q320,160 320,160L320,640Q320,640 320,640Q320,640 320,640ZM160,880Q127,880 103.5,856.5Q80,833 80,800L80,240L160,240L160,800Q160,800 160,800Q160,800 160,800L720,800L720,880L160,880ZM320,160L320,160Q320,160 320,160Q320,160 320,160L320,640Q320,640 320,640Q320,640 320,640L320,640Q320,640 320,640Q320,640 320,640L320,160Q320,160 320,160Q320,160 320,160Z"/>
</vector>

View File

@@ -164,6 +164,7 @@
<string name="action_webview_back">Back</string> <string name="action_webview_back">Back</string>
<string name="action_webview_forward">Forward</string> <string name="action_webview_forward">Forward</string>
<string name="action_webview_refresh">Refresh</string> <string name="action_webview_refresh">Refresh</string>
<string name="action_webview_close_tab">Close tab</string>
<string name="action_start_downloading_now">Start downloading now</string> <string name="action_start_downloading_now">Start downloading now</string>
<string name="action_not_now">Not now</string> <string name="action_not_now">Not now</string>
<string name="action_add_anyway">Add anyway</string> <string name="action_add_anyway">Add anyway</string>