From 558b18899cca41992d6077484ed430972cda35fc Mon Sep 17 00:00:00 2001 From: arkon Date: Sun, 24 Apr 2022 10:22:22 -0400 Subject: [PATCH] Migrate WebViewActivity to Compose --- app/build.gradle.kts | 4 +- .../kanade/presentation/components/AppBar.kt | 103 ++++++++++ .../presentation/webview/WebViewScreen.kt | 152 ++++++++++++++ .../tachiyomi/ui/webview/WebViewActivity.kt | 190 +++--------------- .../tachiyomi/util/system/WebViewUtil.kt | 2 +- .../main/res/drawable/ic_arrow_back_24dp.xml | 9 - app/src/main/res/layout/webview_activity.xml | 40 ---- app/src/main/res/menu/webview.xml | 39 ---- app/src/main/res/values/strings.xml | 1 + gradle/compose.versions.toml | 9 +- 10 files changed, 291 insertions(+), 258 deletions(-) create mode 100644 app/src/main/java/eu/kanade/presentation/components/AppBar.kt create mode 100644 app/src/main/java/eu/kanade/presentation/webview/WebViewScreen.kt delete mode 100644 app/src/main/res/drawable/ic_arrow_back_24dp.xml delete mode 100644 app/src/main/res/layout/webview_activity.xml delete mode 100644 app/src/main/res/menu/webview.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index af695f04f..c811dafff 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -139,12 +139,15 @@ android { } dependencies { + // Compose + implementation(compose.activity) implementation(compose.foundation) implementation(compose.material3.core) implementation(compose.material3.adapter) implementation(compose.material.icons) implementation(compose.animation) implementation(compose.ui.tooling) + implementation(compose.accompanist.webview) implementation(androidx.paging.runtime) implementation(androidx.paging.compose) @@ -154,7 +157,6 @@ dependencies { implementation(libs.sqldelight.android.paging) implementation(kotlinx.reflect) - implementation(kotlinx.bundles.coroutines) // Source models and interfaces from Tachiyomi 1.x diff --git a/app/src/main/java/eu/kanade/presentation/components/AppBar.kt b/app/src/main/java/eu/kanade/presentation/components/AppBar.kt new file mode 100644 index 000000000..9a1acbf45 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/components/AppBar.kt @@ -0,0 +1,103 @@ +package eu.kanade.presentation.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.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.MaterialTheme +import androidx.compose.material3.Text +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.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import eu.kanade.tachiyomi.R + +@Composable +fun AppBarTitle( + title: String?, + subtitle: String? = null, +) { + val subtitleTextStyle = MaterialTheme.typography.bodyMedium + + Column { + title?.let { + Text( + text = it, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + subtitle?.let { + Text( + text = it, + style = subtitleTextStyle, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } +} + +@Composable +fun AppBarActions( + actions: List, +) { + var showMenu by remember { mutableStateOf(false) } + + actions.filterIsInstance().map { + IconButton( + onClick = it.onClick, + enabled = it.isEnabled, + ) { + Icon( + imageVector = it.icon, + contentDescription = it.title, + ) + } + } + + val overflowActions = actions.filterIsInstance() + if (overflowActions.isNotEmpty()) { + IconButton(onClick = { showMenu = !showMenu }) { + Icon(Icons.Default.MoreVert, contentDescription = stringResource(R.string.label_more)) + } + + DropdownMenu( + expanded = showMenu, + onDismissRequest = { showMenu = false } + ) { + overflowActions.map { + DropdownMenuItem( + onClick = { + it.onClick() + showMenu = false + }, + text = { Text(it.title) }, + ) + } + } + } +} + +object AppBar { + interface AppBarAction + + data class Action( + val title: String, + val icon: ImageVector, + val onClick: () -> Unit, + val isEnabled: Boolean = true, + ) : AppBarAction + + data class OverflowAction( + val title: String, + val onClick: () -> Unit, + ) : AppBarAction +} diff --git a/app/src/main/java/eu/kanade/presentation/webview/WebViewScreen.kt b/app/src/main/java/eu/kanade/presentation/webview/WebViewScreen.kt new file mode 100644 index 000000000..aece16b97 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/webview/WebViewScreen.kt @@ -0,0 +1,152 @@ +package eu.kanade.presentation.webview + +import android.content.pm.ApplicationInfo +import android.webkit.WebResourceRequest +import android.webkit.WebView +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.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.filled.ArrowForward +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.SmallTopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +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.AppBarTitle +import eu.kanade.tachiyomi.BuildConfig +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.util.system.setDefaultSettings + +@Composable +fun WebViewScreen( + onUp: () -> Unit, + initialTitle: String?, + url: String, + headers: Map = emptyMap(), + onShare: (String) -> Unit, + onOpenInBrowser: (String) -> Unit, + onClearCookies: (String) -> Unit, +) { + val context = LocalContext.current + val state = rememberWebViewState(url = url) + val navigator = rememberWebViewNavigator() + + Column { + SmallTopAppBar( + title = { + AppBarTitle( + title = state.pageTitle ?: initialTitle, + subtitle = state.content.getCurrentUrl(), + ) + }, + navigationIcon = { + IconButton(onClick = onUp) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = stringResource(R.string.action_close), + ) + } + }, + actions = { + AppBarActions( + listOf( + AppBar.Action( + title = stringResource(R.string.action_webview_back), + icon = Icons.Default.ArrowBack, + onClick = { + if (navigator.canGoBack) { + navigator.navigateBack() + } + }, + isEnabled = navigator.canGoBack, + ), + AppBar.Action( + title = stringResource(R.string.action_webview_forward), + icon = Icons.Default.ArrowForward, + onClick = { + if (navigator.canGoForward) { + navigator.navigateForward() + } + }, + isEnabled = navigator.canGoForward, + ), + AppBar.OverflowAction( + title = stringResource(R.string.action_webview_refresh), + onClick = { navigator.reload() }, + ), + AppBar.OverflowAction( + title = stringResource(R.string.action_share), + onClick = { onShare(state.content.getCurrentUrl()!!) }, + ), + AppBar.OverflowAction( + title = stringResource(R.string.action_open_in_browser), + onClick = { onOpenInBrowser(state.content.getCurrentUrl()!!) }, + ), + AppBar.OverflowAction( + title = stringResource(R.string.pref_clear_cookies), + onClick = { onClearCookies(state.content.getCurrentUrl()!!) }, + ), + ), + ) + }, + ) + + Box(modifier = Modifier.weight(1f)) { + val loadingState = state.loadingState + if (loadingState is LoadingState.Loading) { + LinearProgressIndicator( + progress = loadingState.progress, + modifier = Modifier.fillMaxWidth(), + ) + } + + val webClient = remember { + object : AccompanistWebViewClient() { + override fun shouldOverrideUrlLoading( + view: WebView?, + request: WebResourceRequest?, + ): Boolean { + request?.let { + view?.loadUrl(it.url.toString(), headers) + } + return super.shouldOverrideUrlLoading(view, request) + } + } + } + + WebView( + state = state, + modifier = Modifier.fillMaxSize(), + navigator = navigator, + onCreated = { webView -> + webView.setDefaultSettings() + + // Debug mode (chrome://inspect/#devices) + if (BuildConfig.DEBUG && 0 != context.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) { + WebView.setWebContentsDebuggingEnabled(true) + } + + headers["User-Agent"]?.let { + webView.settings.userAgentString = it + } + }, + client = webClient, + ) + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewActivity.kt index 794df9097..cc0db61d2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewActivity.kt @@ -2,50 +2,28 @@ package eu.kanade.tachiyomi.ui.webview import android.content.Context import android.content.Intent -import android.content.pm.ApplicationInfo -import android.graphics.Bitmap import android.os.Bundle -import android.view.Menu -import android.view.MenuItem -import android.webkit.WebChromeClient -import android.webkit.WebView import android.widget.Toast -import androidx.core.graphics.ColorUtils -import androidx.core.view.isInvisible -import androidx.core.view.isVisible -import androidx.lifecycle.lifecycleScope -import eu.kanade.tachiyomi.BuildConfig +import androidx.activity.compose.setContent +import eu.kanade.presentation.theme.TachiyomiTheme +import eu.kanade.presentation.webview.WebViewScreen import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.databinding.WebviewActivityBinding import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.ui.base.activity.BaseActivity -import eu.kanade.tachiyomi.util.system.WebViewClientCompat import eu.kanade.tachiyomi.util.system.WebViewUtil -import eu.kanade.tachiyomi.util.system.getResourceColor import eu.kanade.tachiyomi.util.system.logcat import eu.kanade.tachiyomi.util.system.openInBrowser -import eu.kanade.tachiyomi.util.system.setDefaultSettings import eu.kanade.tachiyomi.util.system.toast -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach import okhttp3.HttpUrl.Companion.toHttpUrl -import reactivecircus.flowbinding.appcompat.navigationClicks -import reactivecircus.flowbinding.swiperefreshlayout.refreshes import uy.kohesive.injekt.injectLazy class WebViewActivity : BaseActivity() { - private lateinit var binding: WebviewActivityBinding - private val sourceManager: SourceManager by injectLazy() private val network: NetworkHelper by injectLazy() - private var bundle: Bundle? = null - - private var isRefreshing: Boolean = false - init { registerSecureActivity(this) } @@ -59,152 +37,33 @@ class WebViewActivity : BaseActivity() { return } - try { - binding = WebviewActivityBinding.inflate(layoutInflater) - setContentView(binding.root) - } catch (e: Throwable) { - // Potentially throws errors like "Error inflating class android.webkit.WebView" - toast(R.string.information_webview_required, Toast.LENGTH_LONG) - finish() - return + val url = intent.extras!!.getString(URL_KEY) ?: return + var headers = mutableMapOf() + val source = sourceManager.get(intent.extras!!.getLong(SOURCE_KEY)) as? HttpSource + if (source != null) { + headers = source.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }.toMutableMap() } - title = intent.extras?.getString(TITLE_KEY) - - setSupportActionBar(binding.toolbar) - supportActionBar?.setDisplayHomeAsUpEnabled(true) - binding.toolbar.navigationClicks() - .onEach { super.onBackPressed() } - .launchIn(lifecycleScope) - - binding.swipeRefresh.isEnabled = false - binding.swipeRefresh.refreshes() - .onEach { refreshPage() } - .launchIn(lifecycleScope) - - if (bundle == null) { - binding.webview.setDefaultSettings() - - // Debug mode (chrome://inspect/#devices) - if (BuildConfig.DEBUG && 0 != applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) { - WebView.setWebContentsDebuggingEnabled(true) + setContent { + TachiyomiTheme { + WebViewScreen( + onUp = { finish() }, + initialTitle = intent.extras?.getString(TITLE_KEY), + url = url, + headers = headers, + onShare = this::shareWebpage, + onOpenInBrowser = this::openInBrowser, + onClearCookies = this::clearCookies, + ) } - - 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 - } - super.onProgressChanged(view, newProgress) - } - } - } else { - binding.webview.restoreState(bundle!!) - } - - if (bundle == null) { - val url = intent.extras!!.getString(URL_KEY) ?: return - - var headers = mutableMapOf() - val source = sourceManager.get(intent.extras!!.getLong(SOURCE_KEY)) as? HttpSource - if (source != null) { - headers = source.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }.toMutableMap() - binding.webview.settings.userAgentString = source.headers["User-Agent"] - } - - supportActionBar?.subtitle = url - - binding.webview.webViewClient = object : WebViewClientCompat() { - override fun shouldOverrideUrlCompat(view: WebView, url: String): Boolean { - view.loadUrl(url, headers) - return true - } - - override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { - super.onPageStarted(view, url, favicon) - invalidateOptionsMenu() - } - - override fun onPageFinished(view: WebView?, url: String?) { - super.onPageFinished(view, url) - invalidateOptionsMenu() - title = view?.title - supportActionBar?.subtitle = url - binding.swipeRefresh.isEnabled = true - binding.swipeRefresh.isRefreshing = false - - // Reset to top when page refreshes - if (isRefreshing) { - view?.scrollTo(0, 0) - isRefreshing = false - } - } - } - - binding.webview.loadUrl(url, headers) } } - @Suppress("UNNECESSARY_SAFE_CALL") - override fun onDestroy() { - super.onDestroy() - - // Binding sometimes isn't actually instantiated yet somehow - binding?.webview?.destroy() - } - - override fun onCreateOptionsMenu(menu: Menu): Boolean { - menuInflater.inflate(R.menu.webview, menu) - return true - } - - override fun onPrepareOptionsMenu(menu: Menu): Boolean { - val iconTintColor = getResourceColor(R.attr.colorOnSurface) - val translucentIconTintColor = ColorUtils.setAlphaComponent(iconTintColor, 127) - - menu.findItem(R.id.action_web_back).apply { - isEnabled = binding.webview.canGoBack() - icon.setTint(if (binding.webview.canGoBack()) iconTintColor else translucentIconTintColor) - } - - menu.findItem(R.id.action_web_forward).apply { - isEnabled = binding.webview.canGoForward() - icon.setTint(if (binding.webview.canGoForward()) iconTintColor else translucentIconTintColor) - } - - return super.onPrepareOptionsMenu(menu) - } - - 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_refresh -> refreshPage() - R.id.action_web_share -> shareWebpage() - R.id.action_web_browser -> openInBrowser() - R.id.action_clear_cookies -> clearCookies() - } - return super.onOptionsItemSelected(item) - } - - override fun onBackPressed() { - if (binding.webview.canGoBack()) binding.webview.goBack() - else super.onBackPressed() - } - - private fun refreshPage() { - binding.swipeRefresh.isRefreshing = true - binding.webview.reload() - isRefreshing = true - } - - private fun shareWebpage() { + private fun shareWebpage(url: String) { try { val intent = Intent(Intent.ACTION_SEND).apply { type = "text/plain" - putExtra(Intent.EXTRA_TEXT, binding.webview.url) + putExtra(Intent.EXTRA_TEXT, url) } startActivity(Intent.createChooser(intent, getString(R.string.action_share))) } catch (e: Exception) { @@ -212,12 +71,11 @@ class WebViewActivity : BaseActivity() { } } - private fun openInBrowser() { - openInBrowser(binding.webview.url!!, forceDefaultBrowser = true) + private fun openInBrowser(url: String) { + openInBrowser(url, forceDefaultBrowser = true) } - private fun clearCookies() { - val url = binding.webview.url!! + private fun clearCookies(url: String) { val cleared = network.cookieManager.remove(url.toHttpUrl()) logcat { "Cleared $cleared cookies for: $url" } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/WebViewUtil.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/WebViewUtil.kt index d04f136a6..af0b83041 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/system/WebViewUtil.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/WebViewUtil.kt @@ -11,7 +11,7 @@ import logcat.LogPriority object WebViewUtil { const val SPOOF_PACKAGE_NAME = "org.chromium.chrome" - const val MINIMUM_WEBVIEW_VERSION = 95 + const val MINIMUM_WEBVIEW_VERSION = 98 fun supportsWebView(context: Context): Boolean { try { diff --git a/app/src/main/res/drawable/ic_arrow_back_24dp.xml b/app/src/main/res/drawable/ic_arrow_back_24dp.xml deleted file mode 100644 index 1e635c5be..000000000 --- a/app/src/main/res/drawable/ic_arrow_back_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/layout/webview_activity.xml b/app/src/main/res/layout/webview_activity.xml deleted file mode 100644 index 480c80a79..000000000 --- a/app/src/main/res/layout/webview_activity.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/menu/webview.xml b/app/src/main/res/menu/webview.xml deleted file mode 100644 index df91be82b..000000000 --- a/app/src/main/res/menu/webview.xml +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a3e513286..3be9e6193 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -121,6 +121,7 @@ Save Reset Undo + Close Open log Tap to see details Create diff --git a/gradle/compose.versions.toml b/gradle/compose.versions.toml index 26a314221..a230f5026 100644 --- a/gradle/compose.versions.toml +++ b/gradle/compose.versions.toml @@ -1,10 +1,15 @@ [versions] compose = "1.2.0-alpha07" +accompanist = "0.24.6-alpha" [libraries] +activity = "androidx.activity:activity-compose:1.6.0-alpha01" foundation = { module = "androidx.compose.foundation:foundation", version.ref="compose" } +animation = { module = "androidx.compose.animation:animation", version.ref="compose" } +ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref="compose" } + material3-core = "androidx.compose.material3:material3:1.0.0-alpha09" material3-adapter = "com.google.android.material:compose-theme-adapter-3:1.0.6" material-icons = { module = "androidx.compose.material:material-icons-extended", version.ref="compose" } -animation = { module = "androidx.compose.animation:animation", version.ref="compose" } -ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref="compose" } + +accompanist-webview = { module = "com.google.accompanist:accompanist-webview", version.ref="accompanist" } \ No newline at end of file