Răsfoiți Sursa

Migrate WebViewActivity to Compose

arkon 2 ani în urmă
părinte
comite
558b18899c

+ 3 - 1
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

+ 103 - 0
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<AppBar.AppBarAction>,
+) {
+    var showMenu by remember { mutableStateOf(false) }
+
+    actions.filterIsInstance<AppBar.Action>().map {
+        IconButton(
+            onClick = it.onClick,
+            enabled = it.isEnabled,
+        ) {
+            Icon(
+                imageVector = it.icon,
+                contentDescription = it.title,
+            )
+        }
+    }
+
+    val overflowActions = actions.filterIsInstance<AppBar.OverflowAction>()
+    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
+}

+ 152 - 0
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<String, String> = 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,
+            )
+        }
+    }
+}

+ 24 - 166
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<String, String>()
+        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<String, String>()
-            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" }
     }

+ 1 - 1
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 {

+ 0 - 9
app/src/main/res/drawable/ic_arrow_back_24dp.xml

@@ -1,9 +0,0 @@
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="24dp"
-    android:height="24dp"
-    android:viewportWidth="24"
-    android:viewportHeight="24">
-    <path
-        android:fillColor="@android:color/black"
-        android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z" />
-</vector>

+ 0 - 40
app/src/main/res/layout/webview_activity.xml

@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    android:fitsSystemWindows="true"
-    android:orientation="vertical">
-
-    <com.google.android.material.appbar.AppBarLayout
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content">
-
-        <com.google.android.material.appbar.MaterialToolbar
-            android:id="@+id/toolbar"
-            android:layout_width="match_parent"
-            android:layout_height="?attr/actionBarSize"
-            app:navigationIcon="@drawable/ic_close_24dp" />
-
-    </com.google.android.material.appbar.AppBarLayout>
-
-    <eu.kanade.tachiyomi.widget.ThemedSwipeRefreshLayout
-        android:id="@id/swipe_refresh"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent">
-
-        <WebView
-            android:id="@+id/webview"
-            android:layout_width="match_parent"
-            android:layout_height="match_parent">
-
-            <com.google.android.material.progressindicator.LinearProgressIndicator
-                android:id="@+id/progress_bar"
-                android:layout_width="match_parent"
-                android:layout_height="wrap_content" />
-
-        </WebView>
-
-    </eu.kanade.tachiyomi.widget.ThemedSwipeRefreshLayout>
-
-</LinearLayout>

+ 0 - 39
app/src/main/res/menu/webview.xml

@@ -1,39 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<menu xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto">
-
-    <item
-        android:id="@+id/action_web_back"
-        android:icon="@drawable/ic_arrow_back_24dp"
-        android:title="@string/action_webview_back"
-        app:iconTint="?attr/colorOnSurface"
-        app:showAsAction="ifRoom" />
-
-    <item
-        android:id="@+id/action_web_forward"
-        android:icon="@drawable/ic_arrow_forward_24dp"
-        android:title="@string/action_webview_forward"
-        app:iconTint="?attr/colorOnSurface"
-        app:showAsAction="ifRoom" />
-
-    <item
-        android:id="@+id/action_web_refresh"
-        android:title="@string/action_webview_refresh"
-        app:showAsAction="never" />
-
-    <item
-        android:id="@+id/action_web_share"
-        android:title="@string/action_share"
-        app:showAsAction="never" />
-
-    <item
-        android:id="@+id/action_web_browser"
-        android:title="@string/action_open_in_browser"
-        app:showAsAction="never" />
-
-    <item
-        android:id="@+id/action_clear_cookies"
-        android:title="@string/pref_clear_cookies"
-        app:showAsAction="never" />
-
-</menu>

+ 1 - 0
app/src/main/res/values/strings.xml

@@ -121,6 +121,7 @@
     <string name="action_save">Save</string>
     <string name="action_reset">Reset</string>
     <string name="action_undo">Undo</string>
+    <string name="action_close">Close</string>
     <string name="action_open_log">Open log</string>
     <string name="action_show_errors">Tap to see details</string>
     <string name="action_create">Create</string>

+ 7 - 2
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" }