yokai/app/src/main/java/yokai/presentation/webview/WebViewScreenContent.kt
arkon 5e84586ff5
refactor(webview): Replace WebView with its Compose counterpart
Co-authored-by: null2264 <palembani@gmail.com>
2024-11-26 22:26:33 +07:00

251 lines
11 KiB
Kotlin

package yokai.presentation.webview
import android.content.pm.ApplicationInfo
import android.graphics.Bitmap
import android.webkit.WebResourceRequest
import android.webkit.WebView
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ArrowBack
import androidx.compose.material.icons.automirrored.outlined.ArrowForward
import androidx.compose.material.icons.outlined.Close
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.unit.dp
import com.kevinnzou.accompanist.web.AccompanistWebViewClient
import com.kevinnzou.accompanist.web.LoadingState
import com.kevinnzou.accompanist.web.WebView
import com.kevinnzou.accompanist.web.rememberWebViewNavigator
import com.kevinnzou.accompanist.web.rememberWebViewState
import dev.icerock.moko.resources.compose.stringResource
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.util.system.extensionIntentForText
import eu.kanade.tachiyomi.util.system.getHtml
import eu.kanade.tachiyomi.util.system.setDefaultSettings
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.launch
import yokai.i18n.MR
import yokai.presentation.component.AppBar
import yokai.presentation.component.AppBarActions
import yokai.presentation.component.AppBarTitle
import yokai.presentation.component.UpIcon
import yokai.presentation.component.WarningBanner
@Composable
fun WebViewScreenContent(
onNavigateUp: () -> Unit,
initialTitle: String?,
url: String,
onShare: (String) -> Unit,
onOpenInApp: (String) -> Unit,
onOpenInBrowser: (String) -> Unit,
onClearCookies: (String) -> Unit,
headers: Map<String, String> = emptyMap(),
onUrlChange: (String) -> Unit = {},
) {
val state = rememberWebViewState(url = url, additionalHttpHeaders = headers)
val navigator = rememberWebViewNavigator()
val uriHandler = LocalUriHandler.current
val context = LocalContext.current
val scope = rememberCoroutineScope()
var currentUrl by remember { mutableStateOf(url) }
var showCloudflareHelp by remember { mutableStateOf(false) }
val webClient = remember {
object : AccompanistWebViewClient() {
override fun onPageStarted(view: WebView, url: String?, favicon: Bitmap?) {
super.onPageStarted(view, url, favicon)
url?.let {
currentUrl = it
onUrlChange(it)
}
}
override fun onPageFinished(view: WebView, url: String?) {
super.onPageFinished(view, url)
scope.launch {
val html = view.getHtml()
showCloudflareHelp = "window._cf_chl_opt" in html || "Ray ID is" in html
}
}
override fun doUpdateVisitedHistory(
view: WebView,
url: String?,
isReload: Boolean,
) {
super.doUpdateVisitedHistory(view, url, isReload)
url?.let {
currentUrl = it
onUrlChange(it)
}
}
override fun shouldOverrideUrlLoading(
view: WebView?,
request: WebResourceRequest?,
): Boolean {
request?.let {
// Don't attempt to open blobs as webpages
if (it.url.toString().startsWith("blob:http")) {
return false
}
// Ignore intents urls
if (it.url.toString().startsWith("intent://")) {
return true
}
// Continue with request, but with custom headers
view?.loadUrl(it.url.toString(), headers)
}
return super.shouldOverrideUrlLoading(view, request)
}
}
}
Scaffold (
topBar = {
Box {
Column {
TopAppBar(
title = {
AppBarTitle(
title = state.pageTitle ?: initialTitle,
subtitle = currentUrl,
)
},
navigationIcon = {
IconButton(onClick = onNavigateUp) {
UpIcon(navigationIcon = Icons.Outlined.Close)
}
},
actions = {
AppBarActions(
persistentListOf(
AppBar.Action(
title = stringResource(MR.strings.action_webview_back),
icon = Icons.AutoMirrored.Outlined.ArrowBack,
onClick = {
if (navigator.canGoBack) {
navigator.navigateBack()
}
},
enabled = navigator.canGoBack,
),
AppBar.Action(
title = stringResource(MR.strings.action_webview_forward),
icon = Icons.AutoMirrored.Outlined.ArrowForward,
onClick = {
if (navigator.canGoForward) {
navigator.navigateForward()
}
},
enabled = navigator.canGoForward,
),
AppBar.OverflowAction(
title = stringResource(MR.strings.action_webview_refresh),
onClick = { navigator.reload() },
),
AppBar.OverflowAction(
title = stringResource(MR.strings.share),
onClick = { onShare(currentUrl) },
),
AppBar.OverflowAction(
title = stringResource(MR.strings.open_in_app),
onClick = { onOpenInApp(currentUrl) },
isVisible = navigator.canGoBack &&
context.extensionIntentForText(currentUrl) != null,
),
AppBar.OverflowAction(
title = stringResource(MR.strings.open_in_browser),
onClick = { onOpenInBrowser(currentUrl) },
),
AppBar.OverflowAction(
title = stringResource(MR.strings.clear_cookies),
onClick = { onClearCookies(currentUrl) },
),
),
)
},
)
if (showCloudflareHelp) {
Surface(
modifier = Modifier.padding(8.dp),
) {
WarningBanner(
textRes = MR.strings.information_cloudflare_help,
modifier = Modifier
.clip(MaterialTheme.shapes.small)
.clickable {
uriHandler.openUri(
"https://mihon.app/docs/guides/troubleshooting/#cloudflare",
)
},
)
}
}
}
when (val loadingState = state.loadingState) {
is LoadingState.Initializing -> LinearProgressIndicator(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.BottomCenter),
)
is LoadingState.Loading -> LinearProgressIndicator(
progress = { (loadingState as? LoadingState.Loading)?.progress ?: 1f },
modifier = Modifier
.fillMaxWidth()
.align(Alignment.BottomCenter),
)
else -> {}
}
}
},
) { contentPadding ->
WebView(
state = 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)
}
headers["user-agent"]?.let {
webView.settings.userAgentString = it
}
},
client = webClient,
)
}
}