mirror of
https://github.com/null2264/yokai.git
synced 2025-06-20 18:24:42 +00:00
refactor(webview): Replace WebView with its Compose counterpart
Co-authored-by: null2264 <palembani@gmail.com>
This commit is contained in:
parent
a199ff326d
commit
5e84586ff5
15 changed files with 687 additions and 328 deletions
|
@ -168,7 +168,7 @@ dependencies {
|
|||
implementation(compose.bundles.compose)
|
||||
debugImplementation(compose.ui.tooling)
|
||||
implementation(libs.compose.theme.adapter3)
|
||||
implementation(libs.accompanist.webview)
|
||||
implementation(compose.webview)
|
||||
|
||||
implementation(libs.flexbox)
|
||||
|
||||
|
|
|
@ -1,175 +1,44 @@
|
|||
package eu.kanade.tachiyomi.ui.webview
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.assist.AssistContent
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Color
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.webkit.WebChromeClient
|
||||
import android.webkit.WebView
|
||||
import android.widget.LinearLayout
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsCompat.Type.systemBars
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.marginBottom
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.preference.changesIn
|
||||
import eu.kanade.tachiyomi.databinding.WebviewActivityBinding
|
||||
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
|
||||
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
|
||||
import eu.kanade.tachiyomi.util.system.getPrefTheme
|
||||
import eu.kanade.tachiyomi.util.system.getResourceColor
|
||||
import eu.kanade.tachiyomi.util.system.isInNightMode
|
||||
import eu.kanade.tachiyomi.util.system.setDefaultSettings
|
||||
import eu.kanade.tachiyomi.util.view.setStyle
|
||||
import android.R as AR
|
||||
|
||||
open class BaseWebViewActivity : BaseActivity<WebviewActivityBinding>() {
|
||||
|
||||
private var bundle: Bundle? = null
|
||||
// FIXME: Not sure if some of these stuff still needed
|
||||
open class BaseWebViewActivity : BaseActivity<ViewBinding>() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = WebviewActivityBinding.inflate(layoutInflater)
|
||||
delegate.localNightMode = preferences.nightMode().get()
|
||||
setContentView(binding.root)
|
||||
setSupportActionBar(binding.toolbar)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
binding.toolbar.setNavigationOnClickListener {
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
}
|
||||
val tintColor = getResourceColor(R.attr.actionBarTintColor)
|
||||
binding.toolbar.navigationIcon?.setTint(tintColor)
|
||||
binding.toolbar.navigationIcon?.setTint(tintColor)
|
||||
binding.toolbar.overflowIcon?.mutate()
|
||||
binding.toolbar.overflowIcon?.setTint(tintColor)
|
||||
|
||||
val container: ViewGroup = findViewById(R.id.web_view_layout)
|
||||
val content: LinearLayout = binding.webLinearLayout
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(container) { v, insets ->
|
||||
val contextView = window?.decorView?.findViewById<View>(R.id.action_mode_bar)
|
||||
contextView?.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
leftMargin = insets.getInsets(systemBars()).left
|
||||
rightMargin = insets.getInsets(systemBars()).right
|
||||
}
|
||||
// Consume any horizontal insets and pad all content in. There's not much we can do
|
||||
// with horizontal insets
|
||||
v.updatePadding(
|
||||
left = insets.getInsets(systemBars()).left,
|
||||
right = insets.getInsets(systemBars()).right,
|
||||
)
|
||||
WindowInsetsCompat.Builder(insets).setInsets(
|
||||
systemBars(),
|
||||
Insets.of(
|
||||
0,
|
||||
insets.getInsets(systemBars()).top,
|
||||
0,
|
||||
insets.getInsets(systemBars()).bottom,
|
||||
),
|
||||
).build()
|
||||
}
|
||||
binding.swipeRefresh.setStyle()
|
||||
binding.swipeRefresh.setOnRefreshListener {
|
||||
refreshPage()
|
||||
}
|
||||
|
||||
window.statusBarColor = ColorUtils.setAlphaComponent(
|
||||
getResourceColor(R.attr.colorSurface),
|
||||
255,
|
||||
)
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(content) { v, insets ->
|
||||
// if pure white theme on a device that does not support dark status bar
|
||||
/*if (getResourceColor(AR.attr.statusBarColor) != Color.TRANSPARENT)
|
||||
window.statusBarColor = Color.BLACK
|
||||
else window.statusBarColor = getResourceColor(R.attr.colorPrimary)*/
|
||||
window.navigationBarColor = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
Color.BLACK
|
||||
} else {
|
||||
getResourceColor(R.attr.colorPrimaryVariant)
|
||||
}
|
||||
v.setPadding(
|
||||
insets.getInsets(systemBars()).left,
|
||||
insets.getInsets(systemBars()).top,
|
||||
insets.getInsets(systemBars()).right,
|
||||
0,
|
||||
)
|
||||
if (!isInNightMode()) {
|
||||
WindowInsetsControllerCompat(window, content).isAppearanceLightNavigationBars = true
|
||||
}
|
||||
insets
|
||||
}
|
||||
|
||||
binding.swipeRefresh.isEnabled = false
|
||||
|
||||
if (bundle == null) {
|
||||
binding.webview.setDefaultSettings()
|
||||
|
||||
binding.webview.webChromeClient = object : WebChromeClient() {
|
||||
override fun onProgressChanged(view: WebView?, newProgress: Int) {
|
||||
binding.progressBar.isVisible = true
|
||||
binding.progressBar.progress = newProgress
|
||||
if (newProgress == 100) {
|
||||
binding.progressBar.isInvisible = true
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
super.onProgressChanged(view, newProgress)
|
||||
}
|
||||
|
||||
override fun onReceivedTitle(view: WebView?, title: String?) {
|
||||
super.onReceivedTitle(view, title)
|
||||
this@BaseWebViewActivity.title = title
|
||||
binding.toolbarTitle.text = title
|
||||
binding.toolbarSubtitle.text = view?.url
|
||||
binding.toolbarSubtitle.isSelected = true
|
||||
}
|
||||
}
|
||||
val marginB = binding.webview.marginBottom
|
||||
ViewCompat.setOnApplyWindowInsetsListener(binding.swipeRefresh) { v, insets ->
|
||||
val bottomInset = insets.getInsets(systemBars()).bottom
|
||||
v.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
bottomMargin = marginB + bottomInset
|
||||
}
|
||||
insets
|
||||
}
|
||||
} else {
|
||||
bundle?.let {
|
||||
binding.webview.restoreState(it)
|
||||
}
|
||||
}
|
||||
|
||||
preferences.incognitoMode()
|
||||
.changesIn(lifecycleScope) {
|
||||
SecureActivityDelegate.setSecure(this)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onProvideAssistContent(outContent: AssistContent?) {
|
||||
super.onProvideAssistContent(outContent)
|
||||
binding.webview.url?.let { outContent?.webUri = it.toUri() }
|
||||
}
|
||||
|
||||
private fun refreshPage() {
|
||||
binding.swipeRefresh.isRefreshing = true
|
||||
binding.webview.reload()
|
||||
}
|
||||
|
||||
@SuppressLint("ResourceType")
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
|
@ -189,30 +58,14 @@ open class BaseWebViewActivity : BaseActivity<WebviewActivityBinding>() {
|
|||
val attrs = theme.obtainStyledAttributes(
|
||||
intArrayOf(
|
||||
R.attr.colorSurface,
|
||||
R.attr.actionBarTintColor,
|
||||
R.attr.colorPrimaryVariant,
|
||||
),
|
||||
)
|
||||
val colorSurface = attrs.getColor(0, 0)
|
||||
val actionBarTintColor = attrs.getColor(1, 0)
|
||||
val colorPrimaryVariant = attrs.getColor(2, 0)
|
||||
val colorPrimaryVariant = attrs.getColor(1, 0)
|
||||
attrs.recycle()
|
||||
|
||||
window.statusBarColor = ColorUtils.setAlphaComponent(colorSurface, 255)
|
||||
binding.toolbar.setBackgroundColor(colorSurface)
|
||||
binding.toolbar.popupTheme =
|
||||
if (lightMode) {
|
||||
R.style.ThemeOverlay_Material3
|
||||
} else {
|
||||
R.style.ThemeOverlay_Material3_Dark
|
||||
}
|
||||
binding.toolbar.setNavigationIconTint(actionBarTintColor)
|
||||
binding.toolbar.overflowIcon?.mutate()
|
||||
binding.toolbar.setTitleTextColor(actionBarTintColor)
|
||||
binding.toolbar.overflowIcon?.setTint(actionBarTintColor)
|
||||
binding.swipeRefresh.setColorSchemeColors(actionBarTintColor)
|
||||
binding.swipeRefresh.setProgressBackgroundColorSchemeColor(colorPrimaryVariant)
|
||||
|
||||
window.navigationBarColor =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O || !lightMode) {
|
||||
colorPrimaryVariant
|
||||
|
|
|
@ -1,42 +1,97 @@
|
|||
package eu.kanade.tachiyomi.ui.webview
|
||||
|
||||
import android.app.assist.AssistContent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.webkit.WebView
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.addCallback
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import android.widget.Toast
|
||||
import androidx.core.net.toUri
|
||||
import co.touchlab.kermit.Logger
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.util.system.WebViewClientCompat
|
||||
import eu.kanade.tachiyomi.util.system.WebViewUtil
|
||||
import eu.kanade.tachiyomi.util.system.extensionIntentForText
|
||||
import eu.kanade.tachiyomi.util.system.getResourceColor
|
||||
import eu.kanade.tachiyomi.util.system.openInBrowser
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import eu.kanade.tachiyomi.util.view.setComposeContent
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import yokai.i18n.MR
|
||||
import yokai.presentation.webview.WebViewScreenContent
|
||||
import yokai.util.lang.getString
|
||||
|
||||
open class WebViewActivity : BaseWebViewActivity() {
|
||||
|
||||
private val sourceManager by injectLazy<SourceManager>()
|
||||
private var bundle: Bundle? = null
|
||||
|
||||
private val sourceManager: SourceManager by injectLazy()
|
||||
private val network: NetworkHelper by injectLazy()
|
||||
|
||||
private var backPressedCallback: OnBackPressedCallback? = null
|
||||
private val backCallback = {
|
||||
if (binding.webview.canGoBack()) binding.webview.goBack()
|
||||
reEnableBackPressedCallBack()
|
||||
private var assistUrl: String? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
if (!WebViewUtil.supportsWebView(this)) {
|
||||
toast(MR.strings.information_webview_required, Toast.LENGTH_LONG)
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
val url = intent.extras?.getString(URL_KEY) ?: return
|
||||
assistUrl = url
|
||||
|
||||
var headers = emptyMap<String, String>()
|
||||
(sourceManager.get(intent.extras!!.getLong(SOURCE_KEY)) as? HttpSource)?.let { source ->
|
||||
try {
|
||||
headers = source.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }
|
||||
} catch (e: Exception) {
|
||||
Logger.e(e) { "Failed to build headers" }
|
||||
}
|
||||
}
|
||||
|
||||
setComposeContent {
|
||||
WebViewScreenContent(
|
||||
onNavigateUp = { finish() },
|
||||
initialTitle = intent.extras?.getString(TITLE_KEY),
|
||||
url = url,
|
||||
headers = headers,
|
||||
onUrlChange = { assistUrl = it },
|
||||
onShare = this::shareWebpage,
|
||||
onOpenInApp = this::openUrlInApp,
|
||||
onOpenInBrowser = this::openInBrowser,
|
||||
onClearCookies = this::clearCookies,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onProvideAssistContent(outContent: AssistContent) {
|
||||
super.onProvideAssistContent(outContent)
|
||||
assistUrl?.let { outContent.webUri = it.toUri() }
|
||||
}
|
||||
|
||||
private fun openUrlInApp(url: String) {
|
||||
extensionIntentForText(url)?.let { startActivity(it) }
|
||||
}
|
||||
|
||||
private fun shareWebpage(url: String) {
|
||||
try {
|
||||
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||
type = "text/plain"
|
||||
putExtra(Intent.EXTRA_TEXT, url)
|
||||
}
|
||||
startActivity(Intent.createChooser(intent, getString(MR.strings.share)))
|
||||
} catch (e: Exception) {
|
||||
toast(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
private fun openInBrowser(url: String) {
|
||||
openInBrowser(url, forceBrowser = true, fullBrowser = true)
|
||||
}
|
||||
|
||||
private fun clearCookies(url: String) {
|
||||
val cleared = network.cookieJar.remove(url.toHttpUrl())
|
||||
toast("Cleared $cleared cookies for: $url")
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -53,155 +108,4 @@ open class WebViewActivity : BaseWebViewActivity() {
|
|||
return intent
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
title = intent.extras?.getString(TITLE_KEY)
|
||||
|
||||
binding.swipeRefresh.isEnabled = false
|
||||
|
||||
backPressedCallback = onBackPressedDispatcher.addCallback { backCallback() }
|
||||
binding.toolbar.setNavigationOnClickListener {
|
||||
backPressedCallback?.isEnabled = false
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
}
|
||||
if (bundle == null) {
|
||||
val url = intent.extras!!.getString(URL_KEY) ?: return
|
||||
var headers = emptyMap<String, String>()
|
||||
(sourceManager.get(intent.extras!!.getLong(SOURCE_KEY)) as? HttpSource)?.let { source ->
|
||||
try {
|
||||
headers = source.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }
|
||||
} catch (e: Exception) {
|
||||
Logger.e(e) { "Failed to build headers" }
|
||||
}
|
||||
}
|
||||
|
||||
headers["user-agent"]?.let {
|
||||
binding.webview.settings.userAgentString = it
|
||||
}
|
||||
|
||||
binding.webview.webViewClient = object : WebViewClientCompat() {
|
||||
override fun shouldOverrideUrlCompat(view: WebView, url: String): Boolean {
|
||||
// Don't attempt to open blobs as webpages
|
||||
if (url.startsWith("blob:http")) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Continue with request, but with custom headers
|
||||
view.loadUrl(url, headers)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onPageFinished(view: WebView?, url: String?) {
|
||||
super.onPageFinished(view, url)
|
||||
invalidateOptionsMenu()
|
||||
binding.swipeRefresh.isEnabled = true
|
||||
binding.swipeRefresh.isRefreshing = false
|
||||
}
|
||||
|
||||
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
|
||||
super.onPageStarted(view, url, favicon)
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
|
||||
override fun onPageCommitVisible(view: WebView?, url: String?) {
|
||||
super.onPageCommitVisible(view, url)
|
||||
binding.webview.scrollTo(0, 0)
|
||||
}
|
||||
|
||||
override fun doUpdateVisitedHistory(
|
||||
view: WebView?,
|
||||
url: String?,
|
||||
isReload: Boolean,
|
||||
) {
|
||||
super.doUpdateVisitedHistory(view, url, isReload)
|
||||
if (!isReload) {
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.webview.loadUrl(url, headers)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the options menu of the toolbar is being created. It adds our custom menu.
|
||||
*/
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.webview, menu)
|
||||
return true
|
||||
}
|
||||
|
||||
private fun reEnableBackPressedCallBack() {
|
||||
backPressedCallback?.isEnabled = binding.webview.canGoBack()
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
|
||||
val backItem = binding.toolbar.menu.findItem(R.id.action_web_back)
|
||||
val forwardItem = binding.toolbar.menu.findItem(R.id.action_web_forward)
|
||||
backItem?.isEnabled = binding.webview.canGoBack()
|
||||
forwardItem?.isEnabled = binding.webview.canGoForward()
|
||||
val hasHistory = binding.webview.canGoBack() || binding.webview.canGoForward()
|
||||
backItem?.isVisible = hasHistory
|
||||
forwardItem?.isVisible = hasHistory
|
||||
val tintColor = getResourceColor(R.attr.actionBarTintColor)
|
||||
val translucentWhite = ColorUtils.setAlphaComponent(tintColor, 127)
|
||||
backItem.icon?.setTint(if (binding.webview.canGoBack()) tintColor else translucentWhite)
|
||||
forwardItem?.icon?.setTint(if (binding.webview.canGoForward()) tintColor else translucentWhite)
|
||||
val extenstionCanOpenUrl = binding.webview.canGoBack() &&
|
||||
binding.webview.url?.let { extensionIntentForText(it) != null } ?: false
|
||||
binding.toolbar.menu.findItem(R.id.action_open_in_app)?.isVisible = extenstionCanOpenUrl
|
||||
reEnableBackPressedCallBack()
|
||||
return super.onPrepareOptionsMenu(menu)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when an item of the options menu was clicked. Used to handle clicks on our menu
|
||||
* entries.
|
||||
*/
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.action_web_back -> binding.webview.goBack()
|
||||
R.id.action_web_forward -> binding.webview.goForward()
|
||||
R.id.action_web_share -> shareWebpage()
|
||||
R.id.action_web_browser -> openInBrowser()
|
||||
R.id.action_open_in_app -> openUrlInApp()
|
||||
R.id.action_web_clear_cookies -> clearCookies(binding.webview.url)
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private fun clearCookies(url: String?) {
|
||||
url?.toHttpUrlOrNull()?.let {
|
||||
val cleared = network.cookieJar.remove(it)
|
||||
toast("Cleared $cleared cookies for: $url")
|
||||
}
|
||||
}
|
||||
|
||||
private fun openUrlInApp() {
|
||||
val url = binding.webview.url ?: return
|
||||
extensionIntentForText(url)?.let { startActivity(it) }
|
||||
}
|
||||
|
||||
private fun shareWebpage() {
|
||||
try {
|
||||
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||
type = "text/plain"
|
||||
putExtra(Intent.EXTRA_TEXT, binding.webview.url)
|
||||
}
|
||||
startActivity(Intent.createChooser(intent, getString(MR.strings.share)))
|
||||
} catch (e: Exception) {
|
||||
toast(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
private fun openInBrowser() {
|
||||
binding.webview.url?.let { openInBrowser(it, forceBrowser = true, fullBrowser = true) }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
package eu.kanade.tachiyomi.ui.webview
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import eu.kanade.tachiyomi.util.compose.currentOrThrow
|
||||
import yokai.presentation.webview.WebViewScreenContent
|
||||
import yokai.util.AssistContentScreen
|
||||
import yokai.util.Screen
|
||||
|
||||
class WebViewScreen(
|
||||
private val url: String,
|
||||
private val initialTitle: String? = null,
|
||||
private val sourceId: Long? = null,
|
||||
) : Screen(), AssistContentScreen {
|
||||
|
||||
private var assistUrl: String? = null
|
||||
|
||||
override fun onProvideAssistUrl() = assistUrl
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
val context = LocalContext.current
|
||||
val screenModel = rememberScreenModel { WebViewScreenModel(sourceId) }
|
||||
|
||||
WebViewScreenContent(
|
||||
onNavigateUp = { navigator.pop() },
|
||||
initialTitle = initialTitle,
|
||||
url = url,
|
||||
headers = screenModel.headers,
|
||||
onUrlChange = { assistUrl = it },
|
||||
onShare = { screenModel.shareWebpage(context, it) },
|
||||
onOpenInApp = { screenModel.openInApp(context, it) },
|
||||
onOpenInBrowser = { screenModel.openInBrowser(context, it) },
|
||||
onClearCookies = screenModel::clearCookies,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
package eu.kanade.tachiyomi.ui.webview
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||
import co.touchlab.kermit.Logger
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.util.system.extensionIntentForText
|
||||
import eu.kanade.tachiyomi.util.system.openInBrowser
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import yokai.i18n.MR
|
||||
import yokai.presentation.StatsScreenState
|
||||
import yokai.util.lang.getString
|
||||
|
||||
class WebViewScreenModel(
|
||||
val sourceId: Long?,
|
||||
private val sourceManager: SourceManager = Injekt.get(),
|
||||
private val network: NetworkHelper = Injekt.get(),
|
||||
) : StateScreenModel<StatsScreenState>(StatsScreenState.Loading) {
|
||||
|
||||
var headers = emptyMap<String, String>()
|
||||
|
||||
init {
|
||||
sourceId?.let { sourceManager.get(it) as? HttpSource }?.let { source ->
|
||||
try {
|
||||
headers = source.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }
|
||||
} catch (e: Exception) {
|
||||
Logger.e(e) { "Failed to build headers" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun shareWebpage(context: Context, url: String) {
|
||||
try {
|
||||
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||
type = "text/plain"
|
||||
putExtra(Intent.EXTRA_TEXT, url)
|
||||
}
|
||||
context.startActivity(Intent.createChooser(intent, context.getString(MR.strings.share)))
|
||||
} catch (e: Exception) {
|
||||
context.toast(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
fun openInApp(context: Context, url: String) {
|
||||
context.extensionIntentForText(url)?.let { context.startActivity(it) }
|
||||
}
|
||||
|
||||
fun openInBrowser(context: Context, url: String) {
|
||||
context.openInBrowser(url, forceBrowser = true, fullBrowser = true)
|
||||
}
|
||||
|
||||
fun clearCookies(url: String) {
|
||||
url.toHttpUrlOrNull()?.let {
|
||||
val cleared = network.cookieJar.remove(it)
|
||||
Logger.d { "Cleared $cleared cookies for: $url" }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -32,6 +32,8 @@ import android.view.WindowInsets
|
|||
import android.widget.Button
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.TextView
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.annotation.Dimension
|
||||
import androidx.annotation.FloatRange
|
||||
import androidx.annotation.IdRes
|
||||
|
@ -39,6 +41,12 @@ import androidx.annotation.RequiresApi
|
|||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.view.menu.MenuBuilder
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionContext
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.core.animation.addListener
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.ColorUtils
|
||||
|
@ -64,7 +72,6 @@ import com.github.florent37.viewtooltip.ViewTooltip
|
|||
import com.google.android.material.button.MaterialButton
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
import com.google.android.material.datepicker.MaterialDatePicker
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.math.MathUtils
|
||||
import com.google.android.material.navigation.NavigationBarItemView
|
||||
import com.google.android.material.navigation.NavigationBarMenuView
|
||||
|
@ -74,9 +81,6 @@ import com.google.android.material.shape.ShapeAppearanceModel
|
|||
import com.google.android.material.snackbar.Snackbar
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import eu.kanade.tachiyomi.R
|
||||
import yokai.i18n.MR
|
||||
import yokai.util.lang.getString
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import eu.kanade.tachiyomi.util.lang.tintText
|
||||
import eu.kanade.tachiyomi.util.system.ThemeUtil
|
||||
import eu.kanade.tachiyomi.util.system.dpToPx
|
||||
|
@ -90,6 +94,24 @@ import eu.kanade.tachiyomi.widget.StaggeredGridLayoutManagerAccurateOffset
|
|||
import kotlin.math.max
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.roundToInt
|
||||
import yokai.presentation.theme.YokaiTheme
|
||||
import yokai.util.lang.getString
|
||||
|
||||
inline fun ComponentActivity.setComposeContent(
|
||||
parent: CompositionContext? = null,
|
||||
crossinline content: @Composable () -> Unit,
|
||||
) {
|
||||
setContent(parent) {
|
||||
YokaiTheme {
|
||||
CompositionLocalProvider(
|
||||
LocalTextStyle provides MaterialTheme.typography.bodySmall,
|
||||
LocalContentColor provides MaterialTheme.colorScheme.onBackground,
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns coordinates of view.
|
||||
|
|
16
app/src/main/java/yokai/presentation/StatsScreenState.kt
Normal file
16
app/src/main/java/yokai/presentation/StatsScreenState.kt
Normal file
|
@ -0,0 +1,16 @@
|
|||
package yokai.presentation
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
|
||||
sealed interface StatsScreenState {
|
||||
@Immutable
|
||||
data object Loading : StatsScreenState
|
||||
|
||||
// @Immutable
|
||||
// data class Success(
|
||||
// val overview: StatsData.Overview,
|
||||
// val titles: StatsData.Titles,
|
||||
// val chapters: StatsData.Chapters,
|
||||
// val trackers: StatsData.Trackers,
|
||||
// ) : StatsScreenState
|
||||
}
|
160
app/src/main/java/yokai/presentation/component/AppBar.kt
Normal file
160
app/src/main/java/yokai/presentation/component/AppBar.kt
Normal file
|
@ -0,0 +1,160 @@
|
|||
package yokai.presentation.component
|
||||
|
||||
import androidx.compose.foundation.basicMarquee
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.ArrowBack
|
||||
import androidx.compose.material.icons.outlined.MoreVert
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.PlainTooltip
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TooltipBox
|
||||
import androidx.compose.material3.TooltipDefaults
|
||||
import androidx.compose.material3.rememberTooltipState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import yokai.i18n.MR
|
||||
|
||||
@Composable
|
||||
fun AppBarTitle(
|
||||
title: String?,
|
||||
modifier: Modifier = Modifier,
|
||||
subtitle: String? = null,
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
title?.let {
|
||||
Text(
|
||||
text = it,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
subtitle?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.basicMarquee(
|
||||
repeatDelayMillis = 2_000,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AppBarActions(
|
||||
actions: ImmutableList<AppBar.AppBarAction>,
|
||||
) {
|
||||
var showMenu by remember { mutableStateOf(false) }
|
||||
|
||||
actions.filterIsInstance<AppBar.Action>().map {
|
||||
TooltipBox (
|
||||
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
|
||||
tooltip = {
|
||||
PlainTooltip {
|
||||
Text(it.title)
|
||||
}
|
||||
},
|
||||
state = rememberTooltipState(),
|
||||
) {
|
||||
IconButton(
|
||||
onClick = it.onClick,
|
||||
enabled = it.enabled,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = it.icon,
|
||||
tint = it.iconTint ?: LocalContentColor.current,
|
||||
contentDescription = it.title,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val overflowActions = actions
|
||||
.filterIsInstance<AppBar.OverflowAction>()
|
||||
.filter { it.isVisible }
|
||||
if (overflowActions.isNotEmpty()) {
|
||||
TooltipBox(
|
||||
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
|
||||
tooltip = {
|
||||
PlainTooltip {
|
||||
Text(stringResource(MR.strings.action_menu_overflow_description))
|
||||
}
|
||||
},
|
||||
state = rememberTooltipState(),
|
||||
) {
|
||||
IconButton(
|
||||
onClick = { showMenu = !showMenu },
|
||||
) {
|
||||
Icon(
|
||||
Icons.Outlined.MoreVert,
|
||||
contentDescription = stringResource(MR.strings.action_menu_overflow_description),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
DropdownMenu(
|
||||
expanded = showMenu,
|
||||
onDismissRequest = { showMenu = false },
|
||||
) {
|
||||
overflowActions.map {
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
it.onClick()
|
||||
showMenu = false
|
||||
},
|
||||
text = { Text(it.title, fontWeight = FontWeight.Normal) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UpIcon(
|
||||
modifier: Modifier = Modifier,
|
||||
navigationIcon: ImageVector? = null,
|
||||
) {
|
||||
val icon = navigationIcon
|
||||
?: Icons.AutoMirrored.Outlined.ArrowBack
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = stringResource(MR.strings.action_bar_up_description),
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
sealed interface AppBar {
|
||||
sealed interface AppBarAction
|
||||
|
||||
data class Action(
|
||||
val title: String,
|
||||
val icon: ImageVector,
|
||||
val iconTint: Color? = null,
|
||||
val onClick: () -> Unit,
|
||||
val enabled: Boolean = true,
|
||||
) : AppBarAction
|
||||
|
||||
data class OverflowAction(
|
||||
val title: String,
|
||||
val onClick: () -> Unit,
|
||||
val isVisible: Boolean = true,
|
||||
) : AppBarAction
|
||||
}
|
30
app/src/main/java/yokai/presentation/component/Banners.kt
Normal file
30
app/src/main/java/yokai/presentation/component/Banners.kt
Normal file
|
@ -0,0 +1,30 @@
|
|||
package yokai.presentation.component
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
|
||||
@Composable
|
||||
fun WarningBanner(
|
||||
textRes: StringResource,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(textRes),
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colorScheme.error)
|
||||
.padding(16.dp),
|
||||
color = MaterialTheme.colorScheme.onError,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
|
@ -0,0 +1,251 @@
|
|||
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,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -8,3 +8,7 @@ abstract class Screen : Screen {
|
|||
|
||||
override val key: ScreenKey = uniqueScreenKey
|
||||
}
|
||||
|
||||
interface AssistContentScreen {
|
||||
fun onProvideAssistUrl(): String?
|
||||
}
|
||||
|
|
|
@ -7,6 +7,8 @@ import android.webkit.CookieManager
|
|||
import android.webkit.WebSettings
|
||||
import android.webkit.WebView
|
||||
import co.touchlab.kermit.Logger
|
||||
import kotlin.coroutines.resume
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
|
||||
object WebViewUtil {
|
||||
const val MINIMUM_WEBVIEW_VERSION = 114
|
||||
|
@ -65,3 +67,7 @@ private fun WebView.getDefaultUserAgentString(): String {
|
|||
|
||||
return defaultUserAgentString
|
||||
}
|
||||
|
||||
suspend fun WebView.getHtml(): String = suspendCancellableCoroutine {
|
||||
evaluateJavascript("document.documentElement.outerHTML") { html -> it.resume(html) }
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ material-motion = { module = "io.github.fornewid:material-motion-compose-core",
|
|||
ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
|
||||
ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
|
||||
icons = { module = "androidx.compose.material:material-icons-extended" }
|
||||
webview = { module = "io.github.kevinnzou:compose-webview", version = "0.33.3" }
|
||||
|
||||
[bundles]
|
||||
compose = [ "animation", "foundation", "material3", "material-motion", "ui-tooling-preview", "icons" ]
|
||||
|
|
|
@ -18,7 +18,6 @@ voyager = "1.1.0-beta02"
|
|||
[libraries]
|
||||
aboutlibraries = { module = "com.mikepenz:aboutlibraries-compose-m3", version.ref = "aboutlibraries" }
|
||||
|
||||
accompanist-webview = { module = "com.google.accompanist:accompanist-webview", version = "0.30.1" }
|
||||
chucker-library-no-op = { module = "com.github.ChuckerTeam.Chucker:library-no-op", version.ref = "chucker" }
|
||||
chucker-library = { module = "com.github.ChuckerTeam.Chucker:library", version.ref = "chucker" }
|
||||
coil3-bom = { module = "io.coil-kt.coil3:coil-bom", version = "3.0.3" }
|
||||
|
@ -99,6 +98,7 @@ viewtooltip = { module = "com.github.CarlosEsco:ViewTooltip", version = "f79a895
|
|||
|
||||
voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" }
|
||||
voyager-transitions = { module = "cafe.adriel.voyager:voyager-transitions", version.ref = "voyager" }
|
||||
voyager-screenmodel = { module = "cafe.adriel.voyager:voyager-screenmodel", version.ref = "voyager" }
|
||||
|
||||
[plugins]
|
||||
aboutlibraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "aboutlibraries" }
|
||||
|
@ -120,4 +120,4 @@ sqlite = [ "sqlite-ktx" ]
|
|||
test = [ "junit-api", "kotest-assertions", "mockk" ]
|
||||
test-android = [ "junit-android" ]
|
||||
test-runtime = [ "junit-engine" ]
|
||||
voyager = ["voyager-navigator", "voyager-transitions"]
|
||||
voyager = ["voyager-navigator", "voyager-transitions", "voyager-screenmodel"]
|
||||
|
|
|
@ -4,6 +4,9 @@
|
|||
<string name="app_short_name" translatable="false">Yōkai</string>
|
||||
<string name="app_normalized_name" translatable="false">Yokai</string>
|
||||
|
||||
<string name="action_menu_overflow_description">More options</string>
|
||||
<string name="action_bar_up_description">Navigate up</string>
|
||||
|
||||
<string name="all_files_permission_required">File permissions required</string>
|
||||
<string name="external_storage_permission_notice">TachiyomiJ2K requires access to all files in Android 11 to download chapters, create automatic backups, and read local series. \n\nOn the next screen, enable \"Allow access to manage all files.\"</string>
|
||||
<string name="external_storage_download_notice">TachiyomiJ2K requires access to all files to download chapters. Tap here, then enable \"Allow access to manage all files.\"</string>
|
||||
|
@ -1037,9 +1040,14 @@
|
|||
|
||||
<!-- Webview -->
|
||||
<string name="failed_to_bypass_cloudflare">Failed to bypass Cloudflare</string>
|
||||
<string name="information_cloudflare_help">Tap here for help with Cloudflare</string>
|
||||
<string name="please_update_webview">Please update the WebView app for better compatibility</string>
|
||||
<string name="information_webview_required">WebView is required for the app to function</string>
|
||||
<!-- Do not translate "WebView" -->
|
||||
<string name="webview_is_required">WebView is required for Tachiyomi</string>
|
||||
<string name="action_webview_back">Back</string>
|
||||
<string name="action_webview_forward">Forward</string>
|
||||
<string name="action_webview_refresh">Refresh</string>
|
||||
|
||||
<!-- App widget -->
|
||||
<string name="appwidget_updates_description">See your recently updated library entries</string>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue