mirror of
https://github.com/mihonapp/mihon.git
synced 2025-11-10 02:58:55 +01:00
Added proper multi window support in WebView instead of treating everything as a redirect (#2584)
This commit is contained in:
@@ -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))
|
- 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))
|
- 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))
|
- 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
|
### 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))
|
- 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))
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package eu.kanade.presentation.webview
|
|||||||
|
|
||||||
import android.content.pm.ApplicationInfo
|
import android.content.pm.ApplicationInfo
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
|
import android.os.Message
|
||||||
import android.webkit.WebResourceRequest
|
import android.webkit.WebResourceRequest
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
@@ -19,6 +20,7 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.key
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
@@ -28,11 +30,14 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.platform.LocalUriHandler
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
import androidx.compose.ui.unit.dp
|
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.AccompanistWebViewClient
|
||||||
import com.kevinnzou.web.LoadingState
|
import com.kevinnzou.web.LoadingState
|
||||||
|
import com.kevinnzou.web.WebContent
|
||||||
import com.kevinnzou.web.WebView
|
import com.kevinnzou.web.WebView
|
||||||
|
import com.kevinnzou.web.WebViewState
|
||||||
import com.kevinnzou.web.rememberWebViewNavigator
|
import com.kevinnzou.web.rememberWebViewNavigator
|
||||||
import com.kevinnzou.web.rememberWebViewState
|
|
||||||
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
|
||||||
@@ -44,6 +49,18 @@ import kotlinx.coroutines.launch
|
|||||||
import tachiyomi.i18n.MR
|
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) {
|
||||||
|
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
|
@Composable
|
||||||
fun WebViewScreenContent(
|
fun WebViewScreenContent(
|
||||||
onNavigateUp: () -> Unit,
|
onNavigateUp: () -> Unit,
|
||||||
@@ -55,7 +72,26 @@ fun WebViewScreenContent(
|
|||||||
headers: Map<String, String> = emptyMap(),
|
headers: Map<String, String> = emptyMap(),
|
||||||
onUrlChange: (String) -> Unit = {},
|
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 navigator = rememberWebViewNavigator()
|
||||||
val uriHandler = LocalUriHandler.current
|
val uriHandler = LocalUriHandler.current
|
||||||
val scope = rememberCoroutineScope()
|
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(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
Box {
|
Box {
|
||||||
Column {
|
Column {
|
||||||
AppBar(
|
AppBar(
|
||||||
title = state.pageTitle ?: initialTitle,
|
title = currentWindow.state.pageTitle ?: initialTitle,
|
||||||
subtitle = currentUrl,
|
subtitle = currentUrl,
|
||||||
navigateUp = onNavigateUp,
|
navigateUp = popState,
|
||||||
navigationIcon = Icons.Outlined.Close,
|
navigationIcon = Icons.Outlined.Close,
|
||||||
actions = {
|
actions = {
|
||||||
AppBarActions(
|
AppBarActions(
|
||||||
@@ -186,7 +247,7 @@ fun WebViewScreenContent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
when (val loadingState = state.loadingState) {
|
when (val loadingState = currentWindow.state.loadingState) {
|
||||||
is LoadingState.Initializing -> LinearProgressIndicator(
|
is LoadingState.Initializing -> LinearProgressIndicator(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -203,27 +264,55 @@ fun WebViewScreenContent(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
) { contentPadding ->
|
) { contentPadding ->
|
||||||
WebView(
|
// We need to key the WebView composable to the window object since simply updating the WebView composable will
|
||||||
state = state,
|
// not cause it to re-invoke the WebView factory and render the new current window's WebView. This lets us
|
||||||
modifier = Modifier
|
// completely reset the WebView composable when the current window switches.
|
||||||
.fillMaxSize()
|
key(currentWindow) {
|
||||||
.padding(contentPadding),
|
WebView(
|
||||||
navigator = navigator,
|
state = currentWindow.state,
|
||||||
onCreated = { webView ->
|
modifier = Modifier
|
||||||
webView.setDefaultSettings()
|
.fillMaxSize()
|
||||||
|
.padding(contentPadding),
|
||||||
|
navigator = navigator,
|
||||||
|
onCreated = { webView ->
|
||||||
|
webView.setDefaultSettings()
|
||||||
|
|
||||||
// Debug mode (chrome://inspect/#devices)
|
// Debug mode (chrome://inspect/#devices)
|
||||||
if (BuildConfig.DEBUG &&
|
if (BuildConfig.DEBUG &&
|
||||||
0 != webView.context.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE
|
0 != webView.context.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE
|
||||||
) {
|
) {
|
||||||
WebView.setWebContentsDebuggingEnabled(true)
|
WebView.setWebContentsDebuggingEnabled(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
headers["user-agent"]?.let {
|
headers["user-agent"]?.let {
|
||||||
webView.settings.userAgentString = it
|
webView.settings.userAgentString = it
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
client = webClient,
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,6 +83,9 @@ fun WebView.setDefaultSettings() {
|
|||||||
loadWithOverviewMode = true
|
loadWithOverviewMode = true
|
||||||
cacheMode = WebSettings.LOAD_DEFAULT
|
cacheMode = WebSettings.LOAD_DEFAULT
|
||||||
|
|
||||||
|
// Handle popups properly
|
||||||
|
setSupportMultipleWindows(true)
|
||||||
|
|
||||||
// Allow zooming
|
// Allow zooming
|
||||||
setSupportZoom(true)
|
setSupportZoom(true)
|
||||||
builtInZoomControls = true
|
builtInZoomControls = true
|
||||||
|
|||||||
Reference in New Issue
Block a user