diff --git a/app/src/main/java/io/legado/app/data/entities/BaseSource.kt b/app/src/main/java/io/legado/app/data/entities/BaseSource.kt index 7ddbd4955..42e551ca8 100644 --- a/app/src/main/java/io/legado/app/data/entities/BaseSource.kt +++ b/app/src/main/java/io/legado/app/data/entities/BaseSource.kt @@ -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("", true) -> evalJS( it.substring(4, it.lastIndexOf("<")) - )?.toString().orEmpty() + ).toString() else -> it } - if (json.isBlank()) { - return@let - } GSONStrict.fromJsonObject>(json).getOrNull()?.let { map -> putAll(map) } ?: GSON.fromJsonObject>(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) } -} +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesCompose.kt b/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesCompose.kt index bbc7eabdc..8d477e57e 100644 --- a/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesCompose.kt +++ b/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesCompose.kt @@ -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 ) } } diff --git a/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesViewModel.kt b/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesViewModel.kt index c30ae8672..f1824b2f1 100644 --- a/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesViewModel.kt @@ -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 } diff --git a/app/src/main/java/io/legado/app/ui/rss/article/RssSortScreen.kt b/app/src/main/java/io/legado/app/ui/rss/article/RssSortScreen.kt index e1a4dcf4e..2a27231c5 100644 --- a/app/src/main/java/io/legado/app/ui/rss/article/RssSortScreen.kt +++ b/app/src/main/java/io/legado/app/ui/rss/article/RssSortScreen.kt @@ -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) diff --git a/app/src/main/java/io/legado/app/ui/rss/read/RssReadRouteScreen.kt b/app/src/main/java/io/legado/app/ui/rss/read/RssReadRouteScreen.kt index 1cb7b8dae..292386f11 100644 --- a/app/src/main/java/io/legado/app/ui/rss/read/RssReadRouteScreen.kt +++ b/app/src/main/java/io/legado/app/ui/rss/read/RssReadRouteScreen.kt @@ -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(null) } var webView by remember { mutableStateOf(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 { - 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 { + 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, diff --git a/app/src/main/java/io/legado/app/ui/rss/read/VisibleWebView.kt b/app/src/main/java/io/legado/app/ui/rss/read/VisibleWebView.kt index f1af32d19..6c1afb55f 100644 --- a/app/src/main/java/io/legado/app/ui/rss/read/VisibleWebView.kt +++ b/app/src/main/java/io/legado/app/ui/rss/read/VisibleWebView.kt @@ -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)) }