mirror of https://github.com/Ashinch/ReadYou.git
refactor(webview): re-enable webview renderer (#568)
* refactor(reading): reenable webview renderer * refactor(reading): reenable webview renderer * refactor(reading): reenable webview renderer * refactor(reading): reenable webview renderer * refactor(reading): reenable webview renderer * refactor(reading): re-enable webview renderer * refactor(webview): support rtl, video, preference styles
This commit is contained in:
parent
2fc28bd4b7
commit
b30ff86503
|
@ -0,0 +1,86 @@
|
|||
package me.ash.reader.infrastructure.html
|
||||
|
||||
|
||||
import net.dankito.readability4j.extended.processor.ArticleGrabberExtended
|
||||
import net.dankito.readability4j.extended.util.RegExUtilExtended
|
||||
import net.dankito.readability4j.model.ArticleGrabberOptions
|
||||
import net.dankito.readability4j.model.ReadabilityOptions
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import org.jsoup.nodes.TextNode
|
||||
|
||||
open class RYArticleGrabberExtended(options: ReadabilityOptions, regExExtended: RegExUtilExtended) : ArticleGrabberExtended(options, regExExtended) {
|
||||
|
||||
override fun prepareNodes(doc: Document, options: ArticleGrabberOptions): List<Element> {
|
||||
val elementsToScore = ArrayList<Element>()
|
||||
var node: Element? = doc
|
||||
|
||||
while(node != null) {
|
||||
val matchString = node.className() + " " + node.id()
|
||||
|
||||
// Check to see if this node is a byline, and remove it if it is.
|
||||
if(checkByline(node, matchString)) {
|
||||
node = removeAndGetNext(node, "byline")
|
||||
continue
|
||||
}
|
||||
|
||||
// Remove unlikely candidates
|
||||
if(options.stripUnlikelyCandidates) {
|
||||
if(regEx.isUnlikelyCandidate(matchString) &&
|
||||
regEx.okMaybeItsACandidate(matchString) == false &&
|
||||
node.tagName() != "body" &&
|
||||
node.tagName() != "a") {
|
||||
node = this.removeAndGetNext(node, "Removing unlikely candidate")
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Remove DIV, SECTION, and HEADER nodes without any content(e.g. text, image, video, or iframe).
|
||||
if((node.tagName() == "div" || node.tagName() == "section" || node.tagName() == "header" ||
|
||||
node.tagName() == "h1" || node.tagName() == "h2" || node.tagName() == "h3" ||
|
||||
node.tagName() == "h4" || node.tagName() == "h5" || node.tagName() == "h6") &&
|
||||
this.isElementWithoutContent(node)) {
|
||||
node = this.removeAndGetNext(node, "node without content")
|
||||
continue
|
||||
}
|
||||
|
||||
if(DEFAULT_TAGS_TO_SCORE.contains(node.tagName())) {
|
||||
elementsToScore.add(node)
|
||||
}
|
||||
|
||||
// Turn all divs that don't have children block level elements into p's
|
||||
if(node.tagName() == "div") {
|
||||
// Sites like http://mobile.slate.com encloses each paragraph with a DIV
|
||||
// element. DIVs with only a P element inside and no text content can be
|
||||
// safely converted into plain P elements to avoid confusing the scoring
|
||||
// algorithm with DIVs with are, in practice, paragraphs.
|
||||
if(this.hasSinglePInsideElement(node)) {
|
||||
val newNode = node.child(0)
|
||||
node.replaceWith(newNode)
|
||||
node = newNode
|
||||
elementsToScore.add(node)
|
||||
}
|
||||
else if(!this.hasChildBlockElement(node)) {
|
||||
setNodeTag(node, "p")
|
||||
elementsToScore.add(node)
|
||||
}
|
||||
else {
|
||||
node.childNodes().forEach { childNode ->
|
||||
if(childNode is TextNode && childNode.text().trim().length > 0) {
|
||||
val p = doc.createElement("p")
|
||||
p.text(childNode.text())
|
||||
// EXPERIMENTAL
|
||||
// p.attr("style", "display: inline;")
|
||||
// p.addClass("readability-styled")
|
||||
childNode.replaceWith(p)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
node = if(node != null) this.getNextNode(node) else null
|
||||
}
|
||||
|
||||
return elementsToScore
|
||||
}
|
||||
}
|
|
@ -2,6 +2,11 @@ package me.ash.reader.infrastructure.html
|
|||
|
||||
import android.util.Log
|
||||
import net.dankito.readability4j.extended.Readability4JExtended
|
||||
import net.dankito.readability4j.extended.processor.PostprocessorExtended
|
||||
import net.dankito.readability4j.extended.util.RegExUtilExtended
|
||||
import net.dankito.readability4j.model.ReadabilityOptions
|
||||
import net.dankito.readability4j.processor.MetadataParser
|
||||
import net.dankito.readability4j.processor.Preprocessor
|
||||
import org.jsoup.nodes.Element
|
||||
|
||||
object Readability {
|
||||
|
@ -9,7 +14,7 @@ object Readability {
|
|||
fun parseToText(htmlContent: String?, uri: String?): String {
|
||||
htmlContent ?: return ""
|
||||
return try {
|
||||
Readability4JExtended(uri ?: "", htmlContent).parse().textContent?.trim() ?: ""
|
||||
Readability4JExtended(uri, htmlContent).parse().textContent?.trim() ?: ""
|
||||
} catch (e: Exception) {
|
||||
Log.e("RLog", "Readability.parseToText '$uri' is error: ", e)
|
||||
""
|
||||
|
@ -19,10 +24,25 @@ object Readability {
|
|||
fun parseToElement(htmlContent: String?, uri: String?): Element? {
|
||||
htmlContent ?: return null
|
||||
return try {
|
||||
Readability4JExtended(uri ?: "", htmlContent).parse().articleContent
|
||||
Readability4JExtended(uri, htmlContent).parse().articleContent
|
||||
} catch (e: Exception) {
|
||||
Log.e("RLog", "Readability.parseToElement '$uri' is error: ", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun Readability4JExtended(uri: String?, html: String): Readability4JExtended {
|
||||
val options = ReadabilityOptions()
|
||||
val regExUtil = RegExUtilExtended()
|
||||
return Readability4JExtended(
|
||||
uri = uri ?: "",
|
||||
html = html,
|
||||
options = options,
|
||||
regExUtil = regExUtil,
|
||||
preprocessor = Preprocessor(regExUtil),
|
||||
metadataParser = MetadataParser(regExUtil),
|
||||
articleGrabber = RYArticleGrabberExtended(options, regExUtil),
|
||||
postprocessor = PostprocessorExtended(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -53,6 +53,8 @@ fun Preferences.toSettings(): Settings {
|
|||
flowArticleListTonalElevation = FlowArticleListTonalElevationPreference.fromPreferences(this),
|
||||
|
||||
// Reading page
|
||||
readingRenderer = ReadingRendererPreference.fromPreferences(this),
|
||||
readingBionicReading = ReadingBionicReadingPreference.fromPreferences(this),
|
||||
readingTheme = ReadingThemePreference.fromPreferences(this),
|
||||
readingDarkTheme = ReadingDarkThemePreference.fromPreferences(this),
|
||||
readingPageTonalElevation = ReadingPageTonalElevationPreference.fromPreferences(this),
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
package me.ash.reader.infrastructure.preference
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import me.ash.reader.ui.ext.DataStoreKey
|
||||
import me.ash.reader.ui.ext.DataStoreKey.Companion.readingBionicReading
|
||||
import me.ash.reader.ui.ext.dataStore
|
||||
import me.ash.reader.ui.ext.put
|
||||
|
||||
val LocalReadingBionicReading =
|
||||
compositionLocalOf<ReadingBionicReadingPreference> { ReadingBionicReadingPreference.default }
|
||||
|
||||
sealed class ReadingBionicReadingPreference(val value: Boolean) : Preference() {
|
||||
object ON : ReadingBionicReadingPreference(true)
|
||||
object OFF : ReadingBionicReadingPreference(false)
|
||||
|
||||
override fun put(context: Context, scope: CoroutineScope) {
|
||||
scope.launch {
|
||||
context.dataStore.put(readingBionicReading, value)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
val default = OFF
|
||||
val values = listOf(ON, OFF)
|
||||
|
||||
fun fromPreferences(preferences: Preferences) =
|
||||
when (preferences[DataStoreKey.keys[readingBionicReading]?.key as Preferences.Key<Boolean>]) {
|
||||
true -> ON
|
||||
false -> OFF
|
||||
else -> default
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
operator fun ReadingBionicReadingPreference.not(): ReadingBionicReadingPreference =
|
||||
when (value) {
|
||||
true -> ReadingBionicReadingPreference.OFF
|
||||
false -> ReadingBionicReadingPreference.ON
|
||||
}
|
|
@ -15,7 +15,7 @@ val LocalReadingImageRoundedCorners =
|
|||
|
||||
object ReadingImageRoundedCornersPreference {
|
||||
|
||||
const val default = 32
|
||||
const val default = 24
|
||||
|
||||
fun put(context: Context, scope: CoroutineScope, value: Int) {
|
||||
scope.launch {
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
package me.ash.reader.infrastructure.preference
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.Stable
|
||||
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> { ReadingRendererPreference.default }
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
fun toDesc(context: Context): String =
|
||||
when (this) {
|
||||
WebView -> context.getString(R.string.webview)
|
||||
NativeComponent -> context.getString(R.string.native_component)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
val default = WebView
|
||||
val values = listOf(WebView, NativeComponent)
|
||||
|
||||
fun fromPreferences(preferences: Preferences) =
|
||||
when (preferences[DataStoreKey.keys[readingRenderer]?.key as Preferences.Key<Int>]) {
|
||||
0 -> WebView
|
||||
1 -> NativeComponent
|
||||
else -> default
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -47,6 +47,14 @@ sealed class ReadingTextAlignPreference(val value: Int) : Preference() {
|
|||
Justify -> TextAlign.Justify
|
||||
}
|
||||
|
||||
fun toTextAlignCSS(): String =
|
||||
when (this) {
|
||||
Start -> "start"
|
||||
End -> "end"
|
||||
Center -> "center"
|
||||
Justify -> "justify"
|
||||
}
|
||||
|
||||
fun toAlignment(): Alignment.Horizontal =
|
||||
when (this) {
|
||||
Start -> Alignment.Start
|
||||
|
|
|
@ -13,8 +13,8 @@ import me.ash.reader.ui.ext.put
|
|||
val LocalReadingTextLineHeight = compositionLocalOf { ReadingTextLineHeightPreference.default }
|
||||
|
||||
data object ReadingTextLineHeightPreference {
|
||||
const val default = 1f
|
||||
private val range = 0.8f..2f
|
||||
const val default = 1.5F
|
||||
private val range = 0.8F..2F
|
||||
|
||||
fun put(context: Context, scope: CoroutineScope, value: Float) {
|
||||
scope.launch {
|
||||
|
|
|
@ -50,6 +50,7 @@ sealed class ReadingThemePreference(val value: Int) : Preference() {
|
|||
ReadingTextHorizontalPaddingPreference.default)
|
||||
ReadingTextAlignPreference.default.put(context, scope)
|
||||
ReadingTextLetterSpacingPreference.put(context, scope, ReadingTextLetterSpacingPreference.default)
|
||||
ReadingTextLineHeightPreference.put(context, scope, ReadingTextLineHeightPreference.default)
|
||||
ReadingTextFontSizePreference.put(context, scope, ReadingTextFontSizePreference.default)
|
||||
ReadingImageRoundedCornersPreference.put(context, scope, ReadingImageRoundedCornersPreference.default)
|
||||
ReadingImageHorizontalPaddingPreference.put(context, scope,
|
||||
|
@ -69,7 +70,8 @@ sealed class ReadingThemePreference(val value: Int) : Preference() {
|
|||
ReadingTextHorizontalPaddingPreference.default)
|
||||
ReadingTextAlignPreference.default.put(context, scope)
|
||||
ReadingTextLetterSpacingPreference.put(context, scope, ReadingTextLetterSpacingPreference.default)
|
||||
ReadingTextFontSizePreference.put(context, scope, 18)
|
||||
ReadingTextLineHeightPreference.put(context, scope, ReadingTextLineHeightPreference.default)
|
||||
ReadingTextFontSizePreference.put(context, scope, 22)
|
||||
ReadingImageRoundedCornersPreference.put(context, scope, 0)
|
||||
ReadingImageHorizontalPaddingPreference.put(context, scope, 0)
|
||||
ReadingImageMaximizePreference.default.put(context, scope)
|
||||
|
@ -87,6 +89,7 @@ sealed class ReadingThemePreference(val value: Int) : Preference() {
|
|||
ReadingTextHorizontalPaddingPreference.default)
|
||||
ReadingTextAlignPreference.Center.put(context, scope)
|
||||
ReadingTextLetterSpacingPreference.put(context, scope, ReadingTextLetterSpacingPreference.default)
|
||||
ReadingTextLineHeightPreference.put(context, scope, ReadingTextLineHeightPreference.default)
|
||||
ReadingTextFontSizePreference.put(context, scope, 20)
|
||||
ReadingImageRoundedCornersPreference.put(context, scope, 0)
|
||||
ReadingImageHorizontalPaddingPreference.put(context, scope,
|
||||
|
@ -106,6 +109,7 @@ sealed class ReadingThemePreference(val value: Int) : Preference() {
|
|||
ReadingTextHorizontalPaddingPreference.default)
|
||||
ReadingTextAlignPreference.default.put(context, scope)
|
||||
ReadingTextLetterSpacingPreference.put(context, scope, ReadingTextLetterSpacingPreference.default)
|
||||
ReadingTextLineHeightPreference.put(context, scope, ReadingTextLineHeightPreference.default)
|
||||
ReadingTextFontSizePreference.put(context, scope, ReadingTextFontSizePreference.default)
|
||||
ReadingImageRoundedCornersPreference.put(context, scope, ReadingImageRoundedCornersPreference.default)
|
||||
ReadingImageHorizontalPaddingPreference.put(context, scope,
|
||||
|
@ -117,7 +121,7 @@ sealed class ReadingThemePreference(val value: Int) : Preference() {
|
|||
|
||||
companion object {
|
||||
|
||||
val default = MaterialYou
|
||||
val default = Reeder
|
||||
val values = listOf(MaterialYou, Reeder, Paper, Custom)
|
||||
|
||||
fun fromPreferences(preferences: Preferences): ReadingThemePreference =
|
||||
|
|
|
@ -51,6 +51,8 @@ data class Settings(
|
|||
val flowArticleListReadIndicator: FlowArticleReadIndicatorPreference = FlowArticleReadIndicatorPreference.default,
|
||||
|
||||
// Reading page
|
||||
val readingRenderer: ReadingRendererPreference = ReadingRendererPreference.default,
|
||||
val readingBionicReading: ReadingBionicReadingPreference = ReadingBionicReadingPreference.default,
|
||||
val readingTheme: ReadingThemePreference = ReadingThemePreference.default,
|
||||
val readingDarkTheme: ReadingDarkThemePreference = ReadingDarkThemePreference.default,
|
||||
val readingPageTonalElevation: ReadingPageTonalElevationPreference = ReadingPageTonalElevationPreference.default,
|
||||
|
@ -140,6 +142,8 @@ fun SettingsProvider(
|
|||
LocalFlowArticleListReadIndicator provides settings.flowArticleListReadIndicator,
|
||||
|
||||
// Reading page
|
||||
LocalReadingRenderer provides settings.readingRenderer,
|
||||
LocalReadingBionicReading provides settings.readingBionicReading,
|
||||
LocalReadingTheme provides settings.readingTheme,
|
||||
LocalReadingDarkTheme provides settings.readingDarkTheme,
|
||||
LocalReadingPageTonalElevation provides settings.readingPageTonalElevation,
|
||||
|
|
|
@ -17,7 +17,8 @@ import androidx.compose.ui.unit.dp
|
|||
fun CanBeDisabledIconButton(
|
||||
modifier: Modifier = Modifier,
|
||||
disabled: Boolean,
|
||||
imageVector: ImageVector,
|
||||
imageVector: ImageVector? = null,
|
||||
icon: @Composable () -> Unit = {},
|
||||
size: Dp = 24.dp,
|
||||
contentDescription: String?,
|
||||
tint: Color = LocalContentColor.current,
|
||||
|
@ -34,11 +35,15 @@ fun CanBeDisabledIconButton(
|
|||
enabled = !disabled,
|
||||
onClick = onClick,
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(size),
|
||||
imageVector = imageVector,
|
||||
contentDescription = contentDescription,
|
||||
tint = if (disabled) MaterialTheme.colorScheme.outline else tint,
|
||||
)
|
||||
if (imageVector != null) {
|
||||
Icon(
|
||||
modifier = Modifier.size(size),
|
||||
imageVector = imageVector,
|
||||
contentDescription = contentDescription,
|
||||
tint = if (disabled) MaterialTheme.colorScheme.outline else tint,
|
||||
)
|
||||
} else {
|
||||
icon()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<imgs.length;i++){" +
|
||||
"imgs[i].pos = i;" +
|
||||
"imgs[i].onclick=function(){" +
|
||||
// "window.jsCallJavaObj.openImage(this.src,this.pos);" +
|
||||
"alert('asf');" +
|
||||
"}}})()"
|
||||
view!!.loadUrl(jsCode)
|
||||
}
|
||||
|
||||
override fun shouldOverrideUrlLoading(
|
||||
view: WebView?,
|
||||
request: WebResourceRequest?,
|
||||
): Boolean {
|
||||
if (null == request?.url) return false
|
||||
val url = request.url.toString()
|
||||
if (url.isNotEmpty()) context.openURL(url, openLink, openLinkSpecificBrowser)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onReceivedError(
|
||||
view: WebView?,
|
||||
request: WebResourceRequest?,
|
||||
error: WebResourceError?,
|
||||
) {
|
||||
super.onReceivedError(view, request, error)
|
||||
onReceivedError(error)
|
||||
}
|
||||
|
||||
override fun onReceivedSslError(
|
||||
view: WebView?,
|
||||
handler: SslErrorHandler?,
|
||||
error: SslError?,
|
||||
) {
|
||||
handler?.cancel()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
val webView by remember {
|
||||
mutableStateOf(WebView(context).apply {
|
||||
this.webViewClient = webViewClient
|
||||
setBackgroundColor(backgroundColor)
|
||||
isHorizontalScrollBarEnabled = false
|
||||
isVerticalScrollBarEnabled = false
|
||||
})
|
||||
}
|
||||
|
||||
// Column(
|
||||
// modifier = modifier
|
||||
// .height(if (viewState.isLoading) 100.dp else 0.dp),
|
||||
// ) {
|
||||
// Icon(
|
||||
// modifier = modifier
|
||||
// .size(50.dp),
|
||||
// imageVector = Icons.Rounded.HourglassBottom,
|
||||
// contentDescription = "Loading",
|
||||
// tint = MaterialTheme.colorScheme.primary,
|
||||
// )
|
||||
// Spacer(modifier = modifier.height(50.dp))
|
||||
// }
|
||||
|
||||
AndroidView(
|
||||
modifier = modifier,//.padding(horizontal = if (content.contains("class=\"page\"")) 0.dp else 24.dp),
|
||||
factory = { webView },
|
||||
update = {
|
||||
it.apply {
|
||||
Log.i("RLog", "CustomWebView: ${content}")
|
||||
settings.javaScriptEnabled = true
|
||||
loadDataWithBaseURL(
|
||||
null,
|
||||
getStyle(color) + content,
|
||||
"text/HTML",
|
||||
"UTF-8", null
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Stable
|
||||
fun argbToCssColor(argb: Int): String = String.format("#%06X", 0xFFFFFF and argb)
|
||||
|
||||
// TODO: Google sans is deprecated
|
||||
@Stable
|
||||
fun getStyle(argb: Int): String = """
|
||||
<html><head><style>
|
||||
*{
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
color: ${argbToCssColor(argb)};
|
||||
font-family: url('/android_asset_font/font/google_sans_text_regular.TTF'),
|
||||
url('/android_asset_font/font/google_sans_text_medium_italic.TTF'),
|
||||
url('/android_asset_font/font/google_sans_text_medium.TTF'),
|
||||
url('/android_asset_font/font/google_sans_text_italic.TTF'),
|
||||
url('/android_asset_font/font/google_sans_text_bold_italic.TTF'),
|
||||
url('/android_asset_font/font/google_sans_text_bold.TTF');
|
||||
}
|
||||
|
||||
html {
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
img, video, iframe {
|
||||
margin: 0 -24px 20px;
|
||||
width: calc(100% + 48px);
|
||||
border-top: 1px solid ${argbToCssColor(argb)}08;
|
||||
border-bottom: 1px solid ${argbToCssColor(argb)}08;
|
||||
}
|
||||
|
||||
p,span,a,ol,ul,blockquote,article,section {
|
||||
text-align: left;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
ol,ul {
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
section ul {
|
||||
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin-left: 0.5rem;
|
||||
padding-left: 0.7rem;
|
||||
border-left: 1px solid ${argbToCssColor(argb)}33;
|
||||
color: ${argbToCssColor(argb)}cc;
|
||||
}
|
||||
|
||||
pre {
|
||||
max-width: 100%;
|
||||
background: ${argbToCssColor(argb)}11;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
code {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
hr {
|
||||
height: 1px;
|
||||
border: none;
|
||||
background: ${argbToCssColor(argb)}33;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
h1,h2,h3,h4,h5,h6,figure,br {
|
||||
font-size: large;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.element::-webkit-scrollbar { width: 0 !important }
|
||||
</style></head></html>
|
||||
"""
|
|
@ -0,0 +1,59 @@
|
|||
package me.ash.reader.ui.component.webview
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
@Composable
|
||||
fun BionicReadingIcon(
|
||||
modifier: Modifier = Modifier,
|
||||
size: Dp = 24.dp,
|
||||
tint: Color = Color.Black,
|
||||
filled: Boolean = false,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier.size(size),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Row {
|
||||
Text(
|
||||
text = "B",
|
||||
fontFamily = FontFamily.SansSerif,
|
||||
fontSize = (size.value * 0.65F).sp,
|
||||
fontWeight = if (filled) FontWeight.W900 else FontWeight.W700,
|
||||
color = if (filled) tint else tint.copy(alpha = 0.6F),
|
||||
textDecoration = if (filled) TextDecoration.Underline else TextDecoration.None
|
||||
)
|
||||
Text(
|
||||
text = "R",
|
||||
fontFamily = FontFamily.SansSerif,
|
||||
fontSize = (size.value * 0.65F).sp,
|
||||
fontWeight = FontWeight.W300,
|
||||
color = if (filled) tint else tint.copy(alpha = 0.6F),
|
||||
textDecoration = if (filled) TextDecoration.Underline else TextDecoration.None
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(backgroundColor = 0xFFFFFF)
|
||||
@Composable
|
||||
private fun BionicReadingIconPreview() {
|
||||
Column {
|
||||
BionicReadingIcon()
|
||||
BionicReadingIcon(filled = true)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package me.ash.reader.ui.component.webview
|
||||
|
||||
import android.webkit.JavascriptInterface
|
||||
|
||||
interface JavaScriptInterface {
|
||||
|
||||
@JavascriptInterface
|
||||
fun onImgTagClick(imgUrl: String?, alt: String?)
|
||||
|
||||
companion object {
|
||||
|
||||
const val NAME = "JavaScriptInterface"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,126 @@
|
|||
package me.ash.reader.ui.component.webview
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
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.Color
|
||||
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.LocalReadingBionicReading
|
||||
import me.ash.reader.infrastructure.preference.LocalReadingImageHorizontalPadding
|
||||
import me.ash.reader.infrastructure.preference.LocalReadingImageRoundedCorners
|
||||
import me.ash.reader.infrastructure.preference.LocalReadingPageTonalElevation
|
||||
import me.ash.reader.infrastructure.preference.LocalReadingSubheadBold
|
||||
import me.ash.reader.infrastructure.preference.LocalReadingSubheadUpperCase
|
||||
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.infrastructure.preference.LocalReadingTextLetterSpacing
|
||||
import me.ash.reader.infrastructure.preference.LocalReadingTextLineHeight
|
||||
import me.ash.reader.ui.ext.openURL
|
||||
import me.ash.reader.ui.ext.surfaceColorAtElevation
|
||||
import me.ash.reader.ui.theme.palette.alwaysLight
|
||||
|
||||
@Composable
|
||||
fun RYWebView(
|
||||
modifier: Modifier = Modifier,
|
||||
content: String,
|
||||
refererDomain: String? = null,
|
||||
onImageClick: ((imgUrl: String, altText: String) -> Unit)? = null,
|
||||
) {
|
||||
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 selectionTextColor = Color.Black.toArgb()
|
||||
val selectionBgColor = (MaterialTheme.colorScheme.tertiaryContainer alwaysLight true).toArgb()
|
||||
val textColor: Int = MaterialTheme.colorScheme.onSurfaceVariant.toArgb()
|
||||
val textBold: Boolean = LocalReadingTextBold.current.value
|
||||
val textAlign: String = LocalReadingTextAlign.current.toTextAlignCSS()
|
||||
val textMargin: Int = LocalReadingTextHorizontalPadding.current
|
||||
val boldTextColor: Int = MaterialTheme.colorScheme.onSurface.toArgb()
|
||||
val linkTextColor: Int = MaterialTheme.colorScheme.primary.toArgb()
|
||||
val subheadBold: Boolean = LocalReadingSubheadBold.current.value
|
||||
val subheadUpperCase: Boolean = LocalReadingSubheadUpperCase.current.value
|
||||
val fontSize: Int = LocalReadingTextFontSize.current
|
||||
val letterSpacing: Float = LocalReadingTextLetterSpacing.current
|
||||
val lineHeight: Float = LocalReadingTextLineHeight.current
|
||||
val imgMargin: Int = LocalReadingImageHorizontalPadding.current
|
||||
val imgBorderRadius: Int = LocalReadingImageRoundedCorners.current
|
||||
val codeTextColor: Int = MaterialTheme.colorScheme.tertiary.toArgb()
|
||||
val codeBgColor: Int = MaterialTheme.colorScheme
|
||||
.surfaceColorAtElevation((tonalElevation.value + 6).dp).toArgb()
|
||||
val bionicReading = LocalReadingBionicReading.current
|
||||
|
||||
val webView by remember(backgroundColor) {
|
||||
mutableStateOf(
|
||||
WebViewLayout.get(
|
||||
context = context,
|
||||
webViewClient = WebViewClient(
|
||||
context = context,
|
||||
refererDomain = refererDomain,
|
||||
onOpenLink = { url ->
|
||||
context.openURL(url, openLink, openLinkSpecificBrowser)
|
||||
}
|
||||
),
|
||||
onImageClick = onImageClick
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
AndroidView(
|
||||
modifier = modifier,
|
||||
factory = { webView },
|
||||
update = {
|
||||
it.apply {
|
||||
Log.i("RLog", "maxWidth: ${maxWidth}")
|
||||
Log.i("RLog", "readingFont: ${context.filesDir.absolutePath}")
|
||||
Log.i("RLog", "CustomWebView: ${content}")
|
||||
settings.defaultFontSize = fontSize
|
||||
loadDataWithBaseURL(
|
||||
null,
|
||||
WebViewHtml.HTML.format(
|
||||
WebViewStyle.get(
|
||||
fontSize = fontSize,
|
||||
lineHeight = lineHeight,
|
||||
letterSpacing = letterSpacing,
|
||||
textMargin = textMargin,
|
||||
textColor = textColor,
|
||||
textBold = textBold,
|
||||
textAlign = textAlign,
|
||||
boldTextColor = boldTextColor,
|
||||
subheadBold = subheadBold,
|
||||
subheadUpperCase = subheadUpperCase,
|
||||
imgMargin = imgMargin,
|
||||
imgBorderRadius = imgBorderRadius,
|
||||
linkTextColor = linkTextColor,
|
||||
codeTextColor = codeTextColor,
|
||||
codeBgColor = codeBgColor,
|
||||
tableMargin = textMargin,
|
||||
selectionTextColor = selectionTextColor,
|
||||
selectionBgColor = selectionBgColor,
|
||||
),
|
||||
url,
|
||||
content,
|
||||
WebViewScript.get(bionicReading.value),
|
||||
),
|
||||
"text/HTML",
|
||||
"UTF-8", null
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
package me.ash.reader.ui.component.webview
|
||||
|
||||
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 me.ash.reader.ui.ext.isUrl
|
||||
import java.io.DataInputStream
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URI
|
||||
|
||||
const val INJECTION_TOKEN = "/android_asset_font/"
|
||||
|
||||
class WebViewClient(
|
||||
private val context: Context,
|
||||
private val refererDomain: String?,
|
||||
private val onOpenLink: (url: String) -> Unit,
|
||||
) : WebViewClient() {
|
||||
|
||||
override fun shouldInterceptRequest(
|
||||
view: WebView?,
|
||||
request: WebResourceRequest?,
|
||||
): WebResourceResponse? {
|
||||
val url = request?.url?.toString()
|
||||
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")
|
||||
}
|
||||
} else if (url != null && url.isUrl()) {
|
||||
try {
|
||||
var connection = URI.create(url).toURL().openConnection() as HttpURLConnection
|
||||
if (connection.responseCode == 403) {
|
||||
connection.disconnect()
|
||||
connection = URI.create(url).toURL().openConnection() as HttpURLConnection
|
||||
connection.setRequestProperty("Referer", refererDomain)
|
||||
val inputStream = DataInputStream(connection.inputStream)
|
||||
return WebResourceResponse(connection.contentType, "UTF-8", inputStream)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("RLog", "shouldInterceptRequest url: $e")
|
||||
}
|
||||
}
|
||||
return super.shouldInterceptRequest(view, request)
|
||||
}
|
||||
|
||||
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 < imgs.length; i++){
|
||||
imgs[i].pos = i;
|
||||
imgs[i].onclick = function() {
|
||||
window.${JavaScriptInterface.NAME}.onImgTagClick(this.src, this.alt);
|
||||
}
|
||||
}
|
||||
})()
|
||||
"""
|
||||
view!!.loadUrl(jsCode)
|
||||
}
|
||||
|
||||
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
|
||||
if (null == request?.url) return false
|
||||
val url = request.url.toString()
|
||||
if (url.isNotEmpty()) onOpenLink(url)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onReceivedError(
|
||||
view: WebView?,
|
||||
request: WebResourceRequest?,
|
||||
error: WebResourceError?,
|
||||
) {
|
||||
super.onReceivedError(view, request, error)
|
||||
Log.e("RLog", "RYWebView onReceivedError: $error")
|
||||
}
|
||||
|
||||
override fun onReceivedSslError(view: WebView?, handler: SslErrorHandler?, error: SslError?) {
|
||||
handler?.cancel()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package me.ash.reader.ui.component.webview
|
||||
|
||||
object WebViewHtml {
|
||||
|
||||
const val HTML: String = """
|
||||
<!DOCTYPE html>
|
||||
<html dir="auto">
|
||||
<head>
|
||||
<meta name="viewport" content="initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no, width=device-width, viewport-fit=cover" />
|
||||
<meta content="text/html; charset=utf-8" http-equiv="content-type"/>
|
||||
<style type="text/css">
|
||||
%s
|
||||
</style>
|
||||
<base href="%s" />
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<!-- <button id="submit-btn" onclick="bionicRead()">BIONIC</button> -->
|
||||
<article>
|
||||
%s
|
||||
</article>
|
||||
</main>
|
||||
<script>
|
||||
%s
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
package me.ash.reader.ui.component.webview
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.webkit.JavascriptInterface
|
||||
import android.webkit.WebView
|
||||
|
||||
object WebViewLayout {
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
fun get(
|
||||
context: Context,
|
||||
webViewClient: WebViewClient,
|
||||
onImageClick: ((imgUrl: String, altText: String) -> Unit)? = null,
|
||||
) = WebView(context).apply {
|
||||
this.webViewClient = webViewClient
|
||||
scrollBarSize = 0
|
||||
isHorizontalScrollBarEnabled = false
|
||||
isVerticalScrollBarEnabled = true
|
||||
setBackgroundColor(Color.TRANSPARENT)
|
||||
with(this.settings) {
|
||||
domStorageEnabled = true
|
||||
javaScriptEnabled = true
|
||||
addJavascriptInterface(object : JavaScriptInterface {
|
||||
@JavascriptInterface
|
||||
override fun onImgTagClick(imgUrl: String?, alt: String?) {
|
||||
if (onImageClick != null && imgUrl != null) {
|
||||
onImageClick.invoke(imgUrl, alt ?: "")
|
||||
}
|
||||
}
|
||||
}, JavaScriptInterface.NAME)
|
||||
setSupportZoom(false)
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
|
||||
isAlgorithmicDarkeningAllowed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
package me.ash.reader.ui.component.webview
|
||||
|
||||
object WebViewScript {
|
||||
|
||||
fun get(bionicReading: Boolean) = """
|
||||
function bionicRead() {
|
||||
let div = document.body;
|
||||
|
||||
// Check if the input is empty
|
||||
if (!div) {
|
||||
alert("The element with id 'readability-page-1' does not exist.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove all existing <strong> tags
|
||||
let strongTags = div.querySelectorAll('strong');
|
||||
strongTags.forEach(tag => {
|
||||
let parent = tag.parentNode;
|
||||
while (tag.firstChild) {
|
||||
parent.insertBefore(tag.firstChild, tag);
|
||||
}
|
||||
parent.removeChild(tag);
|
||||
});
|
||||
|
||||
// Get all text nodes within the div, ignoring <code> elements and their children
|
||||
let walker = document.createTreeWalker(div, NodeFilter.SHOW_TEXT, {
|
||||
acceptNode: function(node) {
|
||||
let parent = node.parentNode;
|
||||
while (parent) {
|
||||
if (parent.nodeName === 'CODE') {
|
||||
return NodeFilter.FILTER_REJECT;
|
||||
}
|
||||
parent = parent.parentNode;
|
||||
}
|
||||
return NodeFilter.FILTER_ACCEPT;
|
||||
}
|
||||
});
|
||||
|
||||
let textNodes = [];
|
||||
while (walker.nextNode()) {
|
||||
textNodes.push(walker.currentNode);
|
||||
}
|
||||
|
||||
// Regex to match emoji characters
|
||||
const emojiRegex = /[\u{1F600}-\u{1F6FF}|\u{1F300}-\u{1F5FF}|\u{1F680}-\u{1F6FF}|\u{1F700}-\u{1F77F}|\u{1F780}-\u{1F7FF}|\u{1F800}-\u{1F8FF}|\u{1F900}-\u{1F9FF}|\u{1FA00}-\u{1FA6F}|\u{1FA70}-\u{1FAFF}|\u{2600}-\u{26FF}|\u{2700}-\u{27BF}|\u{1F1E0}-\u{1F1FF}]/u;
|
||||
|
||||
// Process each text node
|
||||
textNodes.forEach(node => {
|
||||
let text = node.textContent;
|
||||
|
||||
// Split text into words and process each word
|
||||
let words = text.split(/(\s+)/); // Keep spaces in the split
|
||||
let formattedText = "";
|
||||
words.forEach(word => {
|
||||
if (word.trim() && !emojiRegex.test(word)) {
|
||||
let halfIndex = Math.round(word.length / 2);
|
||||
let half = word.substr(0, halfIndex);
|
||||
let remHalf = word.substr(halfIndex);
|
||||
formattedText += "<strong>" + half + "</strong>" + remHalf;
|
||||
} else {
|
||||
formattedText += word; // Preserve spaces and skip emoji
|
||||
}
|
||||
});
|
||||
|
||||
// Create a temporary div to parse HTML
|
||||
let tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = formattedText;
|
||||
|
||||
// Replace original text node with new HTML content
|
||||
while (tempDiv.firstChild) {
|
||||
node.parentNode.insertBefore(tempDiv.firstChild, node);
|
||||
}
|
||||
node.parentNode.removeChild(node);
|
||||
});
|
||||
}
|
||||
|
||||
${if (bionicReading) "bionicRead()" else ""}
|
||||
|
||||
var images = document.querySelectorAll("img");
|
||||
|
||||
images.forEach(function(img) {
|
||||
img.onload = function() {
|
||||
img.classList.add("loaded");
|
||||
console.log("Image width:", img.width, "px");
|
||||
if (img.width < 412) {
|
||||
img.classList.add("thin");
|
||||
}
|
||||
};
|
||||
|
||||
img.onerror = function() {
|
||||
console.error("Failed to load image:", img.src);
|
||||
};
|
||||
});
|
||||
"""
|
||||
}
|
|
@ -0,0 +1,337 @@
|
|||
package me.ash.reader.ui.component.webview
|
||||
|
||||
object WebViewStyle {
|
||||
|
||||
private fun argbToCssColor(argb: Int): String = String.format("#%06X", 0xFFFFFF and argb)
|
||||
|
||||
fun get(
|
||||
fontSize: Int,
|
||||
lineHeight: Float,
|
||||
letterSpacing: Float,
|
||||
textMargin: Int,
|
||||
textColor: Int,
|
||||
textBold: Boolean,
|
||||
textAlign: String,
|
||||
boldTextColor: Int,
|
||||
subheadBold: Boolean,
|
||||
subheadUpperCase: Boolean,
|
||||
imgMargin: Int,
|
||||
imgBorderRadius: Int,
|
||||
linkTextColor: Int,
|
||||
codeTextColor: Int,
|
||||
codeBgColor: Int,
|
||||
tableMargin: Int,
|
||||
selectionTextColor: Int,
|
||||
selectionBgColor: Int,
|
||||
): String = """
|
||||
:root {
|
||||
/* --font-family: Inter; */
|
||||
--font-size: ${fontSize}px;
|
||||
--line-height: ${lineHeight};
|
||||
--letter-spacing: ${letterSpacing}px;
|
||||
--text-margin: ${textMargin}px;
|
||||
--text-color: ${argbToCssColor(textColor)};
|
||||
--text-bold: ${if(textBold) "600" else "normal"};
|
||||
--text-align: ${textAlign};
|
||||
--bold-text-color: ${argbToCssColor(boldTextColor)};
|
||||
--link-text-color: ${argbToCssColor(linkTextColor)};
|
||||
--selection-text-color: ${argbToCssColor(selectionTextColor)};
|
||||
--selection-bg-color: ${argbToCssColor(selectionBgColor)};
|
||||
--subhead-bold: ${if(subheadBold) "600" else "normal"};
|
||||
--subhead-upper-case: ${if(subheadUpperCase) "uppercase" else "none"};
|
||||
--img-margin: ${imgMargin}px;
|
||||
--img-border-radius: ${imgBorderRadius}px;
|
||||
--content-padding;
|
||||
--bold-text-color;
|
||||
--image-caption-margin;
|
||||
--blockquote-margin: 20px;
|
||||
--blockquote-padding;
|
||||
--blockquote-bg-color;
|
||||
--blockquote-border-width: 3px;
|
||||
--blockquote-border-color: ${argbToCssColor(textColor)}33;
|
||||
--table-margin: ${tableMargin}px;
|
||||
--table-border-width;
|
||||
--table-border-color;
|
||||
--table-cell-padding: 0.2em;
|
||||
--table-alt-row-bg-color;
|
||||
--code-text-color: ${argbToCssColor(codeTextColor)};
|
||||
--code-bg-color: ${argbToCssColor(codeBgColor)};
|
||||
--code-scrollbar-color: ${argbToCssColor(codeTextColor)}22;
|
||||
--code-border-width;
|
||||
--code-border-color;
|
||||
--code-padding;
|
||||
--code-font-family: Menlo, Monospace, 'Courier New';
|
||||
--code-font-size: 0.9em;
|
||||
--pre-color;
|
||||
}
|
||||
|
||||
article {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
margin-left: var(--text-margin) !important;
|
||||
margin-right: var(--text-margin) !important;
|
||||
font-family: var(--font-family) !important;
|
||||
font-size: var(--font-size) !important;
|
||||
font-weight: var(--text-bold) !important;
|
||||
color: var(--text-color) !important;
|
||||
}
|
||||
|
||||
/* Page */
|
||||
body {
|
||||
margin: 0;
|
||||
padding 0;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background-color: var(--selection-bg-color) !important;
|
||||
color: var(--selection-text-color) !important;
|
||||
}
|
||||
|
||||
/* Heading */
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-weight: var(--subhead-bold) !important;
|
||||
text-transform: var(--subhead-upper-case) !important;
|
||||
line-height: calc(min(1.2, var(--line-height))) !important;
|
||||
letter-spacing: var(--letter-spacing) !important;
|
||||
color: var(--bold-text-color) !important;
|
||||
text-align: var(--text-align) !important;
|
||||
}
|
||||
|
||||
/* Paragraph */
|
||||
p {
|
||||
max-width: 100% !important;
|
||||
word-wrap: break-word !important;
|
||||
overflow-wrap: break-word !important;
|
||||
line-height: var(--line-height) !important;
|
||||
letter-spacing: var(--letter-spacing) !important;
|
||||
text-align: var(--text-align) !important;
|
||||
}
|
||||
|
||||
span {
|
||||
line-height: var(--line-height) !important;
|
||||
letter-spacing: var(--letter-spacing) !important;
|
||||
text-align: var(--text-align) !important;
|
||||
}
|
||||
|
||||
/* Strong */
|
||||
strong,
|
||||
b {
|
||||
font-weight: 600 !important;
|
||||
color: var(--bold-text-color) !important;
|
||||
}
|
||||
|
||||
/* Link */
|
||||
a,
|
||||
a > strong {
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
font-weight: 600 !important;
|
||||
color: var(--link-text-color) !important;
|
||||
}
|
||||
div > a {
|
||||
display: block;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
font-weight: 600 !important;
|
||||
color: var(--link-text-color);
|
||||
line-height: var(--line-height);
|
||||
letter-spacing: var(--letter-spacing) !important;
|
||||
text-align: var(--text-align) !important;
|
||||
}
|
||||
|
||||
/* Image */
|
||||
iframe,
|
||||
video,
|
||||
embed,
|
||||
object,
|
||||
img {
|
||||
margin-top: 0.5em !important;
|
||||
margin-left: calc(0px - var(--text-margin) + var(--img-margin)) !important;
|
||||
margin-right: calc(0px - var(--text-margin) + var(--img-margin)) !important;
|
||||
max-width: calc(100% + 2 * var(--text-margin) - 2 * var(--img-margin)) !important;
|
||||
border-radius: var(--img-border-radius) !important;
|
||||
}
|
||||
|
||||
img {
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
img::after {
|
||||
width: 100px !important;
|
||||
}
|
||||
|
||||
img.loaded {
|
||||
opacity: 1; /* 加载完成后设置透明度为1 */
|
||||
}
|
||||
|
||||
img.thin {
|
||||
margin-top: 0.5em !important;
|
||||
margin-bottom: 0.5em !important;
|
||||
margin-left: unset !important;
|
||||
margin-right: unset !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
p > img {
|
||||
margin-top: 0.5em !important;
|
||||
margin-bottom: 0.5em !important;
|
||||
margin-left: calc(0px - var(--text-margin) + var(--img-margin)) !important;
|
||||
margin-right: calc(0px - var(--text-margin) + var(--img-margin)) !important;
|
||||
max-width: calc(100% + 2 * var(--text-margin) - 2 * var(--img-margin)) !important;
|
||||
height: auto !important;
|
||||
border-radius: var(--img-border-radius) !important;
|
||||
}
|
||||
|
||||
img + small {
|
||||
display: inline-block;
|
||||
line-height: calc(min(1.5, var(--line-height))) !important;
|
||||
letter-spacing: var(--letter-spacing) !important;
|
||||
margin-top: var(--image-caption-margin) !important;
|
||||
text-align: var(--text-align) !important;
|
||||
}
|
||||
|
||||
/* List */
|
||||
ul,
|
||||
ol {
|
||||
padding-left: 0 !important;
|
||||
line-height: var(--line-height) !important;
|
||||
letter-spacing: var(--letter-spacing) !important;
|
||||
text-align: var(--text-align) !important;
|
||||
}
|
||||
|
||||
li {
|
||||
line-height: var(--line-height) !important;
|
||||
letter-spacing: var(--letter-spacing) !important;
|
||||
margin-left: 1.5em !important;
|
||||
text-align: var(--text-align) !important;
|
||||
}
|
||||
|
||||
/* Quote */
|
||||
blockquote {
|
||||
margin-left: 0.5em !important;
|
||||
padding-left: calc(0.9em) !important;
|
||||
background-color: var(--blockquote-bg-color) !important;
|
||||
border-left: var(--blockquote-border-width) solid var(--blockquote-border-color) !important;
|
||||
line-height: var(--line-height) !important;
|
||||
letter-spacing: var(--letter-spacing) !important;
|
||||
text-align: var(--text-align) !important;
|
||||
}
|
||||
|
||||
blockquote blockquote {
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
|
||||
blockquote img {
|
||||
max-width 100% !important;
|
||||
left: 0 !important;
|
||||
}
|
||||
|
||||
/* Table */
|
||||
table {
|
||||
display: block;
|
||||
max-width: var(--content-width) !important;
|
||||
width: 100% !important;
|
||||
border-collapse: collapse !important;
|
||||
margin-left: var(--table-margin) !important;
|
||||
margin-right: var(--table-margin) !important;
|
||||
}
|
||||
|
||||
table th,
|
||||
table td {
|
||||
border: var(--table-border-width) solid var(--table-border-color) !important;
|
||||
padding: var(--table-cell-padding) !important;
|
||||
line-height: var(--line-height) !important;
|
||||
letter-spacing: var(--letter-spacing) !important;
|
||||
text-align: var(--text-align) !important;
|
||||
}
|
||||
|
||||
table tr {
|
||||
display: block;
|
||||
}
|
||||
|
||||
table tr table tr td {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
table tr:nth-child(even) {
|
||||
background-color: var(--table-alt-row-bg-color) !important;
|
||||
}
|
||||
|
||||
/* Code */
|
||||
pre,
|
||||
code {
|
||||
color: var(--code-text-color) !important;
|
||||
background-color: var(--code-bg-color) !important;
|
||||
border: 1 solid var(--code-text-color) !important;
|
||||
border-radius: 8px !important;
|
||||
padding: 2px 5px !important;
|
||||
margin: 2px !important;
|
||||
font-family: var(--code-font-family) !important;
|
||||
font-size: var(--code-font-size) !important;
|
||||
}
|
||||
|
||||
pre {
|
||||
overflow: auto !important;
|
||||
}
|
||||
|
||||
code {
|
||||
display: inline-block !important;
|
||||
}
|
||||
|
||||
li code {
|
||||
white-space: pre-wrap !important;
|
||||
word-wrap: break-word !important;
|
||||
overflow-wrap: break-word !important;
|
||||
max-width: 100% !important;
|
||||
}
|
||||
|
||||
pre::-webkit-scrollbar {
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
pre::-webkit-scrollbar-track {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
pre::-webkit-scrollbar-thumb {
|
||||
background-color: var(--code-scrollbar-color);
|
||||
border-radius: 7px;
|
||||
background-clip: content-box;
|
||||
border: 5px solid transparent;
|
||||
border-left-width: 10px;
|
||||
border-right-width: 10px;
|
||||
}
|
||||
|
||||
/* MISC */
|
||||
figure {
|
||||
line-height: calc(min(1.5, var(--line-height))) !important;
|
||||
letter-spacing: var(--letter-spacing) !important;
|
||||
text-align: var(--text-align) !important;
|
||||
margin: 0 !important;
|
||||
opacity: 0.8 !important;
|
||||
font-size: 0.8em !important;
|
||||
}
|
||||
|
||||
figure * {
|
||||
font-size: 1em !important;
|
||||
}
|
||||
|
||||
figure p,
|
||||
caption,
|
||||
figcaption {
|
||||
opacity: 0.8 !important;
|
||||
font-size: 0.8em !important;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: 0 !important;
|
||||
height: 2px !important;
|
||||
background-color: var(--text-color) !important;
|
||||
opacity: 0.08 !important;
|
||||
border-radius: 2px;
|
||||
}
|
||||
"""
|
||||
}
|
|
@ -134,6 +134,8 @@ data class DataStoreKey<T>(
|
|||
const val flowArticleListReadIndicator = "flowArticleListReadIndicator"
|
||||
|
||||
// Reading page
|
||||
const val readingRenderer = "readingRender"
|
||||
const val readingBionicReading = "readingBionicReading"
|
||||
const val readingDarkTheme = "readingDarkTheme"
|
||||
const val readingPageTonalElevation = "readingPageTonalElevation"
|
||||
const val readingTextFontSize = "readingTextFontSize"
|
||||
|
@ -207,6 +209,8 @@ data class DataStoreKey<T>(
|
|||
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),
|
||||
readingBionicReading to DataStoreKey(booleanPreferencesKey(readingBionicReading), Boolean::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),
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package me.ash.reader.ui.page.common
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
|
@ -22,9 +21,9 @@ import kotlinx.coroutines.flow.collectLatest
|
|||
import me.ash.reader.domain.model.general.Filter
|
||||
import me.ash.reader.infrastructure.preference.LocalDarkTheme
|
||||
import me.ash.reader.infrastructure.preference.LocalReadingDarkTheme
|
||||
import me.ash.reader.ui.ext.animatedComposable
|
||||
import me.ash.reader.ui.ext.collectAsStateValue
|
||||
import me.ash.reader.ui.ext.findActivity
|
||||
import me.ash.reader.ui.ext.animatedComposable
|
||||
import me.ash.reader.ui.ext.initialFilter
|
||||
import me.ash.reader.ui.ext.initialPage
|
||||
import me.ash.reader.ui.ext.isFirstLaunch
|
||||
|
@ -41,6 +40,7 @@ import me.ash.reader.ui.page.settings.color.ColorAndStylePage
|
|||
import me.ash.reader.ui.page.settings.color.DarkThemePage
|
||||
import me.ash.reader.ui.page.settings.color.feeds.FeedsPageStylePage
|
||||
import me.ash.reader.ui.page.settings.color.flow.FlowPageStylePage
|
||||
import me.ash.reader.ui.page.settings.color.reading.BionicReadingPage
|
||||
import me.ash.reader.ui.page.settings.color.reading.ReadingDarkThemePage
|
||||
import me.ash.reader.ui.page.settings.color.reading.ReadingImagePage
|
||||
import me.ash.reader.ui.page.settings.color.reading.ReadingStylePage
|
||||
|
@ -207,6 +207,9 @@ fun HomeEntry(
|
|||
animatedComposable(route = RouteName.READING_PAGE_STYLE) {
|
||||
ReadingStylePage(navController)
|
||||
}
|
||||
animatedComposable(route = RouteName.READING_BIONIC_READING) {
|
||||
BionicReadingPage(navController)
|
||||
}
|
||||
animatedComposable(route = RouteName.READING_DARK_THEME) {
|
||||
ReadingDarkThemePage(navController)
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ object RouteName {
|
|||
const val FEEDS_PAGE_STYLE = "feeds_page_style"
|
||||
const val FLOW_PAGE_STYLE = "flow_page_style"
|
||||
const val READING_PAGE_STYLE = "reading_page_style"
|
||||
const val READING_BIONIC_READING = "reading_bionic_reading"
|
||||
const val READING_DARK_THEME = "reading_dark_theme"
|
||||
const val READING_PAGE_TITLE = "reading_page_title"
|
||||
const val READING_PAGE_TEXT = "reading_page_text"
|
||||
|
|
|
@ -1,15 +1,20 @@
|
|||
package me.ash.reader.ui.page.home.reading
|
||||
|
||||
import android.view.HapticFeedbackConstants
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.Article
|
||||
import androidx.compose.material.icons.automirrored.rounded.Article
|
||||
import androidx.compose.material.icons.filled.FiberManualRecord
|
||||
import androidx.compose.material.icons.outlined.Article
|
||||
import androidx.compose.material.icons.outlined.FiberManualRecord
|
||||
import androidx.compose.material.icons.outlined.Headphones
|
||||
import androidx.compose.material.icons.rounded.Article
|
||||
import androidx.compose.material.icons.rounded.ExpandMore
|
||||
import androidx.compose.material.icons.rounded.Star
|
||||
import androidx.compose.material.icons.rounded.StarOutline
|
||||
|
@ -24,8 +29,11 @@ import androidx.compose.ui.unit.dp
|
|||
import androidx.compose.ui.zIndex
|
||||
import me.ash.reader.R
|
||||
import me.ash.reader.infrastructure.preference.LocalReadingPageTonalElevation
|
||||
import me.ash.reader.infrastructure.preference.LocalReadingRenderer
|
||||
import me.ash.reader.infrastructure.preference.ReadingRendererPreference
|
||||
import me.ash.reader.ui.component.base.CanBeDisabledIconButton
|
||||
import me.ash.reader.ui.component.base.RYExtensibleVisibility
|
||||
import me.ash.reader.ui.component.webview.BionicReadingIcon
|
||||
|
||||
@Composable
|
||||
fun BottomBar(
|
||||
|
@ -34,12 +42,16 @@ fun BottomBar(
|
|||
isStarred: Boolean,
|
||||
isNextArticleAvailable: Boolean,
|
||||
isFullContent: Boolean,
|
||||
isBionicReading: Boolean,
|
||||
onUnread: (isUnread: Boolean) -> Unit = {},
|
||||
onStarred: (isStarred: Boolean) -> Unit = {},
|
||||
onNextArticle: () -> Unit = {},
|
||||
onFullContent: (isFullContent: Boolean) -> Unit = {},
|
||||
onBionicReading: () -> Unit = {},
|
||||
onReadAloud: () -> Unit = {},
|
||||
) {
|
||||
val tonalElevation = LocalReadingPageTonalElevation.current
|
||||
val renderer = LocalReadingRenderer.current
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
|
@ -110,12 +122,32 @@ fun BottomBar(
|
|||
}
|
||||
CanBeDisabledIconButton(
|
||||
modifier = Modifier.size(36.dp),
|
||||
disabled = true,
|
||||
imageVector = Icons.Outlined.Headphones,
|
||||
contentDescription = "Add Tag",
|
||||
disabled = false,
|
||||
imageVector = if (renderer == ReadingRendererPreference.WebView) null else Icons.Outlined.Headphones,
|
||||
contentDescription = if (renderer == ReadingRendererPreference.WebView) {
|
||||
stringResource(R.string.bionic_reading)
|
||||
} else {
|
||||
stringResource(R.string.read_aloud)
|
||||
},
|
||||
tint = MaterialTheme.colorScheme.outline,
|
||||
icon = {
|
||||
BionicReadingIcon(
|
||||
filled = isBionicReading,
|
||||
size = 24.dp,
|
||||
tint = if (renderer == ReadingRendererPreference.WebView) {
|
||||
MaterialTheme.colorScheme.onSecondaryContainer
|
||||
} else {
|
||||
MaterialTheme.colorScheme.outline
|
||||
}
|
||||
)
|
||||
},
|
||||
) {
|
||||
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
||||
if (renderer == ReadingRendererPreference.WebView) {
|
||||
onBionicReading()
|
||||
} else {
|
||||
onReadAloud()
|
||||
}
|
||||
}
|
||||
CanBeDisabledIconButton(
|
||||
disabled = false,
|
||||
|
|
|
@ -1,44 +1,35 @@
|
|||
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.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.windowInsetsBottomHeight
|
||||
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 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.reader.Reader
|
||||
import me.ash.reader.ui.component.webview.RYWebView
|
||||
import me.ash.reader.ui.ext.drawVerticalScrollbar
|
||||
import me.ash.reader.ui.ext.extractDomain
|
||||
import me.ash.reader.ui.ext.openURL
|
||||
import me.ash.reader.ui.ext.pagerAnimate
|
||||
import java.util.*
|
||||
import kotlin.math.abs
|
||||
import java.util.Date
|
||||
|
||||
@Composable
|
||||
fun Content(
|
||||
|
@ -57,6 +48,7 @@ fun Content(
|
|||
val subheadUpperCase = LocalReadingSubheadUpperCase.current
|
||||
val openLink = LocalOpenLink.current
|
||||
val openLinkSpecificBrowser = LocalOpenLinkSpecificBrowser.current
|
||||
val renderer = LocalReadingRenderer.current
|
||||
|
||||
if (isLoading) {
|
||||
Column {
|
||||
|
@ -94,17 +86,33 @@ fun Content(
|
|||
)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(22.dp))
|
||||
}
|
||||
Reader(
|
||||
context = context,
|
||||
subheadUpperCase = subheadUpperCase.value,
|
||||
link = link ?: "",
|
||||
content = content,
|
||||
onImageClick = onImageClick,
|
||||
onLinkClick = {
|
||||
context.openURL(it, openLink, openLinkSpecificBrowser)
|
||||
|
||||
when (renderer) {
|
||||
ReadingRendererPreference.WebView -> {
|
||||
item {
|
||||
RYWebView(
|
||||
content = content,
|
||||
refererDomain = link.extractDomain(),
|
||||
onImageClick = onImageClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
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))
|
||||
|
|
|
@ -24,25 +24,25 @@ import androidx.compose.runtime.setValue
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.isSpecified
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import me.ash.reader.R
|
||||
import me.ash.reader.infrastructure.preference.LocalPullToSwitchArticle
|
||||
import me.ash.reader.infrastructure.preference.LocalReadingAutoHideToolbar
|
||||
import me.ash.reader.infrastructure.preference.LocalReadingPageTonalElevation
|
||||
import me.ash.reader.infrastructure.preference.LocalReadingBionicReading
|
||||
import me.ash.reader.infrastructure.preference.LocalReadingTextLineHeight
|
||||
import me.ash.reader.infrastructure.preference.not
|
||||
import me.ash.reader.ui.ext.collectAsStateValue
|
||||
import me.ash.reader.ui.ext.showToast
|
||||
import me.ash.reader.ui.motion.materialSharedAxisY
|
||||
import me.ash.reader.ui.page.home.HomeViewModel
|
||||
import kotlin.math.abs
|
||||
|
||||
|
||||
private const val UPWARD = 1
|
||||
private const val DOWNWARD = -1
|
||||
|
||||
|
@ -55,12 +55,12 @@ fun ReadingPage(
|
|||
homeViewModel: HomeViewModel,
|
||||
readingViewModel: ReadingViewModel = hiltViewModel(),
|
||||
) {
|
||||
val tonalElevation = LocalReadingPageTonalElevation.current
|
||||
val context = LocalContext.current
|
||||
val isPullToSwitchArticleEnabled = LocalPullToSwitchArticle.current.value
|
||||
val readingUiState = readingViewModel.readingUiState.collectAsStateValue()
|
||||
val readerState = readingViewModel.readerStateStateFlow.collectAsStateValue()
|
||||
val homeUiState = homeViewModel.homeUiState.collectAsStateValue()
|
||||
val bionicReading = LocalReadingBionicReading.current
|
||||
|
||||
var isReaderScrollingDown by remember { mutableStateOf(false) }
|
||||
var showFullScreenImageViewer by remember { mutableStateOf(false) }
|
||||
|
@ -217,6 +217,7 @@ fun ReadingPage(
|
|||
isStarred = readingUiState.isStarred,
|
||||
isNextArticleAvailable = isNextArticleAvailable,
|
||||
isFullContent = readerState.content is ReaderState.FullContent,
|
||||
isBionicReading = bionicReading.value,
|
||||
onUnread = {
|
||||
readingViewModel.updateReadStatus(it)
|
||||
},
|
||||
|
@ -230,6 +231,12 @@ fun ReadingPage(
|
|||
if (it) readingViewModel.renderFullContent()
|
||||
else readingViewModel.renderDescriptionContent()
|
||||
},
|
||||
onBionicReading = {
|
||||
(!bionicReading).put(context, homeViewModel.viewModelScope)
|
||||
},
|
||||
onReadAloud = {
|
||||
context.showToast(context.getString(R.string.coming_soon))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,8 +9,17 @@
|
|||
package me.ash.reader.ui.page.settings
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.VerticalDivider
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
|
@ -85,7 +94,7 @@ fun SettingItem(
|
|||
}
|
||||
action?.let {
|
||||
if (separatedActions) {
|
||||
HorizontalDivider(
|
||||
VerticalDivider(
|
||||
modifier = Modifier
|
||||
.padding(start = 16.dp)
|
||||
.size(1.dp, 32.dp),
|
||||
|
|
|
@ -0,0 +1,140 @@
|
|||
package me.ash.reader.ui.page.settings.color.reading
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.windowInsetsBottomHeight
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
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.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavHostController
|
||||
import com.ireward.htmlcompose.HtmlText
|
||||
import me.ash.reader.R
|
||||
import me.ash.reader.infrastructure.preference.LocalOpenLink
|
||||
import me.ash.reader.infrastructure.preference.LocalOpenLinkSpecificBrowser
|
||||
import me.ash.reader.infrastructure.preference.LocalReadingBionicReading
|
||||
import me.ash.reader.infrastructure.preference.not
|
||||
import me.ash.reader.ui.component.base.Banner
|
||||
import me.ash.reader.ui.component.base.DisplayText
|
||||
import me.ash.reader.ui.component.base.FeedbackIconButton
|
||||
import me.ash.reader.ui.component.base.RYScaffold
|
||||
import me.ash.reader.ui.component.base.RYSwitch
|
||||
import me.ash.reader.ui.component.base.Subtitle
|
||||
import me.ash.reader.ui.component.base.Tips
|
||||
import me.ash.reader.ui.component.webview.RYWebView
|
||||
import me.ash.reader.ui.ext.openURL
|
||||
import me.ash.reader.ui.theme.palette.onLight
|
||||
|
||||
@Composable
|
||||
fun BionicReadingPage(
|
||||
navController: NavHostController,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val openLink = LocalOpenLink.current
|
||||
val openLinkSpecificBrowser = LocalOpenLinkSpecificBrowser.current
|
||||
|
||||
val bionicReading = LocalReadingBionicReading.current
|
||||
|
||||
RYScaffold(
|
||||
containerColor = MaterialTheme.colorScheme.surface onLight MaterialTheme.colorScheme.inverseOnSurface,
|
||||
navigationIcon = {
|
||||
FeedbackIconButton(
|
||||
imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
|
||||
contentDescription = stringResource(R.string.back),
|
||||
tint = MaterialTheme.colorScheme.onSurface
|
||||
) {
|
||||
navController.popBackStack()
|
||||
}
|
||||
},
|
||||
content = {
|
||||
LazyColumn {
|
||||
item {
|
||||
DisplayText(text = stringResource(R.string.bionic_reading), desc = "")
|
||||
}
|
||||
|
||||
// Preview
|
||||
item {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 24.dp)
|
||||
.clip(RoundedCornerShape(24.dp))
|
||||
.background(
|
||||
MaterialTheme.colorScheme.inverseOnSurface
|
||||
onLight MaterialTheme.colorScheme.surface.copy(0.7f)
|
||||
)
|
||||
.clickable { },
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
RYWebView(
|
||||
content = stringResource(R.string.bionic_reading_preview),
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
}
|
||||
|
||||
item {
|
||||
Banner(
|
||||
modifier = Modifier.padding(horizontal = 8.dp),
|
||||
title = stringResource(R.string.use_bionic_reading),
|
||||
action = {
|
||||
RYSwitch(activated = bionicReading.value) {
|
||||
(!bionicReading).put(context, scope)
|
||||
}
|
||||
},
|
||||
) {
|
||||
(!bionicReading).put(context, scope)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
item {
|
||||
Subtitle(
|
||||
modifier = Modifier.padding(horizontal = 24.dp),
|
||||
text = stringResource(R.string.about)
|
||||
)
|
||||
Tips(
|
||||
text = stringResource(R.string.bionic_reading_tips),
|
||||
)
|
||||
TextButton(
|
||||
modifier = Modifier.padding(horizontal = 12.dp),
|
||||
onClick = {
|
||||
context.openURL(context.getString(R.string.bionic_reading_link), openLink, openLinkSpecificBrowser)
|
||||
}
|
||||
) {
|
||||
HtmlText(
|
||||
text = stringResource(R.string.browse_bionic_reading_tips),
|
||||
style = MaterialTheme.typography.bodySmall.copy(
|
||||
color = MaterialTheme.colorScheme.outline,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Spacer(modifier = Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars))
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
|
@ -1,19 +1,43 @@
|
|||
package me.ash.reader.ui.page.settings.color.reading
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.windowInsetsBottomHeight
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavHostController
|
||||
import me.ash.reader.R
|
||||
import me.ash.reader.infrastructure.preference.*
|
||||
import me.ash.reader.ui.component.base.*
|
||||
import me.ash.reader.infrastructure.preference.LocalReadingImageHorizontalPadding
|
||||
import me.ash.reader.infrastructure.preference.LocalReadingImageMaximize
|
||||
import me.ash.reader.infrastructure.preference.LocalReadingImageRoundedCorners
|
||||
import me.ash.reader.infrastructure.preference.LocalReadingRenderer
|
||||
import me.ash.reader.infrastructure.preference.LocalReadingTheme
|
||||
import me.ash.reader.infrastructure.preference.ReadingImageHorizontalPaddingPreference
|
||||
import me.ash.reader.infrastructure.preference.ReadingImageRoundedCornersPreference
|
||||
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.base.DisplayText
|
||||
import me.ash.reader.ui.component.base.FeedbackIconButton
|
||||
import me.ash.reader.ui.component.base.RYScaffold
|
||||
import me.ash.reader.ui.component.base.RYSwitch
|
||||
import me.ash.reader.ui.component.base.Subtitle
|
||||
import me.ash.reader.ui.component.base.TextFieldDialog
|
||||
import me.ash.reader.ui.page.settings.SettingItem
|
||||
import me.ash.reader.ui.theme.palette.onLight
|
||||
|
||||
|
@ -28,6 +52,7 @@ fun ReadingImagePage(
|
|||
val roundedCorners = LocalReadingImageRoundedCorners.current
|
||||
val horizontalPadding = LocalReadingImageHorizontalPadding.current
|
||||
val maximize = LocalReadingImageMaximize.current
|
||||
val renderer = LocalReadingRenderer.current
|
||||
|
||||
var roundedCornersDialogVisible by remember { mutableStateOf(false) }
|
||||
var horizontalPaddingDialogVisible by remember { mutableStateOf(false) }
|
||||
|
@ -94,11 +119,13 @@ fun ReadingImagePage(
|
|||
onClick = { roundedCornersDialogVisible = true },
|
||||
) {}
|
||||
SettingItem(
|
||||
enabled = renderer == ReadingRendererPreference.NativeComponent,
|
||||
title = stringResource(R.string.horizontal_padding),
|
||||
desc = "${horizontalPadding}dp",
|
||||
onClick = { horizontalPaddingDialogVisible = true },
|
||||
) {}
|
||||
SettingItem(
|
||||
enabled = renderer == ReadingRendererPreference.NativeComponent,
|
||||
title = stringResource(R.string.maximize),
|
||||
onClick = {
|
||||
(!maximize).put(context, scope)
|
||||
|
|
|
@ -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
|
||||
|
@ -43,12 +42,15 @@ import androidx.navigation.NavHostController
|
|||
import me.ash.reader.R
|
||||
import me.ash.reader.infrastructure.preference.LocalPullToSwitchArticle
|
||||
import me.ash.reader.infrastructure.preference.LocalReadingAutoHideToolbar
|
||||
import me.ash.reader.infrastructure.preference.LocalReadingBionicReading
|
||||
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
|
||||
|
@ -80,9 +82,11 @@ fun ReadingStylePage(
|
|||
val fonts = LocalReadingFonts.current
|
||||
val autoHideToolbar = LocalReadingAutoHideToolbar.current
|
||||
val pullToSwitchArticle = LocalPullToSwitchArticle.current
|
||||
|
||||
val renderer = LocalReadingRenderer.current
|
||||
val bionicReading = LocalReadingBionicReading.current
|
||||
|
||||
var tonalElevationDialogVisible by remember { mutableStateOf(false) }
|
||||
var rendererDialogVisible by remember { mutableStateOf(false) }
|
||||
var fontsDialogVisible by remember { mutableStateOf(false) }
|
||||
|
||||
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
|
||||
|
@ -152,6 +156,32 @@ 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.bionic_reading),
|
||||
separatedActions = renderer == ReadingRendererPreference.WebView,
|
||||
enabled = renderer == ReadingRendererPreference.WebView,
|
||||
desc = if (renderer == ReadingRendererPreference.WebView) stringResource(R.string.bionic_reading_domain)
|
||||
else stringResource(R.string.only_available_on_webview),
|
||||
onClick = {
|
||||
navController.navigate(RouteName.READING_BIONIC_READING) {
|
||||
launchSingleTop = true
|
||||
}
|
||||
},
|
||||
) {
|
||||
if (renderer == ReadingRendererPreference.WebView) {
|
||||
RYSwitch(
|
||||
enable = renderer == ReadingRendererPreference.WebView,
|
||||
activated = bionicReading.value,
|
||||
) {
|
||||
(!bionicReading).put(context, scope)
|
||||
}
|
||||
}
|
||||
}
|
||||
SettingItem(
|
||||
title = stringResource(R.string.reading_fonts),
|
||||
desc = fonts.toDesc(context),
|
||||
|
@ -173,21 +203,6 @@ fun ReadingStylePage(
|
|||
darkThemeNot.put(context, scope)
|
||||
}
|
||||
}
|
||||
SettingItem(
|
||||
title = stringResource(R.string.bionic_reading),
|
||||
separatedActions = true,
|
||||
enabled = false,
|
||||
onClick = {
|
||||
// (!articleListDesc).put(context, scope)
|
||||
},
|
||||
) {
|
||||
RYSwitch(
|
||||
activated = false,
|
||||
enable = false,
|
||||
) {
|
||||
// (!articleListDesc).put(context, scope)
|
||||
}
|
||||
}
|
||||
SettingItem(
|
||||
title = stringResource(R.string.auto_hide_toolbars),
|
||||
onClick = {
|
||||
|
@ -291,6 +306,21 @@ fun ReadingStylePage(
|
|||
tonalElevationDialogVisible = 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
|
||||
}
|
||||
|
||||
RadioDialog(
|
||||
visible = fontsDialogVisible,
|
||||
title = stringResource(R.string.reading_fonts),
|
||||
|
|
|
@ -317,8 +317,8 @@
|
|||
<string name="images">Images</string>
|
||||
<string name="rounded_corners">Rounded corners</string>
|
||||
<string name="videos">Videos</string>
|
||||
<string name="align_start">Align left</string>
|
||||
<string name="align_end">Align right</string>
|
||||
<string name="align_start">Align start</string>
|
||||
<string name="align_end">Align end</string>
|
||||
<string name="center_text">Center text</string>
|
||||
<string name="justify">Justify</string>
|
||||
<string name="external_fonts">External fonts</string>
|
||||
|
@ -445,4 +445,16 @@
|
|||
<string name="import_from_json">Import from JSON</string>
|
||||
<string name="export_as_json">Export as JSON</string>
|
||||
<string name="invalid_json_file_warning">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?</string>
|
||||
<string name="webview" translatable="false">WebView</string>
|
||||
<string name="native_component">Native Component</string>
|
||||
<string name="content_renderer">Content renderer</string>
|
||||
<string name="read_aloud">Read Aloud</string>
|
||||
<string name="only_available_on_webview">Only available on the WebView</string>
|
||||
<string name="bionic_reading_preview" translatable="false"><![CDATA[<p>With Bionic Reading you read texts faster, better and more focused.<p>]]></string>
|
||||
<string name="use_bionic_reading">Use Bionic Reading</string>
|
||||
<string name="about">About</string>
|
||||
<string name="bionic_reading_tips">What is Bionic Reading?</string>
|
||||
<string name="browse_bionic_reading_tips">Learn more at <i><u>bionic-reading.com</u></i>.</string>
|
||||
<string name="bionic_reading_domain" translatable="false">bionic-reading.com</string>
|
||||
<string name="bionic_reading_link" translatable="false">https://bionic-reading.com</string>
|
||||
</resources>
|
||||
|
|
Loading…
Reference in New Issue