WebViewScreenContent.kt 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. package eu.kanade.presentation.webview
  2. import android.content.pm.ApplicationInfo
  3. import android.graphics.Bitmap
  4. import android.webkit.WebResourceRequest
  5. import android.webkit.WebView
  6. import androidx.compose.foundation.clickable
  7. import androidx.compose.foundation.layout.Box
  8. import androidx.compose.foundation.layout.Column
  9. import androidx.compose.foundation.layout.fillMaxSize
  10. import androidx.compose.foundation.layout.fillMaxWidth
  11. import androidx.compose.foundation.layout.padding
  12. import androidx.compose.material.icons.Icons
  13. import androidx.compose.material.icons.outlined.ArrowBack
  14. import androidx.compose.material.icons.outlined.ArrowForward
  15. import androidx.compose.material.icons.outlined.Close
  16. import androidx.compose.material3.LinearProgressIndicator
  17. import androidx.compose.material3.MaterialTheme
  18. import androidx.compose.material3.Surface
  19. import androidx.compose.runtime.Composable
  20. import androidx.compose.runtime.getValue
  21. import androidx.compose.runtime.mutableStateOf
  22. import androidx.compose.runtime.remember
  23. import androidx.compose.runtime.rememberCoroutineScope
  24. import androidx.compose.runtime.setValue
  25. import androidx.compose.ui.Alignment
  26. import androidx.compose.ui.Modifier
  27. import androidx.compose.ui.draw.clip
  28. import androidx.compose.ui.platform.LocalUriHandler
  29. import androidx.compose.ui.res.stringResource
  30. import androidx.compose.ui.unit.dp
  31. import com.google.accompanist.web.AccompanistWebViewClient
  32. import com.google.accompanist.web.LoadingState
  33. import com.google.accompanist.web.WebView
  34. import com.google.accompanist.web.rememberWebViewNavigator
  35. import com.google.accompanist.web.rememberWebViewState
  36. import eu.kanade.presentation.components.AppBar
  37. import eu.kanade.presentation.components.AppBarActions
  38. import eu.kanade.presentation.components.WarningBanner
  39. import eu.kanade.tachiyomi.BuildConfig
  40. import eu.kanade.tachiyomi.R
  41. import eu.kanade.tachiyomi.util.system.getHtml
  42. import eu.kanade.tachiyomi.util.system.setDefaultSettings
  43. import kotlinx.coroutines.launch
  44. import tachiyomi.presentation.core.components.material.Scaffold
  45. @Composable
  46. fun WebViewScreenContent(
  47. onNavigateUp: () -> Unit,
  48. initialTitle: String?,
  49. url: String,
  50. headers: Map<String, String> = emptyMap(),
  51. onUrlChange: (String) -> Unit = {},
  52. onShare: (String) -> Unit,
  53. onOpenInBrowser: (String) -> Unit,
  54. onClearCookies: (String) -> Unit,
  55. ) {
  56. val state = rememberWebViewState(url = url, additionalHttpHeaders = headers)
  57. val navigator = rememberWebViewNavigator()
  58. val uriHandler = LocalUriHandler.current
  59. val scope = rememberCoroutineScope()
  60. var currentUrl by remember { mutableStateOf(url) }
  61. var showCloudflareHelp by remember { mutableStateOf(false) }
  62. val webClient = remember {
  63. object : AccompanistWebViewClient() {
  64. override fun onPageStarted(view: WebView, url: String?, favicon: Bitmap?) {
  65. super.onPageStarted(view, url, favicon)
  66. url?.let {
  67. currentUrl = it
  68. onUrlChange(it)
  69. }
  70. }
  71. override fun onPageFinished(view: WebView, url: String?) {
  72. super.onPageFinished(view, url)
  73. scope.launch {
  74. val html = view.getHtml()
  75. showCloudflareHelp = "window._cf_chl_opt" in html || "Ray ID is" in html
  76. }
  77. }
  78. override fun doUpdateVisitedHistory(
  79. view: WebView,
  80. url: String?,
  81. isReload: Boolean,
  82. ) {
  83. super.doUpdateVisitedHistory(view, url, isReload)
  84. url?.let {
  85. currentUrl = it
  86. onUrlChange(it)
  87. }
  88. }
  89. override fun shouldOverrideUrlLoading(
  90. view: WebView?,
  91. request: WebResourceRequest?,
  92. ): Boolean {
  93. request?.let {
  94. // Don't attempt to open blobs as webpages
  95. if (it.url.toString().startsWith("blob:http")) {
  96. return false
  97. }
  98. // Continue with request, but with custom headers
  99. view?.loadUrl(it.url.toString(), headers)
  100. }
  101. return super.shouldOverrideUrlLoading(view, request)
  102. }
  103. }
  104. }
  105. Scaffold(
  106. topBar = {
  107. Box {
  108. Column {
  109. AppBar(
  110. title = state.pageTitle ?: initialTitle,
  111. subtitle = currentUrl,
  112. navigateUp = onNavigateUp,
  113. navigationIcon = Icons.Outlined.Close,
  114. actions = {
  115. AppBarActions(
  116. listOf(
  117. AppBar.Action(
  118. title = stringResource(R.string.action_webview_back),
  119. icon = Icons.Outlined.ArrowBack,
  120. onClick = {
  121. if (navigator.canGoBack) {
  122. navigator.navigateBack()
  123. }
  124. },
  125. enabled = navigator.canGoBack,
  126. ),
  127. AppBar.Action(
  128. title = stringResource(R.string.action_webview_forward),
  129. icon = Icons.Outlined.ArrowForward,
  130. onClick = {
  131. if (navigator.canGoForward) {
  132. navigator.navigateForward()
  133. }
  134. },
  135. enabled = navigator.canGoForward,
  136. ),
  137. AppBar.OverflowAction(
  138. title = stringResource(R.string.action_webview_refresh),
  139. onClick = { navigator.reload() },
  140. ),
  141. AppBar.OverflowAction(
  142. title = stringResource(R.string.action_share),
  143. onClick = { onShare(currentUrl) },
  144. ),
  145. AppBar.OverflowAction(
  146. title = stringResource(R.string.action_open_in_browser),
  147. onClick = { onOpenInBrowser(currentUrl) },
  148. ),
  149. AppBar.OverflowAction(
  150. title = stringResource(R.string.pref_clear_cookies),
  151. onClick = { onClearCookies(currentUrl) },
  152. ),
  153. ),
  154. )
  155. },
  156. )
  157. if (showCloudflareHelp) {
  158. Surface(
  159. modifier = Modifier.padding(8.dp),
  160. ) {
  161. WarningBanner(
  162. textRes = R.string.information_cloudflare_help,
  163. modifier = Modifier
  164. .clip(MaterialTheme.shapes.small)
  165. .clickable {
  166. uriHandler.openUri(
  167. "https://tachiyomi.org/docs/guides/troubleshooting/#cloudflare",
  168. )
  169. },
  170. )
  171. }
  172. }
  173. }
  174. when (val loadingState = state.loadingState) {
  175. is LoadingState.Initializing -> LinearProgressIndicator(
  176. modifier = Modifier
  177. .fillMaxWidth()
  178. .align(Alignment.BottomCenter),
  179. )
  180. is LoadingState.Loading -> LinearProgressIndicator(
  181. progress = (loadingState as? LoadingState.Loading)?.progress ?: 1f,
  182. modifier = Modifier
  183. .fillMaxWidth()
  184. .align(Alignment.BottomCenter),
  185. )
  186. else -> {}
  187. }
  188. }
  189. },
  190. ) { contentPadding ->
  191. WebView(
  192. state = state,
  193. modifier = Modifier
  194. .fillMaxSize()
  195. .padding(contentPadding),
  196. navigator = navigator,
  197. onCreated = { webView ->
  198. webView.setDefaultSettings()
  199. // Debug mode (chrome://inspect/#devices)
  200. if (BuildConfig.DEBUG &&
  201. 0 != webView.context.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE
  202. ) {
  203. WebView.setWebContentsDebuggingEnabled(true)
  204. }
  205. headers["user-agent"]?.let {
  206. webView.settings.userAgentString = it
  207. }
  208. },
  209. client = webClient,
  210. )
  211. }
  212. }