Drop deprecated Accompanist libraries (#500)
* Migrate XML themes to Compose * Vendor Accompanist WebView
This commit is contained in:
1 parent
b02fb2d1d7
commit
b51120e914
13 files changed
+721
-58
No files matched your search
@@ -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)
|
||||
|
||||
@@ -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<String, String> = 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<WebViewError> = 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<WebView?>(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<String, String> = 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<NavigationEvent> = 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<String, String> = 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<String, String> = 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,
|
||||
)
|
||||
}
|
||||
@@ -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<Boolean>
|
||||
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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -25,14 +25,6 @@
|
||||
<item name="materialAlertDialogTheme">@style/MaterialAlertDialogTheme</item>
|
||||
</style>
|
||||
|
||||
<style name="ThemeOverlay" />
|
||||
|
||||
<style name="ThemeOverlay.Black">
|
||||
<item name="colorSurface">@android:color/black</item>
|
||||
<item name="colorPrimaryDark">@android:color/black</item>
|
||||
<item name="android:colorBackground">@android:color/black</item>
|
||||
</style>
|
||||
|
||||
<style name="RatingBarTheme" parent="ThemeOverlay.Material3">
|
||||
<item name="colorControlNormal">@color/yellow_800</item>
|
||||
<item name="colorControlActivated">@color/yellow_800</item>
|
||||
|
||||
@@ -20,7 +20,6 @@ coil = "3.0.0-SNAPSHOT"
|
||||
compose-bom = "2023.12.00-alpha03"
|
||||
compose-compiler = "1.5.5-dev-k2.0.0-Beta1-06b8ae672a4"
|
||||
compose-destinations = "1.9.55"
|
||||
google-accompanist = "0.32.0"
|
||||
google-android-material = "1.12.0-alpha02"
|
||||
kotlin = "2.0.0-Beta1"
|
||||
kotlinx-coroutines = "1.7.3"
|
||||
@@ -35,9 +34,6 @@ splitties = "3.0.0"
|
||||
[libraries]
|
||||
aboutlibraries-core = { module = "com.mikepenz:aboutlibraries-core", version.ref = "aboutlibraries" }
|
||||
|
||||
accompanist-themeadapter-material3 = { module = "com.google.accompanist:accompanist-themeadapter-material3", version.ref = "google-accompanist" }
|
||||
accompanist-webview = { module = "com.google.accompanist:accompanist-webview", version.ref = "google-accompanist" }
|
||||
|
||||
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" }
|
||||
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" }
|
||||
androidx-biometric = { module = "androidx.biometric:biometric-ktx", version.ref = "androidx-biometric" }
|
||||
@@ -117,7 +113,6 @@ splitties-preferences = { module = "com.louiscad.splitties:splitties-preferences
|
||||
splitties-systemservices = { module = "com.louiscad.splitties:splitties-systemservices", version.ref = "splitties" }
|
||||
|
||||
[bundles]
|
||||
accompanist = ["accompanist-themeadapter-material3", "accompanist-webview"]
|
||||
androidx-navigation = ["androidx-navigation-compose", "androidx-navigation-ui"]
|
||||
coil = ["coil-compose", "coil-gif"]
|
||||
compose = ["compose-foundation", "compose-material-icons-extended", "compose-material3", "compose-material3-windowsizeclass", "compose-ui-util"]
|
||||
|
||||
Reference in new issue
Block a user