diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 4d4c5fa09..afae69dd0 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -2,7 +2,7 @@ import java.util.Properties import java.io.FileInputStream plugins { -alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.android) alias(libs.plugins.android.application) alias(libs.plugins.ksp) alias(libs.plugins.aboutlibraries) @@ -182,4 +182,4 @@ dependencies { testImplementation(libs.mockito.core) testImplementation(libs.mockito.junit.jupiter) testImplementation(libs.mockito.kotlin) -} \ No newline at end of file +} diff --git a/app/src/main/java/me/ash/reader/infrastructure/preference/Preference.kt b/app/src/main/java/me/ash/reader/infrastructure/preference/Preference.kt index b001b01a0..801c7a09e 100644 --- a/app/src/main/java/me/ash/reader/infrastructure/preference/Preference.kt +++ b/app/src/main/java/me/ash/reader/infrastructure/preference/Preference.kt @@ -53,6 +53,7 @@ fun Preferences.toSettings(): Settings { flowArticleListTonalElevation = FlowArticleListTonalElevationPreference.fromPreferences(this), // Reading page + readingRenderer = ReadingRendererPreference.fromPreferences(this), readingTheme = ReadingThemePreference.fromPreferences(this), readingDarkTheme = ReadingDarkThemePreference.fromPreferences(this), readingPageTonalElevation = ReadingPageTonalElevationPreference.fromPreferences(this), diff --git a/app/src/main/java/me/ash/reader/infrastructure/preference/ReadingRendererPreference.kt b/app/src/main/java/me/ash/reader/infrastructure/preference/ReadingRendererPreference.kt new file mode 100644 index 000000000..2c0ac6a71 --- /dev/null +++ b/app/src/main/java/me/ash/reader/infrastructure/preference/ReadingRendererPreference.kt @@ -0,0 +1,46 @@ +package me.ash.reader.infrastructure.preference + +import android.content.Context +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.compositionLocalOf +import androidx.datastore.preferences.core.Preferences +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import me.ash.reader.R +import me.ash.reader.ui.ext.DataStoreKey +import me.ash.reader.ui.ext.DataStoreKey.Companion.readingRenderer +import me.ash.reader.ui.ext.dataStore +import me.ash.reader.ui.ext.put + +val LocalReadingRenderer = + compositionLocalOf { ReadingRendererPreference.default } +@Immutable +sealed class ReadingRendererPreference(val value: Int) : Preference() { + object WebView : ReadingRendererPreference(0) + object NativeComponent : ReadingRendererPreference(1) + + override fun put(context: Context, scope: CoroutineScope) { + scope.launch { + context.dataStore.put(DataStoreKey.readingRenderer, value) + } + } + + fun toDesc(context: Context): String = + when (this) { + WebView -> context.getString(R.string.web_view) + NativeComponent -> context.getString(R.string.native_component) + } + + companion object { + + val default = WebView + val values = listOf(WebView, NativeComponent) + + fun fromPreferences(preferences: Preferences): ReadingRendererPreference = + when (preferences[DataStoreKey.keys[readingRenderer]?.key as Preferences.Key]) { + 0 -> WebView + 1 -> NativeComponent + else -> default + } + } +} diff --git a/app/src/main/java/me/ash/reader/infrastructure/preference/ReadingSubheadAlignPreference.kt b/app/src/main/java/me/ash/reader/infrastructure/preference/ReadingSubheadAlignPreference.kt index 80de07d22..32eafd084 100644 --- a/app/src/main/java/me/ash/reader/infrastructure/preference/ReadingSubheadAlignPreference.kt +++ b/app/src/main/java/me/ash/reader/infrastructure/preference/ReadingSubheadAlignPreference.kt @@ -46,6 +46,14 @@ sealed class ReadingSubheadAlignPreference(val value: Int) : Preference() { Justify -> TextAlign.Justify } + fun toTextAlignCSS(): String = + when (this) { + Start -> "left" + End -> "right" + Center -> "center" + Justify -> "justify" + } + companion object { val default = Start diff --git a/app/src/main/java/me/ash/reader/infrastructure/preference/ReadingTextAlignPreference.kt b/app/src/main/java/me/ash/reader/infrastructure/preference/ReadingTextAlignPreference.kt index 467146312..25631fe82 100644 --- a/app/src/main/java/me/ash/reader/infrastructure/preference/ReadingTextAlignPreference.kt +++ b/app/src/main/java/me/ash/reader/infrastructure/preference/ReadingTextAlignPreference.kt @@ -55,6 +55,14 @@ sealed class ReadingTextAlignPreference(val value: Int) : Preference() { Justify -> Alignment.Start } + fun toTextAlignCSS(): String = + when (this) { + Start -> "left" + End -> "right" + Center -> "center" + Justify -> "justify" + } + companion object { val default = Start diff --git a/app/src/main/java/me/ash/reader/infrastructure/preference/Settings.kt b/app/src/main/java/me/ash/reader/infrastructure/preference/Settings.kt index c7d8b8459..56de1ac3e 100644 --- a/app/src/main/java/me/ash/reader/infrastructure/preference/Settings.kt +++ b/app/src/main/java/me/ash/reader/infrastructure/preference/Settings.kt @@ -51,6 +51,7 @@ data class Settings( val flowArticleListReadIndicator: FlowArticleReadIndicatorPreference = FlowArticleReadIndicatorPreference.default, // Reading page + val readingRenderer: ReadingRendererPreference = ReadingRendererPreference.default, val readingTheme: ReadingThemePreference = ReadingThemePreference.default, val readingDarkTheme: ReadingDarkThemePreference = ReadingDarkThemePreference.default, val readingPageTonalElevation: ReadingPageTonalElevationPreference = ReadingPageTonalElevationPreference.default, @@ -140,6 +141,7 @@ fun SettingsProvider( LocalFlowArticleListReadIndicator provides settings.flowArticleListReadIndicator, // Reading page + LocalReadingRenderer provides settings.readingRenderer, LocalReadingTheme provides settings.readingTheme, LocalReadingDarkTheme provides settings.readingDarkTheme, LocalReadingPageTonalElevation provides settings.readingPageTonalElevation, diff --git a/app/src/main/java/me/ash/reader/ui/component/base/RYWebView.kt b/app/src/main/java/me/ash/reader/ui/component/base/RYWebView.kt new file mode 100644 index 000000000..c5b7de485 --- /dev/null +++ b/app/src/main/java/me/ash/reader/ui/component/base/RYWebView.kt @@ -0,0 +1,377 @@ +package me.ash.reader.ui.component.base + +import android.content.Context +import android.net.http.SslError +import android.util.Log +import android.webkit.SslErrorHandler +import android.webkit.WebResourceError +import android.webkit.WebResourceRequest +import android.webkit.WebResourceResponse +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import me.ash.reader.infrastructure.preference.LocalOpenLink +import me.ash.reader.infrastructure.preference.LocalOpenLinkSpecificBrowser +import me.ash.reader.infrastructure.preference.LocalReadingImageHorizontalPadding +import me.ash.reader.infrastructure.preference.LocalReadingImageRoundedCorners +import me.ash.reader.infrastructure.preference.LocalReadingTextLetterSpacing +import me.ash.reader.infrastructure.preference.LocalReadingPageTonalElevation +import me.ash.reader.infrastructure.preference.LocalReadingSubheadAlign +import me.ash.reader.infrastructure.preference.LocalReadingSubheadBold +import me.ash.reader.infrastructure.preference.LocalReadingTextAlign +import me.ash.reader.infrastructure.preference.LocalReadingTextBold +import me.ash.reader.infrastructure.preference.LocalReadingTextFontSize +import me.ash.reader.infrastructure.preference.LocalReadingTextHorizontalPadding +import me.ash.reader.ui.ext.openURL +import me.ash.reader.ui.ext.surfaceColorAtElevation +import kotlin.math.absoluteValue + +const val INJECTION_TOKEN = "/android_asset_font/" + +@Composable +fun RYWebView( + modifier: Modifier = Modifier, + content: String, + onReceivedError: (error: WebResourceError?) -> Unit = {}, +) { + val context = LocalContext.current + val maxWidth = LocalConfiguration.current.screenWidthDp.dp.value + val openLink = LocalOpenLink.current + val openLinkSpecificBrowser = LocalOpenLinkSpecificBrowser.current + val tonalElevation = LocalReadingPageTonalElevation.current + val backgroundColor = MaterialTheme.colorScheme + .surfaceColorAtElevation(tonalElevation.value.dp).toArgb() + val bodyColor: Int = MaterialTheme.colorScheme.onSurfaceVariant.toArgb() + val linkColor: Int = MaterialTheme.colorScheme.primary.toArgb() + val subheadColor: Int = MaterialTheme.colorScheme.onSurface.toArgb() + val subheadBold: Boolean = LocalReadingSubheadBold.current.value + val subheadAlign: String = LocalReadingSubheadAlign.current.toTextAlignCSS() + val textBold: Boolean = LocalReadingTextBold.current.value + val textAlign: String = LocalReadingTextAlign.current.toTextAlignCSS() + val textFontSize: Int = LocalReadingTextFontSize.current + val textLetterSpacing: Float = LocalReadingTextLetterSpacing.current + val imageHorizontalPadding: Int = LocalReadingImageHorizontalPadding.current + val textHorizontalPadding: Int = LocalReadingTextHorizontalPadding.current + val imageShape: Int = LocalReadingImageRoundedCorners.current + val codeColor: Int = MaterialTheme.colorScheme.primary.toArgb() + val codeBackgroundColor: Int = MaterialTheme.colorScheme + .surfaceColorAtElevation((tonalElevation.value + 6).dp).toArgb() + val webViewClient by remember { + mutableStateOf(object : WebViewClient() { + + override fun shouldInterceptRequest( + view: WebView?, + url: String?, + ): WebResourceResponse? { + if (url != null && url.contains(INJECTION_TOKEN)) { + try { + val assetPath = url.substring( + url.indexOf(INJECTION_TOKEN) + INJECTION_TOKEN.length, + url.length + ) + return WebResourceResponse( + "text/HTML", + "UTF-8", + context.assets.open(assetPath) + ) + } catch (e: Exception) { + Log.e("RLog", "WebView shouldInterceptRequest: $e") + } + } + return super.shouldInterceptRequest(view, url); + } + + override fun onPageFinished(view: WebView?, url: String?) { + super.onPageFinished(view, url) + val jsCode = "javascript:(function(){" + + "var imgs=document.getElementsByTagName(\"img\");" + + "for(var i=0;i +""" \ No newline at end of file diff --git a/app/src/main/java/me/ash/reader/ui/component/base/WebView.kt b/app/src/main/java/me/ash/reader/ui/component/base/WebView.kt deleted file mode 100644 index d5da58dbf..000000000 --- a/app/src/main/java/me/ash/reader/ui/component/base/WebView.kt +++ /dev/null @@ -1,215 +0,0 @@ -package me.ash.reader.ui.component.base - -import android.net.http.SslError -import android.util.Log -import android.webkit.* -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.viewinterop.AndroidView -import me.ash.reader.infrastructure.preference.LocalOpenLink -import me.ash.reader.infrastructure.preference.LocalOpenLinkSpecificBrowser -import me.ash.reader.ui.ext.openURL - -const val INJECTION_TOKEN = "/android_asset_font/" - -@Composable -fun WebView( - modifier: Modifier = Modifier, - content: String, - onReceivedError: (error: WebResourceError?) -> Unit = {}, -) { - val context = LocalContext.current - val openLink = LocalOpenLink.current - val openLinkSpecificBrowser = LocalOpenLinkSpecificBrowser.current - val color = MaterialTheme.colorScheme.onSurfaceVariant.toArgb() - val backgroundColor = MaterialTheme.colorScheme.surface.toArgb() - val webViewClient by remember { - mutableStateOf(object : WebViewClient() { - - override fun shouldInterceptRequest( - view: WebView?, - url: String?, - ): WebResourceResponse? { - if (url != null && url.contains(INJECTION_TOKEN)) { - try { - val assetPath = url.substring( - url.indexOf(INJECTION_TOKEN) + INJECTION_TOKEN.length, - url.length - ) - return WebResourceResponse( - "text/HTML", - "UTF-8", - context.assets.open(assetPath) - ) - } catch (e: Exception) { - Log.e("RLog", "WebView shouldInterceptRequest: $e") - } - } - return super.shouldInterceptRequest(view, url); - } - - override fun onPageFinished(view: WebView?, url: String?) { - super.onPageFinished(view, url) - val jsCode = "javascript:(function(){" + - "var imgs=document.getElementsByTagName(\"img\");" + - "for(var i=0;i -""" diff --git a/app/src/main/java/me/ash/reader/ui/ext/DataStoreExt.kt b/app/src/main/java/me/ash/reader/ui/ext/DataStoreExt.kt index 4205d288a..e3eb9e194 100644 --- a/app/src/main/java/me/ash/reader/ui/ext/DataStoreExt.kt +++ b/app/src/main/java/me/ash/reader/ui/ext/DataStoreExt.kt @@ -134,6 +134,7 @@ data class DataStoreKey( const val flowArticleListReadIndicator = "flowArticleListReadIndicator" // Reading page + const val readingRenderer = "readingRenderer" const val readingDarkTheme = "readingDarkTheme" const val readingPageTonalElevation = "readingPageTonalElevation" const val readingTextFontSize = "readingTextFontSize" @@ -207,6 +208,7 @@ data class DataStoreKey( flowArticleListTonalElevation to DataStoreKey(intPreferencesKey(flowArticleListTonalElevation), Int::class.java), flowArticleListReadIndicator to DataStoreKey(booleanPreferencesKey(flowArticleListReadIndicator), Boolean::class.java), // Reading page + readingRenderer to DataStoreKey(intPreferencesKey(readingRenderer), Int::class.java), readingDarkTheme to DataStoreKey(intPreferencesKey(readingDarkTheme), Int::class.java), readingPageTonalElevation to DataStoreKey(intPreferencesKey(readingPageTonalElevation), Int::class.java), readingTextFontSize to DataStoreKey(intPreferencesKey(readingTextFontSize), Int::class.java), diff --git a/app/src/main/java/me/ash/reader/ui/page/home/reading/Content.kt b/app/src/main/java/me/ash/reader/ui/page/home/reading/Content.kt index ee10dd0e5..6e27ee18b 100644 --- a/app/src/main/java/me/ash/reader/ui/page/home/reading/Content.kt +++ b/app/src/main/java/me/ash/reader/ui/page/home/reading/Content.kt @@ -1,44 +1,26 @@ package me.ash.reader.ui.page.home.reading -import android.util.Log -import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.tween -import androidx.compose.animation.expandVertically -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.scaleIn -import androidx.compose.animation.shrinkVertically -import androidx.compose.animation.togetherWith -import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.text.selection.DisableSelection import androidx.compose.foundation.text.selection.SelectionContainer -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.KeyboardArrowDown -import androidx.compose.material.icons.outlined.KeyboardArrowUp import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp -import androidx.compose.ui.zIndex +import java.util.Date import me.ash.reader.infrastructure.preference.LocalOpenLink import me.ash.reader.infrastructure.preference.LocalOpenLinkSpecificBrowser +import me.ash.reader.infrastructure.preference.LocalReadingRenderer import me.ash.reader.infrastructure.preference.LocalReadingSubheadUpperCase +import me.ash.reader.infrastructure.preference.ReadingRendererPreference +import me.ash.reader.ui.component.base.RYWebView import me.ash.reader.ui.component.reader.Reader import me.ash.reader.ui.ext.drawVerticalScrollbar import me.ash.reader.ui.ext.openURL -import me.ash.reader.ui.ext.pagerAnimate -import java.util.* -import kotlin.math.abs @Composable fun Content( @@ -54,6 +36,7 @@ fun Content( onImageClick: ((imgUrl: String, altText: String) -> Unit)? = null, ) { val context = LocalContext.current + val renderer = LocalReadingRenderer.current val subheadUpperCase = LocalReadingSubheadUpperCase.current val openLink = LocalOpenLink.current val openLinkSpecificBrowser = LocalOpenLinkSpecificBrowser.current @@ -95,16 +78,26 @@ fun Content( } } } - Reader( - context = context, - subheadUpperCase = subheadUpperCase.value, - link = link ?: "", - content = content, - onImageClick = onImageClick, - onLinkClick = { - context.openURL(it, openLink, openLinkSpecificBrowser) - } - ) + when (renderer) { + ReadingRendererPreference.WebView -> + item { + DisableSelection { + RYWebView(content = content) + } + } + + ReadingRendererPreference.NativeComponent -> + Reader( + context = context, + subheadUpperCase = subheadUpperCase.value, + link = link ?: "", + content = content, + onImageClick = onImageClick, + onLinkClick = { + context.openURL(it, openLink, openLinkSpecificBrowser) + } + ) + } item { Spacer(modifier = Modifier.height(128.dp)) diff --git a/app/src/main/java/me/ash/reader/ui/page/settings/color/reading/ReadingStylePage.kt b/app/src/main/java/me/ash/reader/ui/page/settings/color/reading/ReadingStylePage.kt index cf687784b..3126d959c 100644 --- a/app/src/main/java/me/ash/reader/ui/page/settings/color/reading/ReadingStylePage.kt +++ b/app/src/main/java/me/ash/reader/ui/page/settings/color/reading/ReadingStylePage.kt @@ -23,7 +23,6 @@ import androidx.compose.material.icons.automirrored.rounded.ArrowBack import androidx.compose.material.icons.automirrored.rounded.Segment import androidx.compose.material.icons.outlined.Image import androidx.compose.material.icons.outlined.Movie -import androidx.compose.material.icons.rounded.Segment import androidx.compose.material.icons.rounded.Title import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable @@ -46,9 +45,11 @@ import me.ash.reader.infrastructure.preference.LocalReadingAutoHideToolbar import me.ash.reader.infrastructure.preference.LocalReadingDarkTheme import me.ash.reader.infrastructure.preference.LocalReadingFonts import me.ash.reader.infrastructure.preference.LocalReadingPageTonalElevation +import me.ash.reader.infrastructure.preference.LocalReadingRenderer import me.ash.reader.infrastructure.preference.LocalReadingTheme import me.ash.reader.infrastructure.preference.ReadingFontsPreference import me.ash.reader.infrastructure.preference.ReadingPageTonalElevationPreference +import me.ash.reader.infrastructure.preference.ReadingRendererPreference import me.ash.reader.infrastructure.preference.ReadingThemePreference import me.ash.reader.infrastructure.preference.not import me.ash.reader.ui.component.ReadingThemePrev @@ -73,6 +74,7 @@ fun ReadingStylePage( val context = LocalContext.current val scope = rememberCoroutineScope() + val renderer = LocalReadingRenderer.current val readingTheme = LocalReadingTheme.current val darkTheme = LocalReadingDarkTheme.current val darkThemeNot = !darkTheme @@ -81,7 +83,7 @@ fun ReadingStylePage( val autoHideToolbar = LocalReadingAutoHideToolbar.current val pullToSwitchArticle = LocalPullToSwitchArticle.current - + var rendererDialogVisible by remember { mutableStateOf(false) } var tonalElevationDialogVisible by remember { mutableStateOf(false) } var fontsDialogVisible by remember { mutableStateOf(false) } @@ -152,6 +154,11 @@ fun ReadingStylePage( modifier = Modifier.padding(horizontal = 24.dp), text = stringResource(R.string.general) ) + SettingItem( + title = stringResource(R.string.content_renderer), + desc = renderer.toDesc(context), + onClick = { rendererDialogVisible = true }, + ) {} SettingItem( title = stringResource(R.string.reading_fonts), desc = fonts.toDesc(context), @@ -310,4 +317,19 @@ fun ReadingStylePage( ) { fontsDialogVisible = false } + + RadioDialog( + visible = rendererDialogVisible, + title = stringResource(R.string.content_renderer), + options = ReadingRendererPreference.values.map { + RadioDialogOption( + text = it.toDesc(context), + selected = it == renderer, + ) { + it.put(context, scope) + } + } + ) { + rendererDialogVisible = false + } } diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index dfaea2d55..8723f0a96 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -302,4 +302,6 @@ 从 JSON 文件导入 导出为 JSON 文件 该文件可能不是有效的 JSON 文件!导入该文件可能会损坏应用程序并导致当前偏好设置丢失,您确定要继续吗? + 内容渲染器 + 原生组件(实验性) \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5288ec6ce..f6a8fe5a3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -443,4 +443,7 @@ Import from JSON Export as JSON This file may not be a valid JSON file. Importing it could potentially corrupt the app and result in the loss of current preferences. Are you sure you want to proceed? + Content renderer + WebView + Native Component (experimental) diff --git a/build.gradle.kts b/build.gradle.kts index a7ce84225..24b694162 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -10,4 +10,4 @@ plugins { tasks.register("clean") { delete(rootProject.layout.buildDirectory) -} \ No newline at end of file +} diff --git a/gradle.properties b/gradle.properties index 81acb424c..cced061d3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,4 +5,4 @@ android.buildConfig = true android.useAndroidX=true android.nonTransitiveRClass=true android.nonFinalResIds=true -android.enableR8.fullMode=false \ No newline at end of file +android.enableR8.fullMode=false