package eu.kanade.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.outlined.ArrowBack import androidx.compose.material.icons.outlined.ArrowForward import androidx.compose.material.icons.outlined.Close import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface 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.LocalUriHandler import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.google.accompanist.web.AccompanistWebViewClient import com.google.accompanist.web.LoadingState import com.google.accompanist.web.WebView import com.google.accompanist.web.rememberWebViewNavigator import com.google.accompanist.web.rememberWebViewState import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.AppBarActions import eu.kanade.presentation.components.WarningBanner import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.util.system.getHtml import eu.kanade.tachiyomi.util.system.setDefaultSettings import kotlinx.coroutines.launch import tachiyomi.presentation.core.components.material.Scaffold @Composable fun WebViewScreenContent( onNavigateUp: () -> Unit, initialTitle: String?, url: String, headers: Map = emptyMap(), onUrlChange: (String) -> Unit = {}, onShare: (String) -> Unit, onOpenInBrowser: (String) -> Unit, onClearCookies: (String) -> Unit, ) { val state = rememberWebViewState(url = url, additionalHttpHeaders = headers) val navigator = rememberWebViewNavigator() val uriHandler = LocalUriHandler.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 } // Continue with request, but with custom headers view?.loadUrl(it.url.toString(), headers) } return super.shouldOverrideUrlLoading(view, request) } } } Scaffold( topBar = { Box { Column { AppBar( title = state.pageTitle ?: initialTitle, subtitle = currentUrl, navigateUp = onNavigateUp, navigationIcon = Icons.Outlined.Close, actions = { AppBarActions( listOf( AppBar.Action( title = stringResource(R.string.action_webview_back), icon = Icons.Outlined.ArrowBack, onClick = { if (navigator.canGoBack) { navigator.navigateBack() } }, enabled = navigator.canGoBack, ), AppBar.Action( title = stringResource(R.string.action_webview_forward), icon = Icons.Outlined.ArrowForward, onClick = { if (navigator.canGoForward) { navigator.navigateForward() } }, enabled = navigator.canGoForward, ), AppBar.OverflowAction( title = stringResource(R.string.action_webview_refresh), onClick = { navigator.reload() }, ), AppBar.OverflowAction( title = stringResource(R.string.action_share), onClick = { onShare(currentUrl) }, ), AppBar.OverflowAction( title = stringResource(R.string.action_open_in_browser), onClick = { onOpenInBrowser(currentUrl) }, ), AppBar.OverflowAction( title = stringResource(R.string.pref_clear_cookies), onClick = { onClearCookies(currentUrl) }, ), ), ) }, ) if (showCloudflareHelp) { Surface( modifier = Modifier.padding(8.dp), ) { WarningBanner( textRes = R.string.information_cloudflare_help, modifier = Modifier .clip(MaterialTheme.shapes.small) .clickable { uriHandler.openUri( "https://tachiyomi.org/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, ) } }