[优化] 重写订阅阅读页面,可能有bug喵

This commit is contained in:
HapeLee
2026-04-20 13:13:21 +08:00
parent ca70dcaec9
commit c7147e6a3d
6 changed files with 383 additions and 303 deletions
@@ -105,16 +105,13 @@ interface BaseSource : JsExtensions {
header?.let {
try {
val json = when {
it.startsWith("@js:", true) -> evalJS(it.substring(4))?.toString().orEmpty()
it.startsWith("@js:", true) -> evalJS(it.substring(4)).toString()
it.startsWith("<js>", true) -> evalJS(
it.substring(4, it.lastIndexOf("<"))
)?.toString().orEmpty()
).toString()
else -> it
}
if (json.isBlank()) {
return@let
}
GSONStrict.fromJsonObject<Map<String, String>>(json).getOrNull()?.let { map ->
putAll(map)
} ?: GSON.fromJsonObject<Map<String, String>>(json).getOrNull()?.let { map ->
@@ -122,12 +119,7 @@ interface BaseSource : JsExtensions {
putAll(map)
}
} catch (e: Exception) {
val isEmptyJsonNoise = e.message?.contains("Empty JSON string", ignoreCase = true) == true ||
e.toString().contains("Empty JSON string", ignoreCase = true) ||
e.stackTraceToString().contains("Empty JSON string", ignoreCase = true)
if (!isEmptyJsonNoise) {
AppLog.put("执行请求头规则出错\n$e", e)
}
AppLog.put("执行请求头规则出错\n$e", e)
}
}
if (!has(AppConst.UA_NAME, true)) {
@@ -349,4 +341,4 @@ interface BaseSource : JsExtensions {
}
return RhinoScriptEngine.eval(jsStr, scope)
}
}
}
@@ -59,6 +59,7 @@ import io.legado.app.data.appDb
import io.legado.app.data.entities.RssArticle
import io.legado.app.data.entities.RssSource
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.widget.components.EmptyMessage
import io.legado.app.ui.widget.components.card.GlassCard
import io.legado.app.ui.widget.components.cover.buildCoverImageRequest
import io.legado.app.utils.toastOnUi
@@ -295,21 +296,21 @@ private fun LoadMoreFooter(
state.errorMessage != null -> "加载失败,点击重试"
else -> "上拉加载更多"
}
val contentModifier = if (state.errorMessage != null) {
Modifier.clickable(onClick = onRetry)
} else {
Modifier
}
Box(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 12.dp),
contentAlignment = Alignment.Center
) {
Text(
text = text,
style = LegadoTheme.typography.bodySmall,
color = LegadoTheme.colorScheme.onSurfaceVariant,
modifier = if (state.errorMessage != null) {
Modifier.clickable(onClick = onRetry)
} else {
Modifier
}
EmptyMessage(
message = text,
isLoading = state.isLoadingMore,
modifier = contentModifier
)
}
}
@@ -102,6 +102,7 @@ class RssArticlesViewModel(application: Application) : BaseViewModel(application
loadMoreSuccess(it.first)
}.onError {
AppLog.put("rss获取内容失败", it)
if (page > 1) page--
_loadState.value = _loadState.value.copy(
isLoadingMore = false,
errorMessage = it.stackTraceStr
@@ -138,7 +139,8 @@ class RssArticlesViewModel(application: Application) : BaseViewModel(application
_loadState.value = _loadState.value.copy(
isLoadingMore = false,
hasMore = !nextPageUrl.isNullOrBlank()
hasMore = !nextPageUrl.isNullOrBlank(),
errorMessage = null
)
}
}
@@ -150,5 +152,5 @@ data class RssArticlesLoadState(
val errorMessage: String? = null
) {
val canLoadMore: Boolean
get() = hasMore && !isRefreshing && !isLoadingMore
get() = hasMore && !isRefreshing && !isLoadingMore && errorMessage == null
}
@@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.lazy.LazyColumn
@@ -48,12 +49,14 @@ import io.legado.app.ui.rss.read.RedirectPolicy
import io.legado.app.ui.rss.read.title
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.adaptiveHorizontalPaddingTab
import io.legado.app.ui.widget.components.AppScaffold
import io.legado.app.ui.widget.components.EmptyMessage
import io.legado.app.ui.widget.components.button.SmallIconButton
import io.legado.app.ui.widget.components.button.SmallOutlinedIconToggleButton
import io.legado.app.ui.widget.components.button.TopBarActionButton
import io.legado.app.ui.widget.components.button.TopBarNavigationButton
import io.legado.app.ui.widget.components.card.GlassCard
import io.legado.app.ui.widget.components.icon.AppIcons
import io.legado.app.ui.widget.components.menuItem.MenuItemIcon
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
@@ -119,172 +122,178 @@ fun RssSortScreen(
}
}
Column(modifier = Modifier.fillMaxSize()) {
GlassMediumFlexibleTopAppBar(
title = title,
scrollBehavior = scrollBehavior,
navigationIcon = {
TopBarNavigationButton(onClick = onBackClick, imageVector = Icons.AutoMirrored.Filled.ArrowBack)
},
actions = {
TopBarActionButton(
onClick = { showMainMenu = true },
imageVector = Icons.Default.MoreVert,
contentDescription = "Menu"
)
RoundDropdownMenu(
expanded = showMainMenu,
onDismissRequest = { showMainMenu = false }
) { dismiss ->
if (hasLogin) {
AppScaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
GlassMediumFlexibleTopAppBar(
title = title,
scrollBehavior = scrollBehavior,
navigationIcon = {
TopBarNavigationButton(onClick = onBackClick, imageVector = AppIcons.Back)
},
actions = {
TopBarActionButton(
onClick = { showMainMenu = true },
imageVector = AppIcons.MoreVert,
contentDescription = "Menu"
)
RoundDropdownMenu(
expanded = showMainMenu,
onDismissRequest = { showMainMenu = false }
) { dismiss ->
if (hasLogin) {
RoundDropdownMenuItem(
text = stringResource(R.string.login),
leadingIcon = { MenuItemIcon(Icons.AutoMirrored.Filled.Login) },
onClick = {
dismiss()
onLogin()
}
)
}
RoundDropdownMenuItem(
text = stringResource(R.string.login),
leadingIcon = { MenuItemIcon(Icons.AutoMirrored.Filled.Login) },
text = stringResource(R.string.refresh_sort),
leadingIcon = { MenuItemIcon(Icons.Default.Refresh) },
onClick = {
dismiss()
onLogin()
onRefreshSort()
}
)
}
RoundDropdownMenuItem(
text = stringResource(R.string.refresh_sort),
leadingIcon = { MenuItemIcon(Icons.Default.Refresh) },
onClick = {
dismiss()
onRefreshSort()
}
)
RoundDropdownMenuItem(
text = stringResource(R.string.set_source_variable),
leadingIcon = { MenuItemIcon(Icons.Default.Dataset) },
onClick = {
dismiss()
onSetSourceVariable()
}
)
RoundDropdownMenuItem(
text = stringResource(R.string.edit_source),
leadingIcon = { MenuItemIcon(Icons.Default.Edit) },
onClick = {
dismiss()
onEditSource()
}
)
RoundDropdownMenuItem(
text = stringResource(R.string.switchLayout),
leadingIcon = { MenuItemIcon(Icons.Default.Style) },
onClick = {
dismiss()
onSwitchLayout()
}
)
RoundDropdownMenuItem(
text = stringResource(R.string.read_record),
leadingIcon = { MenuItemIcon(Icons.Default.History) },
onClick = {
dismiss()
onReadRecord()
}
)
RoundDropdownMenuItem(
text = stringResource(R.string.clear),
leadingIcon = { MenuItemIcon(Icons.Default.CleaningServices) },
onClick = {
dismiss()
onClearArticles()
}
)
RoundDropdownMenuItem(
text = stringResource(R.string.redirect_policy),
onClick = { showRedirectMenu = true }
)
}
RoundDropdownMenu(
expanded = showRedirectMenu,
onDismissRequest = { showRedirectMenu = false },
modifier = Modifier.width(280.dp)
) { dismiss ->
RedirectPolicy.entries.forEach { policy ->
RoundDropdownMenuItem(
text = policy.title(),
isSelected = policy == redirectPolicy,
text = stringResource(R.string.set_source_variable),
leadingIcon = { MenuItemIcon(Icons.Default.Dataset) },
onClick = {
dismiss()
showMainMenu = false
onRedirectPolicyChanged(policy)
onSetSourceVariable()
}
)
}
}
},
bottomContent = {
if (sortList.size > 1) {
Row(
modifier = Modifier
.fillMaxWidth()
.adaptiveHorizontalPaddingTab(),
verticalAlignment = Alignment.CenterVertically
) {
AppTabRow(
tabTitles = tabTitles,
selectedTabIndex = selectedTabIndex,
onTabSelected = { index ->
scope.launch { pagerState.animateScrollToPage(index) }
},
modifier = Modifier.weight(1f)
RoundDropdownMenuItem(
text = stringResource(R.string.edit_source),
leadingIcon = { MenuItemIcon(Icons.Default.Edit) },
onClick = {
dismiss()
onEditSource()
}
)
RoundDropdownMenuItem(
text = stringResource(R.string.switchLayout),
leadingIcon = { MenuItemIcon(Icons.Default.Style) },
onClick = {
dismiss()
onSwitchLayout()
}
)
RoundDropdownMenuItem(
text = stringResource(R.string.read_record),
leadingIcon = { MenuItemIcon(Icons.Default.History) },
onClick = {
dismiss()
onReadRecord()
}
)
RoundDropdownMenuItem(
text = stringResource(R.string.clear),
leadingIcon = { MenuItemIcon(Icons.Default.CleaningServices) },
onClick = {
dismiss()
onClearArticles()
}
)
RoundDropdownMenuItem(
text = stringResource(R.string.redirect_policy),
onClick = { showRedirectMenu = true }
)
}
if (BookshelfConfig.shouldShowExpandButton) {
Box {
SmallOutlinedIconToggleButton(
checked = showGroupMenu,
onCheckedChange = { showGroupMenu = it },
imageVector = Icons.AutoMirrored.Filled.FormatListBulleted,
contentDescription = stringResource(R.string.group_manage)
)
RoundDropdownMenu(
expanded = showGroupMenu,
onDismissRequest = { showGroupMenu = false }
) { dismiss ->
sortList.forEachIndexed { index, group ->
RoundDropdownMenuItem(
text = group.first,
onClick = {
scope.launch { pagerState.animateScrollToPage(index) }
dismiss()
},
trailingIcon = {
if (selectedTabIndex == index) {
Icon(
Icons.Default.Check,
null,
modifier = Modifier.size(18.dp)
)
RoundDropdownMenu(
expanded = showRedirectMenu,
onDismissRequest = { showRedirectMenu = false },
modifier = Modifier.width(280.dp)
) { dismiss ->
RedirectPolicy.entries.forEach { policy ->
RoundDropdownMenuItem(
text = policy.title(),
isSelected = policy == redirectPolicy,
onClick = {
dismiss()
showMainMenu = false
onRedirectPolicyChanged(policy)
}
)
}
}
},
bottomContent = {
if (sortList.size > 1) {
Row(
modifier = Modifier
.fillMaxWidth()
.adaptiveHorizontalPaddingTab(),
verticalAlignment = Alignment.CenterVertically
) {
AppTabRow(
tabTitles = tabTitles,
selectedTabIndex = selectedTabIndex,
onTabSelected = { index ->
scope.launch { pagerState.animateScrollToPage(index) }
},
modifier = Modifier.weight(1f)
)
if (BookshelfConfig.shouldShowExpandButton) {
Box {
SmallOutlinedIconToggleButton(
checked = showGroupMenu,
onCheckedChange = { showGroupMenu = it },
imageVector = Icons.AutoMirrored.Filled.FormatListBulleted,
contentDescription = stringResource(R.string.group_manage)
)
RoundDropdownMenu(
expanded = showGroupMenu,
onDismissRequest = { showGroupMenu = false }
) { dismiss ->
sortList.forEachIndexed { index, group ->
RoundDropdownMenuItem(
text = group.first,
onClick = {
scope.launch { pagerState.animateScrollToPage(index) }
dismiss()
},
trailingIcon = {
if (selectedTabIndex == index) {
Icon(
Icons.Default.Check,
null,
modifier = Modifier.size(18.dp)
)
}
}
}
)
)
}
}
}
}
}
}
}
}
)
)
}
) { paddingValues ->
if (sortList.isEmpty()) {
EmptyMessage(
message = stringResource(R.string.empty),
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(24.dp)
)
} else {
HorizontalPager(
state = pagerState,
contentPadding = PaddingValues(bottom = 0.dp),
modifier = Modifier.fillMaxSize()
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) { page ->
val item = sortList.getOrNull(page) ?: return@HorizontalPager
pagerContent(page, item)
@@ -6,6 +6,7 @@ import android.annotation.SuppressLint
import android.net.Uri
import android.net.http.SslError
import android.os.SystemClock
import android.view.MotionEvent
import android.view.View
import android.webkit.JavascriptInterface
import android.webkit.SslErrorHandler
@@ -18,11 +19,18 @@ import android.webkit.WebViewClient
import androidx.activity.compose.BackHandler
import androidx.activity.compose.LocalActivity
import androidx.appcompat.app.AlertDialog
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.clickable
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.Login
@@ -46,9 +54,11 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
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.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -67,16 +77,18 @@ import io.legado.app.ui.config.otherConfig.OtherConfig
import io.legado.app.ui.login.SourceLoginActivity
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.widget.components.AppTextField
import io.legado.app.ui.widget.components.button.SmallIconButton
import io.legado.app.ui.widget.components.button.TopBarActionButton
import io.legado.app.ui.widget.components.button.TopBarNavigationButton
import io.legado.app.ui.widget.components.button.MediumIconButton
import io.legado.app.ui.widget.components.card.GlassCard
import io.legado.app.ui.widget.components.card.NormalCard
import io.legado.app.ui.widget.components.button.SmallIconButton
import io.legado.app.ui.widget.components.button.SmallTonalIconButton
import io.legado.app.ui.widget.components.button.TopBarActionButton
import io.legado.app.ui.widget.components.list.TopFloatingStickyItem
import io.legado.app.ui.widget.components.menuItem.MenuItemIcon
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
import io.legado.app.ui.widget.components.text.AppText
import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
import io.legado.app.utils.NetworkUtils
import io.legado.app.utils.isTrue
import io.legado.app.utils.keepScreenOn
@@ -86,6 +98,8 @@ import io.legado.app.utils.setDarkeningAllowed
import io.legado.app.utils.share
import io.legado.app.utils.startActivity
import io.legado.app.utils.toastOnUi
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.apache.commons.text.StringEscapeUtils
import org.jsoup.Jsoup
@@ -115,6 +129,8 @@ fun RssReadRouteScreen(
var webProgress by remember { mutableIntStateOf(100) }
var showMenu by remember { mutableStateOf(false) }
var showRedirectMenu by remember { mutableStateOf(false) }
var showFloatingCard by remember { mutableStateOf(true) }
var showFloatingCardJob by remember { mutableStateOf<Job?>(null) }
var webView by remember { mutableStateOf<WebView?>(null) }
@@ -174,157 +190,208 @@ fun RssReadRouteScreen(
}
}
Column(modifier = Modifier.fillMaxSize()) {
GlassMediumFlexibleTopAppBar(
title = pageTitle.ifBlank { defaultTopBarTitle },
navigationIcon = {
TopBarNavigationButton(
onClick = onBackClick,
imageVector = Icons.AutoMirrored.Filled.ArrowBack
)
},
scrollBehavior = null,
actions = {
TopBarActionButton(
onClick = { viewModel.refresh { webView?.reload() } },
imageVector = Icons.Default.Refresh,
contentDescription = stringResource(R.string.refresh)
)
TopBarActionButton(
onClick = {
viewModel.addFavorite()
favoriteTitle = viewModel.rssArticle?.title.orEmpty()
favoriteGroup = viewModel.rssArticle?.group.orEmpty()
showFavoriteSheet = true
},
imageVector = Icons.Default.Star,
contentDescription = stringResource(R.string.favorite)
)
TopBarActionButton(
onClick = { showMenu = true },
imageVector = Icons.Default.MoreVert,
contentDescription = "Menu"
)
RoundDropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false }
) { dismiss ->
RoundDropdownMenuItem(
text = stringResource(R.string.share),
leadingIcon = { MenuItemIcon(Icons.Default.Share) },
onClick = {
dismiss()
webView?.url?.let {
context.share(it)
} ?: viewModel.rssArticle?.let {
context.share(it.link)
} ?: context.toastOnUi(R.string.null_url)
Box(modifier = Modifier.fillMaxSize()) {
VisibleWebViewCompose(
modifier = Modifier.fillMaxSize(),
onCreated = { createdWebView ->
webView = createdWebView
configureRssReadWebView(
webView = createdWebView,
context = context,
activity = activity,
appCompatActivity = appCompatActivity,
viewModel = viewModel,
initialTitle = title,
redirectPolicyProvider = { redirectPolicy },
callbacks = RssReadWebControllerCallbacks(
onProgressChanged = { webProgress = it },
onPageTitleResolved = { resolved ->
pageTitle = resolved.ifBlank { defaultTopBarTitle }
}
)
RoundDropdownMenuItem(
text = if (isSpeaking) stringResource(R.string.aloud_stop) else stringResource(
R.string.read_aloud
),
leadingIcon = {
MenuItemIcon(if (isSpeaking) Icons.Default.Stop else Icons.Default.VolumeUp)
},
onClick = {
dismiss()
if (isSpeaking) {
viewModel.stopReadAloud()
} else {
webView?.evaluateJavascript("document.documentElement.outerHTML") {
val html = StringEscapeUtils.unescapeJson(it)
.replace("^\"|\"$".toRegex(), "")
viewModel.readAloud(
Jsoup.parse(html)
.text()
)
}
}
)
createdWebView.setOnTouchListener { _, event ->
when (event.actionMasked) {
MotionEvent.ACTION_DOWN,
MotionEvent.ACTION_MOVE -> {
showFloatingCardJob?.cancel()
showFloatingCard = false
}
)
if (!viewModel.rssSource?.loginUrl.isNullOrBlank()) {
RoundDropdownMenuItem(
text = stringResource(R.string.login),
leadingIcon = { MenuItemIcon(Icons.AutoMirrored.Filled.Login) },
onClick = {
dismiss()
context.startActivity<SourceLoginActivity> {
putExtra("type", "rssSource")
putExtra("key", viewModel.rssSource?.sourceUrl)
}
}
)
}
RoundDropdownMenuItem(
text = stringResource(R.string.redirect_policy),
onClick = { showRedirectMenu = true }
)
RoundDropdownMenuItem(
text = stringResource(R.string.open_in_browser),
leadingIcon = { MenuItemIcon(Icons.Default.OpenInBrowser) },
onClick = {
dismiss()
webView?.url?.let { context.openUrl(it) } ?: context.toastOnUi("url null")
}
)
}
RoundDropdownMenu(
expanded = showRedirectMenu,
onDismissRequest = { showRedirectMenu = false }
) { dismiss ->
RedirectPolicy.entries.forEach { policy ->
RoundDropdownMenuItem(
text = policy.title(),
onClick = {
dismiss()
showMenu = false
viewModel.rssSource?.let { source ->
viewModel.updateRssSourceRedirectPolicy(source.sourceUrl, policy.name)
redirectPolicy = policy
}
context.toastOnUi("重定向策略已更新")
},
trailingIcon = {
if (policy == redirectPolicy) {
Icon(Icons.Default.Check, null)
}
MotionEvent.ACTION_UP,
MotionEvent.ACTION_CANCEL -> {
createdWebView.performClick()
showFloatingCardJob?.cancel()
showFloatingCardJob = scope.launch {
delay(180L)
showFloatingCard = true
}
)
}
}
false
}
}
)
Box(modifier = Modifier.fillMaxSize()) {
VisibleWebViewCompose(
modifier = Modifier.fillMaxSize(),
onCreated = { createdWebView ->
webView = createdWebView
configureRssReadWebView(
webView = createdWebView,
context = context,
activity = activity,
appCompatActivity = appCompatActivity,
viewModel = viewModel,
initialTitle = title,
redirectPolicyProvider = { redirectPolicy },
callbacks = RssReadWebControllerCallbacks(
onProgressChanged = { webProgress = it },
onPageTitleResolved = { resolved ->
pageTitle = resolved.ifBlank { defaultTopBarTitle }
}
)
)
}
if (webProgress in 0..99) {
LinearProgressIndicator(
progress = { webProgress / 100f },
modifier = Modifier.fillMaxWidth()
)
if (webProgress in 0..99) {
LinearProgressIndicator(
progress = { webProgress / 100f },
modifier = Modifier.fillMaxWidth()
}
TopFloatingStickyItem(
item = if (showFloatingCard) {
RssReadFloatingSummary(
titleText = pageTitle.ifBlank { defaultTopBarTitle },
progressText = if (webProgress in 0..99) " · ${webProgress}%" else null
)
} else {
null
},
modifier = Modifier
.align(Alignment.TopCenter)
.statusBarsPadding()
.padding(top = 6.dp),
) { summary ->
NormalCard(
modifier = Modifier.animateContentSize(),
cornerRadius = 32.dp,
containerColor = LegadoTheme.colorScheme.surfaceContainer,
contentColor = LegadoTheme.colorScheme.onCardContainer
) {
Row(
modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
SmallTonalIconButton(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(R.string.back),
onClick = onBackClick
)
Spacer(modifier = Modifier.width(8.dp))
AppText(
text = summary.titleText,
style = LegadoTheme.typography.labelSmallEmphasized,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.padding(horizontal = 12.dp)
)
summary.progressText?.let { text ->
AppText(text = text, style = LegadoTheme.typography.labelMediumEmphasized)
Spacer(modifier = Modifier.width(6.dp))
}
SmallTonalIconButton(
imageVector = Icons.Default.Refresh,
contentDescription = stringResource(R.string.refresh),
onClick = { viewModel.refresh { webView?.reload() } }
)
Spacer(modifier = Modifier.width(8.dp))
SmallTonalIconButton(
imageVector = Icons.Default.Star,
contentDescription = stringResource(R.string.favorite),
onClick = {
viewModel.addFavorite()
favoriteTitle = viewModel.rssArticle?.title.orEmpty()
favoriteGroup = viewModel.rssArticle?.group.orEmpty()
showFavoriteSheet = true
}
)
Spacer(modifier = Modifier.width(8.dp))
Box {
SmallTonalIconButton(
imageVector = Icons.Default.MoreVert,
contentDescription = "Menu",
onClick = { showMenu = true }
)
RoundDropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false }
) { dismiss ->
RoundDropdownMenuItem(
text = stringResource(R.string.share),
leadingIcon = { MenuItemIcon(Icons.Default.Share) },
onClick = {
dismiss()
webView?.url?.let {
context.share(it)
} ?: viewModel.rssArticle?.let {
context.share(it.link)
} ?: context.toastOnUi(R.string.null_url)
}
)
RoundDropdownMenuItem(
text = if (isSpeaking) stringResource(R.string.aloud_stop) else stringResource(
R.string.read_aloud
),
leadingIcon = {
MenuItemIcon(if (isSpeaking) Icons.Default.Stop else Icons.Default.VolumeUp)
},
onClick = {
dismiss()
if (isSpeaking) {
viewModel.stopReadAloud()
} else {
webView?.evaluateJavascript("document.documentElement.outerHTML") {
val html = StringEscapeUtils.unescapeJson(it)
.replace("^\"|\"$".toRegex(), "")
viewModel.readAloud(
Jsoup.parse(html).text()
)
}
}
}
)
if (!viewModel.rssSource?.loginUrl.isNullOrBlank()) {
RoundDropdownMenuItem(
text = stringResource(R.string.login),
leadingIcon = { MenuItemIcon(Icons.AutoMirrored.Filled.Login) },
onClick = {
dismiss()
context.startActivity<SourceLoginActivity> {
putExtra("type", "rssSource")
putExtra("key", viewModel.rssSource?.sourceUrl)
}
}
)
}
RoundDropdownMenuItem(
text = stringResource(R.string.redirect_policy),
onClick = { showRedirectMenu = true }
)
RoundDropdownMenuItem(
text = stringResource(R.string.open_in_browser),
leadingIcon = { MenuItemIcon(Icons.Default.OpenInBrowser) },
onClick = {
dismiss()
webView?.url?.let { context.openUrl(it) } ?: context.toastOnUi("url null")
}
)
}
RoundDropdownMenu(
expanded = showRedirectMenu,
onDismissRequest = { showRedirectMenu = false }
) { dismiss ->
RedirectPolicy.entries.forEach { policy ->
RoundDropdownMenuItem(
text = policy.title(),
onClick = {
dismiss()
showMenu = false
viewModel.rssSource?.let { source ->
viewModel.updateRssSourceRedirectPolicy(source.sourceUrl, policy.name)
redirectPolicy = policy
}
context.toastOnUi("重定向策略已更新")
},
trailingIcon = {
if (policy == redirectPolicy) {
Icon(Icons.Default.Check, null)
}
}
)
}
}
}
}
}
}
}
@@ -347,6 +414,11 @@ fun RssReadRouteScreen(
)
}
private data class RssReadFloatingSummary(
val titleText: String,
val progressText: String?,
)
@Composable
private fun FavoriteEditSheet(
show: Boolean,
@@ -51,6 +51,10 @@ class VisibleWebView(
super.onWindowVisibilityChanged(VISIBLE)
}
override fun performClick(): Boolean {
return super.performClick()
}
override fun startActionMode(callback: ActionMode.Callback?): ActionMode {
return super.startActionMode(createWrappedCallback(callback))
}