界面优化

This commit is contained in:
HapeLee
2026-05-17 04:08:40 +08:00
parent 139dd76f3e
commit 1ac5f6549b
12 changed files with 377 additions and 224 deletions
@@ -3,6 +3,9 @@ package io.legado.app.ui.book.manage
import androidx.activity.compose.BackHandler
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.tween
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@@ -22,10 +25,8 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.MenuOpen
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.Bookmarks
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Download
import androidx.compose.material.icons.filled.Info
@@ -38,18 +39,9 @@ import androidx.compose.material.icons.filled.SkipNext
import androidx.compose.material.icons.filled.Stop
import androidx.compose.material.icons.filled.Upload
import androidx.compose.material3.Checkbox
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FloatingActionButtonMenu
import androidx.compose.material3.FloatingActionButtonMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text
import androidx.compose.material3.ToggleFloatingActionButton
import androidx.compose.material3.ToggleFloatingActionButtonDefaults.animateIcon
import androidx.compose.material3.animateFloatingActionButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf
@@ -60,7 +52,6 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
@@ -81,13 +72,18 @@ import io.legado.app.ui.book.changesource.ChangeSourceMigrationOptionsSheet
import io.legado.app.ui.book.info.ChangeSourceSheet
import io.legado.app.ui.book.info.GroupSelectSheet
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.LegadoTheme.composeEngine
import io.legado.app.ui.theme.ThemeResolver
import io.legado.app.ui.theme.adaptiveContentPadding
import io.legado.app.ui.theme.adaptiveHorizontalPadding
import io.legado.app.ui.widget.components.AppFloatingActionButtonMenu
import io.legado.app.ui.widget.components.AppTextField
import io.legado.app.ui.widget.components.FabMenuItem
import io.legado.app.ui.widget.components.alert.AppAlertDialog
import io.legado.app.ui.widget.components.button.MediumIconButton
import io.legado.app.ui.widget.components.button.SmallTonalIconButton
import io.legado.app.ui.widget.components.button.SmallTonalTextButton
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.card.ReorderableSelectionItem
import io.legado.app.ui.widget.components.card.SelectionItemCard
@@ -119,11 +115,6 @@ import org.koin.androidx.compose.koinViewModel
import sh.calvin.reorderable.ReorderableItem
import sh.calvin.reorderable.rememberReorderableLazyListState
data class BookshelfManageFabAction(
val icon: androidx.compose.ui.graphics.vector.ImageVector,
val label: String,
val action: () -> Unit
)
private data class BookshelfManageListState(
override val items: List<Book> = emptyList(),
@@ -150,7 +141,6 @@ fun BookshelfManageRouteScreen(
)
}
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
private fun BookshelfManageScreen(
viewModel: BookshelfManageScreenViewModel,
@@ -390,20 +380,20 @@ private fun BookshelfManageScreen(
return if (targetBooks.all { it.group == firstGroup }) firstGroup else 0L
}
val fabItems = listOf(
BookshelfManageFabAction(
FabMenuItem(
Icons.Default.SelectAll,
stringResource(R.string.select_all)
) {
selectedBookUrls = filteredBooks.mapTo(hashSetOf()) { it.bookUrl }
},
BookshelfManageFabAction(
FabMenuItem(
Icons.Default.Refresh,
stringResource(R.string.revert_selection)
) {
val filteredUrls = filteredBooks.map { it.bookUrl }.toSet()
selectedBookUrls = (selectedBookUrls - filteredUrls) + (filteredUrls - selectedBookUrls)
},
BookshelfManageFabAction(
FabMenuItem(
Icons.Default.Download,
"缓存选中"
) {
@@ -411,7 +401,7 @@ private fun BookshelfManageScreen(
showBatchDownloadConfirmDialog = true
}
},
BookshelfManageFabAction(
FabMenuItem(
Icons.Default.Refresh,
"批量换源"
) {
@@ -419,7 +409,7 @@ private fun BookshelfManageScreen(
showBatchSourcePickerSheet = true
}
},
BookshelfManageFabAction(
FabMenuItem(
Icons.Default.Bookmarks,
stringResource(R.string.move_to_group)
) {
@@ -429,20 +419,20 @@ private fun BookshelfManageScreen(
showGroupSelectSheet = true
}
},
BookshelfManageFabAction(
FabMenuItem(
Icons.Default.Upload,
"导出选中"
) {
exportSelected()
},
BookshelfManageFabAction(
FabMenuItem(
Icons.Default.Delete,
stringResource(R.string.clear_cache)
) {
viewModel.dispatch(BookshelfManageScreenIntent.ClearCachesForBooks(selectedBookUrls))
clearSelection()
},
BookshelfManageFabAction(
FabMenuItem(
Icons.Default.Delete,
stringResource(R.string.delete)
) {
@@ -620,44 +610,14 @@ private fun BookshelfManageScreen(
)
},
floatingActionButton = {
FloatingActionButtonMenu(
AppFloatingActionButtonMenu(
modifier = Modifier.offset(x = 16.dp, y = 16.dp),
expanded = fabMenuExpanded,
button = {
ToggleFloatingActionButton(
modifier = Modifier
.animateFloatingActionButton(
visible = true,
alignment = Alignment.BottomEnd,
)
.focusRequester(focusRequester),
checked = fabMenuExpanded,
onCheckedChange = { fabMenuExpanded = !fabMenuExpanded },
) {
val imageVector by remember {
derivedStateOf {
if (checkedProgress > 0.5f) Icons.Filled.Close else Icons.AutoMirrored.Filled.MenuOpen
}
}
Icon(
imageVector = imageVector,
contentDescription = "Menu",
modifier = Modifier.animateIcon({ checkedProgress }),
)
}
}
) {
fabItems.forEach { (icon, label, action) ->
FloatingActionButtonMenuItem(
onClick = {
action()
fabMenuExpanded = false
},
icon = { Icon(icon, contentDescription = null) },
text = { Text(text = label) }
)
}
}
onExpandedChange = { fabMenuExpanded = it },
items = fabItems,
visible = true,
focusRequester = focusRequester
)
}
) { paddingValues ->
val renderVersion by rememberUpdatedState(state.cacheVersion)
@@ -681,6 +641,15 @@ private fun BookshelfManageScreen(
stringResource(R.string.cache_download_failed, it)
}
val isSelected = selectedBookUrls.contains(book.bookUrl)
val isMiuix = ThemeResolver.isMiuixEngine(composeEngine)
val animatedContainerColor by animateColorAsState(
targetValue = if (isSelected)
LegadoTheme.colorScheme.secondaryContainer
else
if (isMiuix) LegadoTheme.colorScheme.surfaceContainer else LegadoTheme.colorScheme.surfaceContainerLow,
animationSpec = tween(durationMillis = 200, easing = FastOutSlowInEasing),
label = "CardColor"
)
val exportMsg = remember(renderVersion, book.bookUrl) {
ExportBookService.exportMsg[book.bookUrl]
}
@@ -689,7 +658,7 @@ private fun BookshelfManageScreen(
key = book.bookUrl,
enabled = canReorderBooks
) {
NormalCard(
GlassCard(
modifier = Modifier
.fillMaxWidth()
.then(
@@ -701,11 +670,7 @@ private fun BookshelfManageScreen(
),
onClick = { toggleBookSelection(book) },
onLongClick = { toggleBookSelection(book) },
containerColor = if (isSelected) {
LegadoTheme.colorScheme.surfaceContainerHigh
} else {
LegadoTheme.colorScheme.surfaceContainerLow
}
containerColor = animatedContainerColor
) {
Column(
modifier = Modifier
@@ -36,10 +36,8 @@ import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.FormatListBulleted
import androidx.compose.material.icons.automirrored.filled.MenuOpen
import androidx.compose.material.icons.filled.BookmarkAdd
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Download
import androidx.compose.material.icons.filled.DownloadForOffline
import androidx.compose.material.icons.filled.ExpandMore
@@ -52,16 +50,10 @@ import androidx.compose.material.icons.outlined.DownloadForOffline
import androidx.compose.material.icons.rounded.LocationOn
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FloatingActionButtonMenu
import androidx.compose.material3.FloatingActionButtonMenuItem
import androidx.compose.material3.FloatingToolbarDefaults.ScreenOffset
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.ToggleFloatingActionButton
import androidx.compose.material3.ToggleFloatingActionButtonDefaults.animateIcon
import androidx.compose.material3.animateFloatingActionButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
@@ -76,7 +68,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
@@ -95,8 +86,10 @@ import io.legado.app.ui.theme.adaptiveContentPaddingOnlyVertical
import io.legado.app.ui.theme.adaptiveHorizontalPadding
import io.legado.app.ui.widget.CollapsibleHeader
import io.legado.app.ui.widget.components.ActionItem
import io.legado.app.ui.widget.components.AppFloatingActionButtonMenu
import io.legado.app.ui.widget.components.AppScaffold
import io.legado.app.ui.widget.components.EmptyMessage
import io.legado.app.ui.widget.components.FabMenuItem
import io.legado.app.ui.widget.components.SelectionBottomBar
import io.legado.app.ui.widget.components.bookmark.BookmarkEditSheet
import io.legado.app.ui.widget.components.bookmark.BookmarkItem
@@ -215,7 +208,7 @@ fun TocScreen(
val fabItems = remember(state.items) {
listOf(
FabAction(Icons.Default.LocationOn, "定位至当前阅读") {
FabMenuItem(Icons.Default.LocationOn, "定位至当前阅读") {
scope.launch {
val target = state.items.indexOfFirst { it.isDur }
if (target != -1) {
@@ -226,13 +219,13 @@ fun TocScreen(
}
}
},
FabAction(Icons.Default.VerticalAlignTop, "移至顶部") {
FabMenuItem(Icons.Default.VerticalAlignTop, "移至顶部") {
scope.launch { listState.animateScrollToItem(0) }
},
FabAction(Icons.Default.VerticalAlignBottom, "移至底部") {
FabMenuItem(Icons.Default.VerticalAlignBottom, "移至底部") {
scope.launch { listState.animateScrollToItem(state.items.size) }
},
FabAction(Icons.Default.DownloadForOffline, "下载全部") {
FabMenuItem(Icons.Default.DownloadForOffline, "下载全部") {
viewModel.downloadAll()
}
)
@@ -509,45 +502,15 @@ fun TocScreen(
)
},
floatingActionButton = {
FloatingActionButtonMenu(
AppFloatingActionButtonMenu(
modifier = Modifier
.offset(x = 16.dp, y = 16.dp),
expanded = fabMenuExpanded,
button = {
ToggleFloatingActionButton(
modifier = Modifier
.animateFloatingActionButton(
visible = shouldShowFab,
alignment = Alignment.BottomEnd,
)
.focusRequester(focusRequester),
checked = fabMenuExpanded,
onCheckedChange = { fabMenuExpanded = !fabMenuExpanded },
) {
val imageVector by remember {
derivedStateOf {
if (checkedProgress > 0.5f) Icons.Filled.Close else Icons.AutoMirrored.Filled.MenuOpen
}
}
Icon(
imageVector = imageVector,
contentDescription = "Menu",
modifier = Modifier.animateIcon({ checkedProgress }),
)
}
}
) {
fabItems.forEach { (icon, label, action) ->
FloatingActionButtonMenuItem(
onClick = {
action()
fabMenuExpanded = false
},
icon = { Icon(icon, contentDescription = null) },
text = { Text(text = label) }
)
}
}
onExpandedChange = { fabMenuExpanded = it },
items = fabItems,
visible = shouldShowFab,
focusRequester = focusRequester
)
}
) { padding ->
Box(modifier = Modifier.fillMaxSize()) {
@@ -4,7 +4,7 @@ import android.app.Application
import android.net.Uri
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import io.legado.app.base.BaseRuleViewModel
@@ -109,8 +109,6 @@ private data class TitleCacheKey(
val chapterCount: Int
)
data class FabAction(val icon: ImageVector, val label: String, val action: () -> Unit)
@OptIn(ExperimentalCoroutinesApi::class)
class TocViewModel(
application: Application,
@@ -219,7 +219,9 @@ fun RssFavoritesScreen(
path = rssStar.image,
sourceOrigin = rssStar.origin,
contentScale = ContentScale.Crop,
modifier = Modifier.size(54.dp)
modifier = Modifier
.padding(vertical = 12.dp)
.size(54.dp)
)
}
} else null,
@@ -3,24 +3,15 @@
package io.legado.app.ui.rss.read
import android.annotation.SuppressLint
import android.net.Uri
import android.net.http.SslError
import android.os.SystemClock
import android.view.View
import android.view.ViewGroup
import android.webkit.JavascriptInterface
import android.webkit.SslErrorHandler
import android.webkit.URLUtil
import android.webkit.WebChromeClient
import android.webkit.WebResourceRequest
import android.webkit.WebSettings
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.activity.compose.BackHandler
import androidx.activity.compose.LocalActivity
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
@@ -28,7 +19,6 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.clickable
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Login
import androidx.compose.material.icons.filled.Check
@@ -42,10 +32,6 @@ import androidx.compose.material.icons.filled.Stop
import androidx.compose.material.icons.filled.VolumeUp
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import io.legado.app.ui.widget.components.progressIndicator.AppLinearProgressIndicator
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
@@ -59,40 +45,30 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
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.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.zIndex
import androidx.core.net.toUri
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import com.script.rhino.runScriptWithContext
import io.legado.app.R
import io.legado.app.constant.AppConst
import io.legado.app.constant.AppLog
import io.legado.app.help.config.AppConfig
import io.legado.app.help.http.CookieManager
import io.legado.app.lib.dialogs.SelectItem
import io.legado.app.lib.dialogs.selector
import io.legado.app.model.Download
import io.legado.app.ui.association.OnLineImportActivity
import io.legado.app.ui.config.otherConfig.OtherConfig
import io.legado.app.ui.login.SourceLoginActivity
import io.legado.app.ui.widget.components.AppScaffold
import io.legado.app.ui.widget.components.AppTextField
import io.legado.app.ui.widget.components.button.ConfirmDismissButtonsRow
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.button.SmallIconButton
import io.legado.app.ui.widget.components.topbar.TopBarActionButton
import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
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.progressIndicator.AppLinearProgressIndicator
import io.legado.app.ui.widget.components.topbar.GlassTopAppBar
import io.legado.app.ui.widget.components.topbar.TopBarActionButton
import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
import io.legado.app.utils.NetworkUtils
import io.legado.app.utils.isTrue
import io.legado.app.utils.keepScreenOn
import io.legado.app.utils.longSnackbar
import io.legado.app.utils.openUrl
import io.legado.app.utils.setDarkeningAllowed
import io.legado.app.utils.share
@@ -101,7 +77,6 @@ import io.legado.app.utils.toastOnUi
import io.legado.app.utils.toggleSystemBar
import org.apache.commons.text.StringEscapeUtils
import org.jsoup.Jsoup
import java.net.URLDecoder
import org.koin.androidx.compose.koinViewModel
@SuppressLint("SetJavaScriptEnabled")
@@ -140,7 +115,6 @@ fun RssReadRouteScreen(
val content by viewModel.contentState.collectAsStateWithLifecycle()
val analyzeUrl by viewModel.urlState.collectAsStateWithLifecycle()
val rssStar by viewModel.rssStarState.collectAsStateWithLifecycle()
val isSpeaking by viewModel.isSpeakingState.collectAsStateWithLifecycle()
val fallbackUserAgent = OtherConfig.userAgent
@@ -175,19 +149,29 @@ fun RssReadRouteScreen(
val article = viewModel.rssArticle ?: return@LaunchedEffect
val url = NetworkUtils.getAbsoluteURL(article.origin, article.link)
val html = viewModel.clHtml(body)
if (viewModel.rssSource?.loadWithBaseUrl == true) {
currentWebView.loadDataWithBaseURL(url, html, "text/html", "utf-8", url)
} else {
currentWebView.loadDataWithBaseURL(null, html, "text/html;charset=utf-8", "utf-8", url)
if (currentWebView.url != url) {
if (viewModel.rssSource?.loadWithBaseUrl == true) {
currentWebView.loadDataWithBaseURL(url, html, "text/html", "utf-8", url)
} else {
currentWebView.loadDataWithBaseURL(
null,
html,
"text/html;charset=utf-8",
"utf-8",
url
)
}
}
}
LaunchedEffect(analyzeUrl, webView) {
val url = analyzeUrl ?: return@LaunchedEffect
val currentWebView = webView ?: return@LaunchedEffect
CookieManager.applyToWebView(url.url)
currentWebView.settings.userAgentString = url.getUserAgent()
currentWebView.loadUrl(url.url, url.headerMap)
if (currentWebView.url != url.url) {
CookieManager.applyToWebView(url.url)
currentWebView.settings.userAgentString = url.getUserAgent()
currentWebView.loadUrl(url.url, url.headerMap)
}
}
BackHandler {
@@ -205,17 +189,19 @@ fun RssReadRouteScreen(
}
}
val isNight = isSystemInDarkTheme()
LaunchedEffect(isNight, webView) {
val currentWebView = webView ?: return@LaunchedEffect
currentWebView.settings.setDarkeningAllowed(isNight)
}
Box(modifier = Modifier.fillMaxSize()) {
Scaffold(
AppScaffold(
disableHazeSource = true,
topBar = {
TopAppBar(
title = {
Text(
text = pageTitle.ifBlank { defaultTopBarTitle },
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
},
GlassTopAppBar(
title = pageTitle.ifBlank { defaultTopBarTitle },
navigationIcon = {
TopBarNavigationButton(onClick = onBackClick)
},
@@ -236,11 +222,11 @@ fun RssReadRouteScreen(
}
)
Box {
TopBarActionButton(
imageVector = Icons.Default.MoreVert,
contentDescription = "Menu",
onClick = { showMenu = true }
)
TopBarActionButton(
imageVector = Icons.Default.MoreVert,
contentDescription = "Menu",
onClick = { showMenu = true }
)
RoundDropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false }
@@ -256,7 +242,7 @@ fun RssReadRouteScreen(
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
@@ -276,21 +262,21 @@ fun RssReadRouteScreen(
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.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),
@@ -301,8 +287,9 @@ fun RssReadRouteScreen(
leadingIcon = { MenuItemIcon(Icons.Default.OpenInBrowser) },
onClick = {
dismiss()
webView?.url?.let { context.openUrl(it) } ?: context.toastOnUi("url null")
}
webView?.url?.let { context.openUrl(it) }
?: context.toastOnUi("url null")
}
)
}
RoundDropdownMenu(
@@ -310,27 +297,30 @@ fun RssReadRouteScreen(
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)
}
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)
}
}
)
}
}
}
}
)
}
) { paddingValues ->
@@ -369,18 +359,18 @@ fun RssReadRouteScreen(
}
},
onHideCustomView = { hideCustomView() }
)
)
}
)
}
)
if (webProgress in 0..99) {
AppLinearProgressIndicator(
progress = webProgress / 100f,
modifier = Modifier.fillMaxWidth()
)
}
}
}
}
customView?.let { view ->
key(view) {
@@ -29,6 +29,7 @@ class VisibleWebView(
private var lastSelectedText: String = ""
init {
setBackgroundColor(0)
settings.javaScriptEnabled = true
settings.domStorageEnabled = true
@@ -1,23 +1,55 @@
package io.legado.app.ui.widget.components
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.MenuOpen
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.FloatingActionButtonMenu
import androidx.compose.material3.FloatingActionButtonMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.PlainTooltip
import androidx.compose.material3.Text
import androidx.compose.material3.ToggleFloatingActionButton
import androidx.compose.material3.ToggleFloatingActionButtonDefaults.animateIcon
import androidx.compose.material3.TooltipAnchorPosition
import androidx.compose.material3.TooltipBox
import androidx.compose.material3.TooltipDefaults
import androidx.compose.material3.animateFloatingActionButton
import androidx.compose.material3.rememberTooltipState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.ThemeResolver
import io.legado.app.ui.widget.components.icon.AppIcon
import io.legado.app.ui.widget.components.text.AppText
import top.yukonga.miuix.kmp.basic.Button
import top.yukonga.miuix.kmp.theme.MiuixTheme
import top.yukonga.miuix.kmp.basic.FloatingActionButton as MiuixFloatingActionButton
import top.yukonga.miuix.kmp.basic.Icon as MiuixIcon
import top.yukonga.miuix.kmp.basic.Text as MiuixText
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -85,3 +117,126 @@ fun AppFloatingActionButton(
}
}
}
data class FabMenuItem(
val icon: ImageVector,
val label: String,
val action: () -> Unit
)
@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class)
@Composable
fun AppFloatingActionButtonMenu(
expanded: Boolean,
onExpandedChange: (Boolean) -> Unit,
items: List<FabMenuItem>,
modifier: Modifier = Modifier,
visible: Boolean = true,
focusRequester: FocusRequester = remember { FocusRequester() }
) {
val isMiuix = ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)
if (isMiuix) {
Column(
modifier = modifier.padding(horizontal = 16.dp, vertical = 24.dp),
horizontalAlignment = Alignment.End,
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
items.forEachIndexed { index, (icon, label, action) ->
AnimatedVisibility(
visible = expanded,
enter = fadeIn(tween(delayMillis = index * 40)) +
scaleIn(tween(delayMillis = index * 40), initialScale = 0.8f),
exit = fadeOut(tween(80)) + scaleOut(tween(80), targetScale = 0.8f)
) {
Button(
onClick = {
action()
onExpandedChange(false)
}
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
MiuixIcon(
imageVector = icon,
contentDescription = null,
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.width(8.dp))
MiuixText(
text = label,
style = MiuixTheme.textStyles.subtitle
)
}
}
}
}
AnimatedVisibility(
visible = visible,
enter = fadeIn() + scaleIn(initialScale = 0.8f),
exit = fadeOut() + scaleOut(targetScale = 0.8f)
) {
MiuixFloatingActionButton(
onClick = { onExpandedChange(!expanded) }
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
modifier = Modifier.padding(horizontal = 16.dp)
) {
MiuixIcon(
imageVector = if (expanded) Icons.Filled.Close
else Icons.AutoMirrored.Filled.MenuOpen,
contentDescription = "Menu",
tint = Color.White
)
Spacer(modifier = Modifier.width(8.dp))
Text("菜单", color = Color.White)
}
}
}
}
} else {
FloatingActionButtonMenu(
modifier = modifier,
expanded = expanded,
button = {
ToggleFloatingActionButton(
modifier = Modifier
.animateFloatingActionButton(
visible = visible,
alignment = Alignment.BottomEnd,
)
.focusRequester(focusRequester),
checked = expanded,
onCheckedChange = { onExpandedChange(!expanded) },
) {
val imageVector by remember {
derivedStateOf {
if (checkedProgress > 0.5f) Icons.Filled.Close
else Icons.AutoMirrored.Filled.MenuOpen
}
}
Icon(
imageVector = imageVector,
contentDescription = "Menu",
modifier = Modifier.animateIcon({ checkedProgress }),
)
}
}
) {
items.forEach { (icon, label, action) ->
FloatingActionButtonMenuItem(
onClick = {
action()
onExpandedChange(false)
},
icon = { Icon(icon, contentDescription = null) },
text = { Text(text = label) }
)
}
}
}
}
@@ -17,7 +17,6 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.blur
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
@@ -44,6 +43,7 @@ fun AppScaffold(
contentColor: Color = contentColorFor(MiuixTheme.colorScheme.surface),
contentWindowInsets: WindowInsets = ScaffoldDefaults.contentWindowInsets,
alwaysDrawBehindBars: Boolean = false,
disableHazeSource: Boolean = false,
content: @Composable (PaddingValues) -> Unit
) {
val isDark = isSystemInDarkTheme()
@@ -97,7 +97,10 @@ fun AppScaffold(
Box(
modifier = Modifier
.fillMaxSize()
.responsiveHazeSource(hazeState)
.then(
if (!disableHazeSource) Modifier.responsiveHazeSource(hazeState)
else Modifier
)
.then(
if (contentDrawsBehindBars) Modifier
else Modifier.padding(scaffoldPadding)
@@ -136,7 +139,10 @@ fun AppScaffold(
Box(
modifier = Modifier
.fillMaxSize()
.responsiveHazeSource(hazeState)
.then(
if (!disableHazeSource) Modifier.responsiveHazeSource(hazeState)
else Modifier
)
.then(
if (contentDrawsBehindBars) Modifier
else Modifier.padding(scaffoldPadding)
@@ -22,9 +22,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import io.legado.app.ui.theme.adaptiveHorizontalPadding
import io.legado.app.ui.widget.components.SearchBar
import io.legado.app.ui.widget.components.icon.AppIcon
import io.legado.app.ui.widget.components.topbar.TopBarActionButton
import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
import io.legado.app.ui.widget.components.icon.AppIcons
import io.legado.app.ui.widget.components.list.ListUiState
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
@@ -118,12 +115,6 @@ fun <T> DynamicTopAppBar(
onQueryChange = onSearchQueryChange,
onSearch = onSearchSubmit,
placeholder = searchPlaceholder,
leadingIcon = {
AppIcon(
imageVector = searchLeadingIcon,
contentDescription = null
)
},
trailingIcon = searchTrailingIcon,
dropdownMenu = searchDropdownMenu
)
@@ -0,0 +1,74 @@
package io.legado.app.ui.widget.components.topbar
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.RowScope
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextOverflow
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.LocalHazeState
import io.legado.app.ui.theme.ThemeResolver
import io.legado.app.ui.theme.responsiveHazeEffect
import top.yukonga.miuix.kmp.basic.TopAppBar as MiuixTopAppBar
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun GlassTopAppBar(
title: String,
modifier: Modifier = Modifier,
navigationIcon: @Composable () -> Unit = {},
actions: @Composable RowScope.() -> Unit = {},
) {
val hazeState = LocalHazeState.current
val composeEngine = LegadoTheme.composeEngine
val isMiuix = ThemeResolver.isMiuixEngine(composeEngine)
val containerColor = if (!isMiuix) {
GlassTopAppBarDefaults.containerColor()
} else {
GlassTopAppBarDefaults.getMiuixAppBarColor()
}
val finalModifier = if (hazeState != null) {
modifier
.background(color = containerColor)
.responsiveHazeEffect(state = hazeState)
} else {
modifier.background(color = containerColor)
}
Column(modifier = finalModifier) {
if (isMiuix) {
MiuixTopAppBar(
title = title,
navigationIcon = navigationIcon,
actions = actions,
color = Color.Transparent
)
} else {
TopAppBar(
title = {
Text(
text = title,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.titleLarge
)
},
navigationIcon = navigationIcon,
actions = actions,
colors = TopAppBarDefaults.topAppBarColors(
containerColor = Color.Transparent,
scrolledContainerColor = Color.Transparent
)
)
}
}
}
@@ -3,9 +3,9 @@
package io.legado.app.utils
import android.content.res.Configuration
import android.content.res.Resources
import splitties.init.appCtx
val sysConfiguration: Configuration = Resources.getSystem().configuration
val sysConfiguration: Configuration get() = appCtx.resources.configuration
val Configuration.isNightMode: Boolean
get() {