[优化] 订阅界面

This commit is contained in:
HapeLee
2026-04-26 15:13:51 +08:00
parent 7e27686fbf
commit e59a6b8ec0
2 changed files with 66 additions and 130 deletions
@@ -6,7 +6,6 @@ import android.annotation.SuppressLint
import android.net.Uri import android.net.Uri
import android.net.http.SslError import android.net.http.SslError
import android.os.SystemClock import android.os.SystemClock
import android.view.MotionEvent
import android.view.View import android.view.View
import android.webkit.JavascriptInterface import android.webkit.JavascriptInterface
import android.webkit.SslErrorHandler import android.webkit.SslErrorHandler
@@ -19,20 +18,14 @@ import android.webkit.WebViewClient
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.activity.compose.LocalActivity import androidx.activity.compose.LocalActivity
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.compose.animation.animateContentSize import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column 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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding 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.foundation.clickable
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.Login import androidx.compose.material.icons.automirrored.filled.Login
import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.CleaningServices import androidx.compose.material.icons.filled.CleaningServices
@@ -44,17 +37,18 @@ import androidx.compose.material.icons.filled.Star
import androidx.compose.material.icons.filled.Stop import androidx.compose.material.icons.filled.Stop
import androidx.compose.material.icons.filled.VolumeUp import androidx.compose.material.icons.filled.VolumeUp
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@@ -74,20 +68,16 @@ import io.legado.app.model.Download
import io.legado.app.ui.association.OnLineImportActivity import io.legado.app.ui.association.OnLineImportActivity
import io.legado.app.ui.config.otherConfig.OtherConfig import io.legado.app.ui.config.otherConfig.OtherConfig
import io.legado.app.ui.login.SourceLoginActivity 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.AppTextField
import io.legado.app.ui.widget.components.button.MediumIconButton 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.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.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.button.TopBarActionButton
import io.legado.app.ui.widget.components.list.TopFloatingStickyItem import io.legado.app.ui.widget.components.button.TopBarNavigationButton
import io.legado.app.ui.widget.components.menuItem.MenuItemIcon 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.RoundDropdownMenu
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem 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.modalBottomSheet.AppModalBottomSheet
import io.legado.app.ui.widget.components.text.AppText
import io.legado.app.utils.NetworkUtils import io.legado.app.utils.NetworkUtils
import io.legado.app.utils.isTrue import io.legado.app.utils.isTrue
import io.legado.app.utils.keepScreenOn import io.legado.app.utils.keepScreenOn
@@ -97,9 +87,6 @@ import io.legado.app.utils.setDarkeningAllowed
import io.legado.app.utils.share import io.legado.app.utils.share
import io.legado.app.utils.startActivity import io.legado.app.utils.startActivity
import io.legado.app.utils.toastOnUi 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.apache.commons.text.StringEscapeUtils
import org.jsoup.Jsoup import org.jsoup.Jsoup
import java.net.URLDecoder import java.net.URLDecoder
@@ -118,8 +105,7 @@ fun RssReadRouteScreen(
) { ) {
val context = LocalContext.current val context = LocalContext.current
val activity = LocalActivity.current val activity = LocalActivity.current
val appCompatActivity = activity as? androidx.appcompat.app.AppCompatActivity val appCompatActivity = activity as? AppCompatActivity
val scope = rememberCoroutineScope()
val defaultTopBarTitle = stringResource(R.string.rss) val defaultTopBarTitle = stringResource(R.string.rss)
var pageTitle by remember(title, defaultTopBarTitle) { var pageTitle by remember(title, defaultTopBarTitle) {
@@ -128,9 +114,6 @@ fun RssReadRouteScreen(
var webProgress by remember { mutableIntStateOf(100) } var webProgress by remember { mutableIntStateOf(100) }
var showMenu by remember { mutableStateOf(false) } var showMenu by remember { mutableStateOf(false) }
var showRedirectMenu 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) } var webView by remember { mutableStateOf<WebView?>(null) }
var redirectPolicy by remember { mutableStateOf(RedirectPolicy.ALLOW_ALL) } var redirectPolicy by remember { mutableStateOf(RedirectPolicy.ALLOW_ALL) }
@@ -189,102 +172,26 @@ fun RssReadRouteScreen(
} }
} }
Box(modifier = Modifier.fillMaxSize()) { Scaffold(
VisibleWebViewCompose( topBar = {
modifier = Modifier.fillMaxSize(), TopAppBar(
onCreated = { createdWebView -> title = {
webView = createdWebView Text(
configureRssReadWebView( text = pageTitle.ifBlank { defaultTopBarTitle },
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 }
}
)
)
createdWebView.setOnTouchListener { _, event ->
when (event.actionMasked) {
MotionEvent.ACTION_DOWN,
MotionEvent.ACTION_MOVE -> {
showFloatingCardJob?.cancel()
showFloatingCard = false
}
MotionEvent.ACTION_UP,
MotionEvent.ACTION_CANCEL -> {
createdWebView.performClick()
showFloatingCardJob?.cancel()
showFloatingCardJob = scope.launch {
delay(180L)
showFloatingCard = true
}
}
}
false
}
}
)
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, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis
modifier = Modifier.padding(horizontal = 12.dp)
) )
summary.progressText?.let { text -> },
AppText(text = text, style = LegadoTheme.typography.labelMediumEmphasized) navigationIcon = {
Spacer(modifier = Modifier.width(6.dp)) TopBarNavigationButton(onClick = onBackClick)
} },
SmallTonalIconButton( actions = {
TopBarActionButton(
imageVector = Icons.Default.Refresh, imageVector = Icons.Default.Refresh,
contentDescription = stringResource(R.string.refresh), contentDescription = stringResource(R.string.refresh),
onClick = { viewModel.refresh { webView?.reload() } } onClick = { viewModel.refresh { webView?.reload() } }
) )
Spacer(modifier = Modifier.width(8.dp)) TopBarActionButton(
SmallTonalIconButton(
imageVector = Icons.Default.Star, imageVector = Icons.Default.Star,
contentDescription = stringResource(R.string.favorite), contentDescription = stringResource(R.string.favorite),
onClick = { onClick = {
@@ -294,9 +201,8 @@ fun RssReadRouteScreen(
showFavoriteSheet = true showFavoriteSheet = true
} }
) )
Spacer(modifier = Modifier.width(8.dp))
Box { Box {
SmallTonalIconButton( TopBarActionButton(
imageVector = Icons.Default.MoreVert, imageVector = Icons.Default.MoreVert,
contentDescription = "Menu", contentDescription = "Menu",
onClick = { showMenu = true } onClick = { showMenu = true }
@@ -391,6 +297,40 @@ fun RssReadRouteScreen(
} }
} }
} }
)
}
) { paddingValues ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
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()
)
} }
} }
} }
@@ -413,11 +353,6 @@ fun RssReadRouteScreen(
) )
} }
private data class RssReadFloatingSummary(
val titleText: String,
val progressText: String?,
)
@Composable @Composable
private fun FavoriteEditSheet( private fun FavoriteEditSheet(
show: Boolean, show: Boolean,
@@ -11,6 +11,7 @@ import android.webkit.WebView
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import io.legado.app.R import io.legado.app.R
@@ -47,10 +48,6 @@ class VisibleWebView(
evaluateJavascript(js, null) evaluateJavascript(js, null)
} }
override fun onWindowVisibilityChanged(visibility: Int) {
super.onWindowVisibilityChanged(VISIBLE)
}
override fun performClick(): Boolean { override fun performClick(): Boolean {
return super.performClick() return super.performClick()
} }
@@ -143,24 +140,28 @@ fun VisibleWebViewCompose(
onCreated: (VisibleWebView) -> Unit, onCreated: (VisibleWebView) -> Unit,
onDestroyed: (() -> Unit)? = null onDestroyed: (() -> Unit)? = null
) { ) {
var webViewRef: VisibleWebView? = null val webViewHolder = remember { WebViewHolder() }
AndroidView( AndroidView(
modifier = modifier, modifier = modifier,
factory = { context -> factory = { context ->
VisibleWebView(context).also { VisibleWebView(context).also {
webViewRef = it webViewHolder.webView = it
onCreated(it) onCreated(it)
} }
}, },
update = { update = {
webViewRef = it webViewHolder.webView = it
} }
) )
DisposableEffect(Unit) { DisposableEffect(Unit) {
onDispose { onDispose {
onDestroyed?.invoke() onDestroyed?.invoke()
webViewRef?.destroy() webViewHolder.webView?.destroy()
webViewRef = null webViewHolder.webView = null
} }
} }
} }
private class WebViewHolder {
var webView: VisibleWebView? = null
}