From b51120e914571d8c2e1435babf0474c0e3f87bbd Mon Sep 17 00:00:00 2001 From: FooIbar <118464521+FooIbar@users.noreply.github.com> Date: Thu, 14 Dec 2023 18:04:51 +0800 Subject: [PATCH] Drop deprecated Accompanist libraries (#500) * Migrate XML themes to Compose * Vendor Accompanist WebView --- app/build.gradle.kts | 2 - .../com/google/accompanist/web/WebView.kt | 653 ++++++++++++++++++ .../kotlin/com/hippo/ehviewer/Settings.kt | 2 +- .../com/hippo/ehviewer/SettingsCollector.kt | 9 - .../com/hippo/ehviewer/ui/EhActivity.kt | 18 - .../kotlin/com/hippo/ehviewer/ui/Theme.kt | 20 +- .../hippo/ehviewer/ui/settings/EhScreen.kt | 2 +- .../com/hippo/ehviewer/ui/theme/Theme.kt | 51 ++ .../tachiyomi/ui/reader/ReaderActivity.kt | 2 - .../reader/viewer/ReaderProgressIndicator.kt | 4 +- .../util/system/ContextExtensions.kt | 3 - app/src/main/res/values/themes.xml | 8 - gradle/libs.versions.toml | 5 - 13 files changed, 721 insertions(+), 58 deletions(-) create mode 100644 app/src/main/kotlin/com/google/accompanist/web/WebView.kt create mode 100644 app/src/main/kotlin/com/hippo/ehviewer/ui/theme/Theme.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f7d2d9d4e..4108475eb 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -233,8 +233,6 @@ dependencies { implementation(libs.androidx.work.runtime) implementation(libs.photoview) // Dead Dependency implementation(libs.directionalviewpager) // Dead Dependency - // https://github.com/google/accompanist/releases - implementation(libs.bundles.accompanist) implementation(libs.material) implementation(libs.bundles.splitties) diff --git a/app/src/main/kotlin/com/google/accompanist/web/WebView.kt b/app/src/main/kotlin/com/google/accompanist/web/WebView.kt new file mode 100644 index 000000000..8b6847a71 --- /dev/null +++ b/app/src/main/kotlin/com/google/accompanist/web/WebView.kt @@ -0,0 +1,653 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.accompanist.web + +import android.content.Context +import android.graphics.Bitmap +import android.os.Bundle +import android.view.ViewGroup.LayoutParams +import android.webkit.WebChromeClient +import android.webkit.WebResourceError +import android.webkit.WebResourceRequest +import android.webkit.WebView +import android.webkit.WebViewClient +import android.widget.FrameLayout +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow +import androidx.compose.runtime.snapshots.SnapshotStateList +import androidx.compose.ui.Modifier +import androidx.compose.ui.viewinterop.AndroidView +import com.google.accompanist.web.LoadingState.Finished +import com.google.accompanist.web.LoadingState.Loading +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +/** + * A wrapper around the Android View WebView to provide a basic WebView composable. + * + * If you require more customisation you are most likely better rolling your own and using this + * wrapper as an example. + * + * The WebView attempts to set the layoutParams based on the Compose modifier passed in. If it + * is incorrectly sizing, use the layoutParams composable function instead. + * + * @param state The webview state holder where the Uri to load is defined. + * @param modifier A compose modifier + * @param captureBackPresses Set to true to have this Composable capture back presses and navigate + * the WebView back. + * @param navigator An optional navigator object that can be used to control the WebView's + * navigation from outside the composable. + * @param onCreated Called when the WebView is first created, this can be used to set additional + * settings on the WebView. WebChromeClient and WebViewClient should not be set here as they will be + * subsequently overwritten after this lambda is called. + * @param onDispose Called when the WebView is destroyed. Provides a bundle which can be saved + * if you need to save and restore state in this WebView. + * @param client Provides access to WebViewClient via subclassing + * @param chromeClient Provides access to WebChromeClient via subclassing + * @param factory An optional WebView factory for using a custom subclass of WebView + */ +@Composable +fun WebView( + state: WebViewState, + modifier: Modifier = Modifier, + captureBackPresses: Boolean = true, + navigator: WebViewNavigator = rememberWebViewNavigator(), + onCreated: (WebView) -> Unit = {}, + onDispose: (WebView) -> Unit = {}, + client: AccompanistWebViewClient = remember { AccompanistWebViewClient() }, + chromeClient: AccompanistWebChromeClient = remember { AccompanistWebChromeClient() }, + factory: ((Context) -> WebView)? = null, +) { + BoxWithConstraints(modifier) { + // WebView changes it's layout strategy based on + // it's layoutParams. We convert from Compose Modifier to + // layout params here. + val width = + if (constraints.hasFixedWidth) { + LayoutParams.MATCH_PARENT + } else { + LayoutParams.WRAP_CONTENT + } + val height = + if (constraints.hasFixedHeight) { + LayoutParams.MATCH_PARENT + } else { + LayoutParams.WRAP_CONTENT + } + + val layoutParams = FrameLayout.LayoutParams( + width, + height, + ) + + WebView( + state, + layoutParams, + Modifier, + captureBackPresses, + navigator, + onCreated, + onDispose, + client, + chromeClient, + factory, + ) + } +} + +/** + * A wrapper around the Android View WebView to provide a basic WebView composable. + * + * If you require more customisation you are most likely better rolling your own and using this + * wrapper as an example. + * + * The WebView attempts to set the layoutParams based on the Compose modifier passed in. If it + * is incorrectly sizing, use the layoutParams composable function instead. + * + * @param state The webview state holder where the Uri to load is defined. + * @param layoutParams A FrameLayout.LayoutParams object to custom size the underlying WebView. + * @param modifier A compose modifier + * @param captureBackPresses Set to true to have this Composable capture back presses and navigate + * the WebView back. + * @param navigator An optional navigator object that can be used to control the WebView's + * navigation from outside the composable. + * @param onCreated Called when the WebView is first created, this can be used to set additional + * settings on the WebView. WebChromeClient and WebViewClient should not be set here as they will be + * subsequently overwritten after this lambda is called. + * @param onDispose Called when the WebView is destroyed. Provides a bundle which can be saved + * if you need to save and restore state in this WebView. + * @param client Provides access to WebViewClient via subclassing + * @param chromeClient Provides access to WebChromeClient via subclassing + * @param factory An optional WebView factory for using a custom subclass of WebView + */ +@Composable +fun WebView( + state: WebViewState, + layoutParams: FrameLayout.LayoutParams, + modifier: Modifier = Modifier, + captureBackPresses: Boolean = true, + navigator: WebViewNavigator = rememberWebViewNavigator(), + onCreated: (WebView) -> Unit = {}, + onDispose: (WebView) -> Unit = {}, + client: AccompanistWebViewClient = remember { AccompanistWebViewClient() }, + chromeClient: AccompanistWebChromeClient = remember { AccompanistWebChromeClient() }, + factory: ((Context) -> WebView)? = null, +) { + val webView = state.webView + + BackHandler(captureBackPresses && navigator.canGoBack) { + webView?.goBack() + } + + webView?.let { wv -> + LaunchedEffect(wv, navigator) { + with(navigator) { + wv.handleNavigationEvents() + } + } + + LaunchedEffect(wv, state) { + snapshotFlow { state.content }.collect { content -> + when (content) { + is WebContent.Url -> { + wv.loadUrl(content.url, content.additionalHttpHeaders) + } + + is WebContent.Data -> { + wv.loadDataWithBaseURL( + content.baseUrl, + content.data, + content.mimeType, + content.encoding, + content.historyUrl, + ) + } + + is WebContent.Post -> { + wv.postUrl( + content.url, + content.postData, + ) + } + + is WebContent.NavigatorOnly -> { + // NO-OP + } + } + } + } + } + + // Set the state of the client and chrome client + // This is done internally to ensure they always are the same instance as the + // parent Web composable + client.state = state + client.navigator = navigator + chromeClient.state = state + + AndroidView( + factory = { context -> + (factory?.invoke(context) ?: WebView(context)).apply { + onCreated(this) + + this.layoutParams = layoutParams + + state.viewState?.let { + this.restoreState(it) + } + + webChromeClient = chromeClient + webViewClient = client + }.also { state.webView = it } + }, + modifier = modifier, + onRelease = { + onDispose(it) + }, + ) +} + +/** + * AccompanistWebViewClient + * + * A parent class implementation of WebViewClient that can be subclassed to add custom behaviour. + * + * As Accompanist Web needs to set its own web client to function, it provides this intermediary + * class that can be overriden if further custom behaviour is required. + */ +open class AccompanistWebViewClient : WebViewClient() { + open lateinit var state: WebViewState + internal set + open lateinit var navigator: WebViewNavigator + internal set + + override fun onPageStarted(view: WebView, url: String?, favicon: Bitmap?) { + super.onPageStarted(view, url, favicon) + state.loadingState = Loading(0.0f) + state.errorsForCurrentRequest.clear() + state.pageTitle = null + state.pageIcon = null + + state.lastLoadedUrl = url + } + + override fun onPageFinished(view: WebView, url: String?) { + super.onPageFinished(view, url) + state.loadingState = Finished + } + + override fun doUpdateVisitedHistory(view: WebView, url: String?, isReload: Boolean) { + super.doUpdateVisitedHistory(view, url, isReload) + + navigator.canGoBack = view.canGoBack() + navigator.canGoForward = view.canGoForward() + } + + override fun onReceivedError( + view: WebView, + request: WebResourceRequest?, + error: WebResourceError?, + ) { + super.onReceivedError(view, request, error) + + if (error != null) { + state.errorsForCurrentRequest.add(WebViewError(request, error)) + } + } +} + +/** + * AccompanistWebChromeClient + * + * A parent class implementation of WebChromeClient that can be subclassed to add custom behaviour. + * + * As Accompanist Web needs to set its own web client to function, it provides this intermediary + * class that can be overriden if further custom behaviour is required. + */ +open class AccompanistWebChromeClient : WebChromeClient() { + open lateinit var state: WebViewState + internal set + + override fun onReceivedTitle(view: WebView, title: String?) { + super.onReceivedTitle(view, title) + state.pageTitle = title + } + + override fun onReceivedIcon(view: WebView, icon: Bitmap?) { + super.onReceivedIcon(view, icon) + state.pageIcon = icon + } + + override fun onProgressChanged(view: WebView, newProgress: Int) { + super.onProgressChanged(view, newProgress) + if (state.loadingState is Finished) return + state.loadingState = Loading(newProgress / 100.0f) + } +} + +sealed class WebContent { + data class Url( + val url: String, + val additionalHttpHeaders: Map = emptyMap(), + ) : WebContent() + + data class Data( + val data: String, + val baseUrl: String? = null, + val encoding: String = "utf-8", + val mimeType: String? = null, + val historyUrl: String? = null, + ) : WebContent() + + data class Post( + val url: String, + val postData: ByteArray, + ) : WebContent() { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Post + + if (url != other.url) return false + if (!postData.contentEquals(other.postData)) return false + + return true + } + + override fun hashCode(): Int { + var result = url.hashCode() + result = 31 * result + postData.contentHashCode() + return result + } + } + + data object NavigatorOnly : WebContent() +} + +/** + * Sealed class for constraining possible loading states. + * See [Loading] and [Finished]. + */ +sealed class LoadingState { + /** + * Describes a WebView that has not yet loaded for the first time. + */ + data object Initializing : LoadingState() + + /** + * Describes a webview between `onPageStarted` and `onPageFinished` events, contains a + * [progress] property which is updated by the webview. + */ + data class Loading(val progress: Float) : LoadingState() + + /** + * Describes a webview that has finished loading content. + */ + data object Finished : LoadingState() +} + +/** + * A state holder to hold the state for the WebView. In most cases this will be remembered + * using the rememberWebViewState(uri) function. + */ +@Stable +class WebViewState(webContent: WebContent) { + var lastLoadedUrl: String? by mutableStateOf(null) + internal set + + /** + * The content being loaded by the WebView + */ + var content: WebContent by mutableStateOf(webContent) + + /** + * Whether the WebView is currently [LoadingState.Loading] data in its main frame (along with + * progress) or the data loading has [LoadingState.Finished]. See [LoadingState] + */ + var loadingState: LoadingState by mutableStateOf(LoadingState.Initializing) + internal set + + /** + * Whether the webview is currently loading data in its main frame + */ + val isLoading: Boolean + get() = loadingState !is Finished + + /** + * The title received from the loaded content of the current page + */ + var pageTitle: String? by mutableStateOf(null) + internal set + + /** + * the favicon received from the loaded content of the current page + */ + var pageIcon: Bitmap? by mutableStateOf(null) + internal set + + /** + * A list for errors captured in the last load. Reset when a new page is loaded. + * Errors could be from any resource (iframe, image, etc.), not just for the main page. + * For more fine grained control use the OnError callback of the WebView. + */ + val errorsForCurrentRequest: SnapshotStateList = mutableStateListOf() + + /** + * The saved view state from when the view was destroyed last. To restore state, + * use the navigator and only call loadUrl if the bundle is null. + * See WebViewSaveStateSample. + */ + var viewState: Bundle? = null + internal set + + // We need access to this in the state saver. An internal DisposableEffect or AndroidView + // onDestroy is called after the state saver and so can't be used. + internal var webView by mutableStateOf(null) +} + +/** + * Allows control over the navigation of a WebView from outside the composable. E.g. for performing + * a back navigation in response to the user clicking the "up" button in a TopAppBar. + * + * @see [rememberWebViewNavigator] + */ +@Stable +class WebViewNavigator(private val coroutineScope: CoroutineScope) { + private sealed interface NavigationEvent { + data object Back : NavigationEvent + data object Forward : NavigationEvent + data object Reload : NavigationEvent + data object StopLoading : NavigationEvent + + data class LoadUrl( + val url: String, + val additionalHttpHeaders: Map = emptyMap(), + ) : NavigationEvent + + data class LoadHtml( + val html: String, + val baseUrl: String? = null, + val mimeType: String? = null, + val encoding: String? = "utf-8", + val historyUrl: String? = null, + ) : NavigationEvent + + data class PostUrl( + val url: String, + val postData: ByteArray, + ) : NavigationEvent { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as PostUrl + + if (url != other.url) return false + if (!postData.contentEquals(other.postData)) return false + + return true + } + + override fun hashCode(): Int { + var result = url.hashCode() + result = 31 * result + postData.contentHashCode() + return result + } + } + } + + private val navigationEvents: MutableSharedFlow = MutableSharedFlow(replay = 1) + + // Use Dispatchers.Main to ensure that the webview methods are called on UI thread + internal suspend fun WebView.handleNavigationEvents(): Nothing = withContext(Dispatchers.Main) { + navigationEvents.collect { event -> + when (event) { + is NavigationEvent.Back -> goBack() + is NavigationEvent.Forward -> goForward() + is NavigationEvent.Reload -> reload() + is NavigationEvent.StopLoading -> stopLoading() + is NavigationEvent.LoadHtml -> loadDataWithBaseURL( + event.baseUrl, + event.html, + event.mimeType, + event.encoding, + event.historyUrl, + ) + + is NavigationEvent.LoadUrl -> { + loadUrl(event.url, event.additionalHttpHeaders) + } + + is NavigationEvent.PostUrl -> { + postUrl(event.url, event.postData) + } + } + } + } + + /** + * True when the web view is able to navigate backwards, false otherwise. + */ + var canGoBack: Boolean by mutableStateOf(false) + internal set + + /** + * True when the web view is able to navigate forwards, false otherwise. + */ + var canGoForward: Boolean by mutableStateOf(false) + internal set + + fun loadUrl(url: String, additionalHttpHeaders: Map = emptyMap()) { + coroutineScope.launch { + navigationEvents.emit( + NavigationEvent.LoadUrl( + url, + additionalHttpHeaders, + ), + ) + } + } + + fun loadHtml( + html: String, + baseUrl: String? = null, + mimeType: String? = null, + encoding: String? = "utf-8", + historyUrl: String? = null, + ) { + coroutineScope.launch { + navigationEvents.emit( + NavigationEvent.LoadHtml( + html, + baseUrl, + mimeType, + encoding, + historyUrl, + ), + ) + } + } + + fun postUrl( + url: String, + postData: ByteArray, + ) { + coroutineScope.launch { + navigationEvents.emit( + NavigationEvent.PostUrl( + url, + postData, + ), + ) + } + } + + /** + * Navigates the webview back to the previous page. + */ + fun navigateBack() { + coroutineScope.launch { navigationEvents.emit(NavigationEvent.Back) } + } + + /** + * Navigates the webview forward after going back from a page. + */ + fun navigateForward() { + coroutineScope.launch { navigationEvents.emit(NavigationEvent.Forward) } + } + + /** + * Reloads the current page in the webview. + */ + fun reload() { + coroutineScope.launch { navigationEvents.emit(NavigationEvent.Reload) } + } + + /** + * Stops the current page load (if one is loading). + */ + fun stopLoading() { + coroutineScope.launch { navigationEvents.emit(NavigationEvent.StopLoading) } + } +} + +/** + * Creates and remembers a [WebViewNavigator] using the default [CoroutineScope] or a provided + * override. + */ +@Composable +fun rememberWebViewNavigator( + coroutineScope: CoroutineScope = rememberCoroutineScope(), +): WebViewNavigator = remember(coroutineScope) { WebViewNavigator(coroutineScope) } + +/** + * A wrapper class to hold errors from the WebView. + */ +@Immutable +data class WebViewError( + /** + * The request the error came from. + */ + val request: WebResourceRequest?, + /** + * The error that was reported. + */ + val error: WebResourceError, +) + +/** + * Creates a WebView state that is remembered across Compositions. + * + * @param url The url to load in the WebView + * @param additionalHttpHeaders Optional, additional HTTP headers that are passed to [WebView.loadUrl]. + * Note that these headers are used for all subsequent requests of the WebView. + */ +@Composable +fun rememberWebViewState( + url: String, + additionalHttpHeaders: Map = emptyMap(), +): WebViewState = +// Rather than using .apply {} here we will recreate the state, this prevents + // a recomposition loop when the webview updates the url itself. + remember { + WebViewState( + WebContent.Url( + url = url, + additionalHttpHeaders = additionalHttpHeaders, + ), + ) + }.apply { + this.content = WebContent.Url( + url = url, + additionalHttpHeaders = additionalHttpHeaders, + ) + } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/Settings.kt b/app/src/main/kotlin/com/hippo/ehviewer/Settings.kt index 73b899242..83636f140 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/Settings.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/Settings.kt @@ -110,6 +110,7 @@ object Settings : DataStorePreferences(null) { val languageFilter = intPref("language_filter", -1) val meteredNetworkWarning = boolPref("cellular_network_warning", false) val predictiveNavAnim = boolPref("predictive_nav_anim", true) + val blackDarkTheme = boolPref("black_dark_theme", false) val needSignInFlow: Flow var downloadScheme by stringOrNullPref("image_scheme", null) @@ -146,7 +147,6 @@ object Settings : DataStorePreferences(null) { var security by boolPref("require_unlock", false) var removeImageFiles by boolPref("include_pic", true) var needSignIn by boolPref("need_sign_in", true).also { needSignInFlow = it.valueFlow() } - var blackDarkTheme by boolPref("black_dark_theme", false).observed { updateWhenAmoledModeChanges() } var harmonizeCategoryColor by boolPref("harmonize_category_color", true) var preloadThumbAggressively by boolPref("preload_thumb_aggressively", false) var downloadOriginImage by boolPref("download_origin_image", false) diff --git a/app/src/main/kotlin/com/hippo/ehviewer/SettingsCollector.kt b/app/src/main/kotlin/com/hippo/ehviewer/SettingsCollector.kt index 70d7a702d..2499423c3 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/SettingsCollector.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/SettingsCollector.kt @@ -2,7 +2,6 @@ package com.hippo.ehviewer import android.app.UiModeManager import androidx.appcompat.app.AppCompatDelegate -import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY import androidx.core.os.LocaleListCompat import com.hippo.ehviewer.client.EhCookieStore import com.hippo.ehviewer.client.EhEngine @@ -59,14 +58,6 @@ fun updateWhenThemeChanges() { } } -fun updateWhenAmoledModeChanges() { - collectScope.launch { - delay(100) // Avoid recompose being cancelled - AppCompatDelegate.setDefaultNightMode(MODE_NIGHT_AUTO_BATTERY) // Trigger onNightModeChanged - AppCompatDelegate.setDefaultNightMode(Settings.theme) - } -} - fun updateWhenRequestNewsChanges() { updateDailyCheckWork(appCtx) } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/EhActivity.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/EhActivity.kt index becae1ff4..99de0328a 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/EhActivity.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/EhActivity.kt @@ -15,32 +15,14 @@ */ package com.hippo.ehviewer.ui -import android.content.res.Resources.Theme import android.os.Bundle import androidx.activity.enableEdgeToEdge -import androidx.annotation.StyleRes import androidx.appcompat.app.AppCompatActivity import androidx.navigation.ActivityNavigator -import com.hippo.ehviewer.R import com.hippo.ehviewer.Settings -import eu.kanade.tachiyomi.util.system.isNightMode import eu.kanade.tachiyomi.util.view.setSecureScreen abstract class EhActivity : AppCompatActivity() { - @StyleRes - fun getThemeStyleRes(): Int { - return if (Settings.blackDarkTheme && isNightMode()) R.style.ThemeOverlay_Black else R.style.ThemeOverlay - } - - override fun onApplyThemeResource(theme: Theme, resid: Int, first: Boolean) { - super.onApplyThemeResource(theme, resid, first) - theme.applyStyle(getThemeStyleRes(), true) - } - - override fun onNightModeChanged(mode: Int) { - theme.applyStyle(getThemeStyleRes(), true) - } - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/Theme.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/Theme.kt index 7f3f90f84..d999fe2a7 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/Theme.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/Theme.kt @@ -5,34 +5,40 @@ import androidx.activity.compose.setContent import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.remember import androidx.compose.ui.platform.ComposeView -import com.google.accompanist.themeadapter.material3.Mdc3Theme +import com.hippo.ehviewer.ui.theme.EhTheme import com.hippo.ehviewer.ui.tools.DialogState import com.hippo.ehviewer.ui.tools.LocalDialogState import com.hippo.ehviewer.ui.tools.ProvideVectorPainterCache import com.ramcosta.composedestinations.animations.defaults.RootNavGraphDefaultAnimations -// To make sure compose theme and legacy view theme are in sync, we only use Mdc3Theme - inline fun ComposeView.setMD3Content(crossinline content: @Composable () -> Unit) = setContent { - Mdc3Theme { + EhTheme { val dialogState = remember { DialogState() } dialogState.Intercept() - CompositionLocalProvider(LocalDialogState provides dialogState) { + CompositionLocalProvider( + LocalDialogState provides dialogState, + LocalContentColor provides MaterialTheme.colorScheme.onBackground, + ) { content() } } } inline fun ComponentActivity.setMD3Content(crossinline content: @Composable () -> Unit) = setContent { - Mdc3Theme { + EhTheme { ProvideVectorPainterCache { val dialogState = remember { DialogState() } dialogState.Intercept() - CompositionLocalProvider(LocalDialogState provides dialogState) { + CompositionLocalProvider( + LocalDialogState provides dialogState, + LocalContentColor provides MaterialTheme.colorScheme.onBackground, + ) { content() } } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/EhScreen.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/EhScreen.kt index 694a03542..458a3cf59 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/EhScreen.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/settings/EhScreen.kt @@ -218,7 +218,7 @@ fun EhScreen(navigator: DestinationsNavigator) { ) SwitchPreference( title = stringResource(id = R.string.black_dark_theme), - value = Settings::blackDarkTheme, + value = Settings.blackDarkTheme::value, ) SwitchPreference( title = stringResource(id = R.string.harmonize_category_color), diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/theme/Theme.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/theme/Theme.kt new file mode 100644 index 000000000..3e8ad77b0 --- /dev/null +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/theme/Theme.kt @@ -0,0 +1,51 @@ +package com.hippo.ehviewer.ui.theme + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import com.hippo.ehviewer.Settings +import com.hippo.ehviewer.collectAsState +import com.hippo.ehviewer.util.isAtLeastS + +@Composable +fun EhTheme(content: @Composable () -> Unit) { + val useDarkTheme = isSystemInDarkTheme() + val amoled by Settings.blackDarkTheme.collectAsState() + val context = LocalContext.current + val colors = if (useDarkTheme) { + if (isAtLeastS) { + dynamicDarkColorScheme(context) + } else { + darkColorScheme() + }.let { + if (amoled) { + it.copy( + surface = Color.Black, + onSurface = Color.White, + background = Color.Black, + onBackground = Color.White, + ) + } else { + it + } + } + } else { + if (isAtLeastS) { + dynamicLightColorScheme(context) + } else { + lightColorScheme() + } + } + + MaterialTheme( + colorScheme = colors, + content = content, + ) +} diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt index 32258dc13..5e961b451 100644 --- a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt +++ b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt @@ -43,7 +43,6 @@ import androidx.activity.result.contract.ActivityResultContracts.CreateDocument import androidx.activity.viewModels import androidx.annotation.ChecksSdkIntAtLeast import androidx.annotation.RequiresApi -import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheetFix @@ -666,7 +665,6 @@ class ReaderActivity : EhActivity() { binding.pageNumber.setMD3Content { CompositionLocalProvider( LocalTextStyle provides MaterialTheme.typography.bodySmall, - LocalContentColor provides MaterialTheme.colorScheme.onBackground, ) { PageIndicatorText( currentPage = currentPage, diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/ReaderProgressIndicator.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/ReaderProgressIndicator.kt index 142e734aa..0fb714cec 100644 --- a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/ReaderProgressIndicator.kt +++ b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/viewer/ReaderProgressIndicator.kt @@ -14,7 +14,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.platform.AbstractComposeView import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.core.view.isVisible -import com.google.accompanist.themeadapter.material3.Mdc3Theme +import com.hippo.ehviewer.ui.theme.EhTheme class ReaderProgressIndicator @JvmOverloads constructor( context: Context, @@ -32,7 +32,7 @@ class ReaderProgressIndicator @JvmOverloads constructor( @Composable override fun Content() { - Mdc3Theme { + EhTheme { if (visible) CombinedCircularProgressIndicator(progress = progress) } } diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/util/system/ContextExtensions.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/util/system/ContextExtensions.kt index 7e15dcdd1..24181fb3f 100644 --- a/app/src/main/kotlin/eu/kanade/tachiyomi/util/system/ContextExtensions.kt +++ b/app/src/main/kotlin/eu/kanade/tachiyomi/util/system/ContextExtensions.kt @@ -17,7 +17,6 @@ import androidx.core.graphics.blue import androidx.core.graphics.green import androidx.core.graphics.red import com.hippo.ehviewer.R -import com.hippo.ehviewer.Settings as AppSettings import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences import kotlin.math.roundToInt import splitties.systemservices.windowManager @@ -122,8 +121,6 @@ fun Context.createReaderThemeContext(): Context { val wrappedContext = ContextThemeWrapper(this, R.style.AppTheme) wrappedContext.applyOverrideConfiguration(overrideConf) - val resId = if (AppSettings.blackDarkTheme && isDarkBackground) R.style.ThemeOverlay_Black else R.style.ThemeOverlay - wrappedContext.theme.applyStyle(resId, true) return wrappedContext } return this diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 917ba6ac9..9f4a5c063 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -25,14 +25,6 @@ @style/MaterialAlertDialogTheme - -