[优化] 实现了更多原书架功能(日志、快速滚动条等),修复了一些问题

This commit is contained in:
HapeLee
2026-03-21 04:18:55 +08:00
parent 26246bb441
commit 29ec76481a
12 changed files with 300 additions and 58 deletions
@@ -0,0 +1,124 @@
package io.legado.app.ui.about
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.DeleteSweep
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import io.legado.app.R
import io.legado.app.constant.AppLog
import io.legado.app.ui.widget.components.modalBottomSheet.GlassModalBottomSheet
import io.legado.app.utils.LogUtils
import java.util.Date
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AppLogSheet(
onDismissRequest: () -> Unit
) {
var logs by remember { mutableStateOf(AppLog.logs) }
var showStackTrace by remember { mutableStateOf<String?>(null) }
GlassModalBottomSheet(
onDismissRequest = onDismissRequest
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = stringResource(R.string.log),
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.weight(1f)
)
IconButton(onClick = {
AppLog.clear()
logs = emptyList()
}) {
Icon(Icons.Default.DeleteSweep, contentDescription = "Clear")
}
}
LazyColumn(
modifier = Modifier.fillMaxWidth(),
contentPadding = PaddingValues(bottom = 16.dp)
) {
items(logs) { item ->
LogItem(item) {
item.third?.let {
showStackTrace = it.stackTraceToString()
}
}
}
}
}
showStackTrace?.let { stackTrace ->
AlertDialog(
onDismissRequest = { showStackTrace = null },
title = { Text("Log") },
text = {
SelectionContainer {
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
Text(text = stackTrace, style = MaterialTheme.typography.bodySmall)
}
}
},
confirmButton = {
TextButton(onClick = { showStackTrace = null }) {
Text(stringResource(android.R.string.ok))
}
}
)
}
}
@Composable
private fun LogItem(
item: Triple<Long, String, Throwable?>,
onClick: () -> Unit
) {
Column(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick)
.padding(16.dp)
) {
Text(
text = LogUtils.logTimeFormat.format(Date(item.first)),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.outline
)
Text(
text = item.second,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(top = 4.dp)
)
}
}
@@ -9,7 +9,7 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
@@ -22,7 +22,6 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedIconButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -41,8 +40,8 @@ import io.legado.app.data.entities.BookGroup
import io.legado.app.lib.dialogs.alert
import io.legado.app.ui.widget.components.cover.Cover
import io.legado.app.ui.widget.components.modalBottomSheet.GlassModalBottomSheet
import io.legado.app.ui.widget.components.settingItem.DropdownListSettingItem
import io.legado.app.ui.widget.components.settingItem.SettingItem
import io.legado.app.ui.widget.components.settingItem.CompactDropdownSettingItem
import io.legado.app.ui.widget.components.settingItem.CompactSwitchSettingItem
import io.legado.app.utils.FileUtils
import io.legado.app.utils.MD5Utils
import io.legado.app.utils.SelectImageContract
@@ -128,7 +127,7 @@ fun GroupEditContent(
Cover(
path = coverPath,
modifier = Modifier
.size(width = 90.dp, height = 130.dp)
.width(96.dp)
.clickable { selectImage.launch() }
)
@@ -144,7 +143,7 @@ fun GroupEditContent(
singleLine = true
)
DropdownListSettingItem(
CompactDropdownSettingItem(
title = stringResource(R.string.sort),
selectedValue = selectedSortIndex.toString(),
displayEntries = sortOptions,
@@ -158,15 +157,10 @@ fun GroupEditContent(
Spacer(modifier = Modifier.height(8.dp))
SettingItem(
CompactSwitchSettingItem(
title = stringResource(R.string.allow_drop_down_refresh),
trailingContent = {
Switch(
checked = enableRefresh,
onCheckedChange = { enableRefresh = it }
)
},
onClick = { enableRefresh = !enableRefresh }
checked = enableRefresh,
onCheckedChange = { enableRefresh = it }
)
Spacer(modifier = Modifier.height(16.dp))
@@ -74,18 +74,18 @@ fun BookshelfItem(
.padding(4.dp)
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
val coverModifier = if (coverShadow) {
Modifier
.fillMaxWidth()
.shadow(4.dp, MaterialTheme.shapes.extraSmall)
} else {
Modifier.fillMaxWidth()
}
Box(
modifier = Modifier.clip(MaterialTheme.shapes.extraSmall)
modifier = Modifier
.then(
if (coverShadow) Modifier.shadow(
4.dp,
MaterialTheme.shapes.extraSmall
) else Modifier
)
.clip(MaterialTheme.shapes.extraSmall) // 先阴影后裁剪
) {
cover(coverModifier)
cover(Modifier.fillMaxWidth())
if (gridStyle == 1) {
Text(
text = title,
@@ -96,6 +96,7 @@ fun BookshelfItem(
blurRadius = 4f
)
),
textAlign = if (titleCenter) TextAlign.Center else TextAlign.Start,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
modifier = Modifier
@@ -5,12 +5,12 @@ import androidx.activity.compose.BackHandler
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
@@ -35,8 +35,8 @@ import androidx.compose.material3.PrimaryScrollableTabRow
import androidx.compose.material3.Surface
import androidx.compose.material3.Tab
import androidx.compose.material3.Text
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults
import androidx.compose.material3.pulltorefresh.pullToRefresh
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -57,6 +57,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import io.legado.app.R
import io.legado.app.data.entities.Book
import io.legado.app.ui.about.AppLogSheet
import io.legado.app.ui.book.cache.CacheActivity
import io.legado.app.ui.book.import.local.ImportBookActivity
import io.legado.app.ui.book.import.remote.RemoteBookActivity
@@ -67,6 +68,7 @@ import io.legado.app.ui.file.HandleFileContract
import io.legado.app.ui.widget.components.GlassTopAppBarDefaults
import io.legado.app.ui.widget.components.filePicker.FilePickerSheet
import io.legado.app.ui.widget.components.importComponents.SourceInputDialog
import io.legado.app.ui.widget.components.lazylist.FastScrollLazyVerticalGrid
import io.legado.app.ui.widget.components.list.ListScaffold
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
import io.legado.app.utils.readText
@@ -94,6 +96,7 @@ fun BookshelfScreen(
var showImportSheet by remember { mutableStateOf(false) }
var showConfigSheet by remember { mutableStateOf(false) }
var showGroupManageSheet by remember { mutableStateOf(false) }
var showLogSheet by remember { mutableStateOf(false) }
val importLauncher = rememberLauncherForActivityResult(HandleFileContract()) {
runCatching {
@@ -232,6 +235,7 @@ fun BookshelfScreen(
RoundDropdownMenuItem(
text = { Text(stringResource(R.string.log)) },
onClick = {
showLogSheet = true
dismiss()
},
leadingIcon = { Icon(Icons.Default.History, null) }
@@ -240,8 +244,11 @@ fun BookshelfScreen(
bottomContent = if (bookGroupStyle == 0) {
{
if (uiState.groups.isNotEmpty()) {
val selectedTabIndex = remember(pagerState.currentPage, uiState.groups.size) {
pagerState.currentPage.coerceIn(0, uiState.groups.size - 1)
}
PrimaryScrollableTabRow(
selectedTabIndex = pagerState.currentPage,
selectedTabIndex = selectedTabIndex,
edgePadding = 0.dp,
divider = { },
containerColor = GlassTopAppBarDefaults.containerColor(),
@@ -249,7 +256,7 @@ fun BookshelfScreen(
) {
uiState.groups.forEachIndexed { index, group ->
Tab(
selected = pagerState.currentPage == index,
selected = selectedTabIndex == index,
onClick = {
scope.launch { pagerState.animateScrollToPage(index) }
},
@@ -271,31 +278,28 @@ fun BookshelfScreen(
) { paddingValues ->
var isRefreshing by remember { mutableStateOf(false) }
val pullToRefreshState = rememberPullToRefreshState()
val currentGroup = uiState.groups.getOrNull(pagerState.currentPage)
val pullToRefreshEnabled = currentGroup?.enableRefresh ?: true
PullToRefreshBox(
isRefreshing = isRefreshing,
onRefresh = {
scope.launch {
isRefreshing = true
viewModel.upToc(uiState.items)
delay(1000)
isRefreshing = false
}
},
state = pullToRefreshState,
modifier = Modifier.fillMaxSize(),
indicator = {
PullToRefreshDefaults.LoadingIndicator(
Box(
modifier = Modifier
.fillMaxSize()
.pullToRefresh(
state = pullToRefreshState,
isRefreshing = isRefreshing,
modifier = Modifier
.align(Alignment.TopCenter)
.padding(top = paddingValues.calculateTopPadding())
onRefresh = {
scope.launch {
isRefreshing = true
viewModel.upToc(uiState.items)
delay(1000)
isRefreshing = false
}
},
enabled = pullToRefreshEnabled
)
}
) {
if (bookGroupStyle == 2 && isInFolderRoot) {
LazyVerticalGrid(
FastScrollLazyVerticalGrid(
columns = GridCells.Fixed(bookshelfLayoutGrid.coerceAtLeast(1)),
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(
@@ -305,7 +309,8 @@ fun BookshelfScreen(
end = if (bookshelfLayoutMode != 0) 12.dp else 0.dp
),
verticalArrangement = Arrangement.spacedBy(if (bookshelfLayoutMode != 0) 8.dp else 0.dp),
horizontalArrangement = Arrangement.spacedBy(if (bookshelfLayoutMode != 0) 8.dp else 0.dp)
horizontalArrangement = Arrangement.spacedBy(if (bookshelfLayoutMode != 0) 8.dp else 0.dp),
showFastScroll = BookshelfConfig.showBookshelfFastScroller
) {
items(uiState.groups) { group ->
if (bookshelfLayoutMode == 0) {
@@ -369,6 +374,14 @@ fun BookshelfScreen(
}
}
}
PullToRefreshDefaults.LoadingIndicator(
state = pullToRefreshState,
isRefreshing = isRefreshing,
modifier = Modifier
.align(Alignment.TopCenter)
.padding(top = paddingValues.calculateTopPadding())
)
}
}
@@ -409,6 +422,10 @@ fun BookshelfScreen(
)
}
if (showLogSheet) {
AppLogSheet(onDismissRequest = { showLogSheet = false })
}
if (uiState.isLoading) {
Dialog(onDismissRequest = {}) {
Surface(
@@ -443,7 +460,7 @@ fun BookshelfPage(
onBookClick: (Book) -> Unit,
onBookLongClick: (Book) -> Unit
) {
LazyVerticalGrid(
FastScrollLazyVerticalGrid(
columns = GridCells.Fixed(bookshelfLayoutGrid.coerceAtLeast(1)),
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(
@@ -453,7 +470,8 @@ fun BookshelfPage(
end = if (bookshelfLayoutMode != 0) 12.dp else 0.dp
),
verticalArrangement = Arrangement.spacedBy(if (bookshelfLayoutMode != 0) 8.dp else 0.dp),
horizontalArrangement = Arrangement.spacedBy(if (bookshelfLayoutMode != 0) 8.dp else 0.dp)
horizontalArrangement = Arrangement.spacedBy(if (bookshelfLayoutMode != 0) 8.dp else 0.dp),
showFastScroll = BookshelfConfig.showBookshelfFastScroller
) {
items(books, key = { it.bookUrl }) { book ->
BookItem(
@@ -95,7 +95,6 @@ fun GroupManageSheet(
Column(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 32.dp)
) {
Row(
modifier = Modifier
@@ -0,0 +1,43 @@
package io.legado.app.ui.widget.components
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material3.Icon
import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchColors
import androidx.compose.material3.SwitchDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
@Composable
fun IconSwitch(
checked: Boolean,
onCheckedChange: (Boolean) -> Unit,
enabled: Boolean = true,
checkedIcon: ImageVector = Icons.Filled.Check,
uncheckedIcon: ImageVector? = null,
showIcon: Boolean = true,
colors: SwitchColors = SwitchDefaults.colors()
) {
Switch(
checked = checked,
onCheckedChange = onCheckedChange,
enabled = enabled,
colors = colors,
thumbContent = {
if (!showIcon) return@Switch
val icon = if (checked) checkedIcon else uncheckedIcon
if (icon != null) {
Icon(
imageVector = icon,
contentDescription = null,
modifier = Modifier.size(SwitchDefaults.IconSize)
)
}
}
)
}
@@ -37,7 +37,6 @@ import androidx.compose.ui.unit.dp
import androidx.core.graphics.withSave
import coil.compose.AsyncImage
import coil.request.ImageRequest
import io.legado.app.help.config.AppConfig
import io.legado.app.model.BookCover
import io.legado.app.ui.config.coverConfig.CoverConfig
import io.legado.app.ui.widget.components.card.TextCard
@@ -57,7 +56,7 @@ fun BookCover(
val context = LocalContext.current
val isNight = isSystemInDarkTheme()
val useDefault = AppConfig.useDefaultCover
val useDefault = CoverConfig.useDefaultCover
val finalPath = if (useDefault) null else path
// 为每个 BookCover 组件实例基于其书名或路径生成一个随机封面
@@ -43,7 +43,7 @@ fun Cover(
val boxModifier = modifier
.aspectRatio(3f / 4f)
.clip(RoundedCornerShape(4.dp))
.background(MaterialTheme.colorScheme.surfaceContainerLow)
.background(MaterialTheme.colorScheme.surfaceContainer)
Box(modifier = boxModifier) {
if (path == null) {
@@ -6,6 +6,11 @@ import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyGridScope
import androidx.compose.foundation.lazy.grid.LazyGridState
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
@@ -80,3 +85,52 @@ fun FastScrollLazyColumn(
)
}
}
@Composable
fun FastScrollLazyVerticalGrid(
columns: GridCells,
modifier: Modifier = Modifier,
state: LazyGridState = rememberLazyGridState(),
contentPadding: PaddingValues = PaddingValues(0.dp),
reverseLayout: Boolean = false,
verticalArrangement: Arrangement.Vertical = Arrangement.Top,
horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
userScrollEnabled: Boolean = true,
showFastScroll: Boolean = true,
content: LazyGridScope.() -> Unit,
) {
if (showFastScroll) {
VerticalGridFastScroller(
state = state,
columns = columns,
arrangement = horizontalArrangement,
contentPadding = contentPadding,
modifier = modifier,
topContentPadding = contentPadding.calculateTopPadding(),
endContentPadding = contentPadding.calculateEndPadding(LocalLayoutDirection.current),
) {
LazyVerticalGrid(
columns = columns,
state = state,
contentPadding = contentPadding,
reverseLayout = reverseLayout,
verticalArrangement = verticalArrangement,
horizontalArrangement = horizontalArrangement,
userScrollEnabled = userScrollEnabled,
content = content,
)
}
} else {
LazyVerticalGrid(
columns = columns,
modifier = modifier,
state = state,
contentPadding = contentPadding,
reverseLayout = reverseLayout,
verticalArrangement = verticalArrangement,
horizontalArrangement = horizontalArrangement,
userScrollEnabled = userScrollEnabled,
content = content,
)
}
}
@@ -26,7 +26,6 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TooltipAnchorPosition
import androidx.compose.material3.TooltipBox
import androidx.compose.material3.TooltipDefaults
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.material3.animateFloatingActionButton
import androidx.compose.material3.rememberTooltipState
@@ -40,6 +39,7 @@ import androidx.compose.ui.zIndex
import dev.chrisbanes.haze.HazeState
import io.legado.app.ui.theme.responsiveHazeEffect
import io.legado.app.ui.theme.responsiveHazeSource
import io.legado.app.ui.widget.components.GlassTopAppBarDefaults
import io.legado.app.ui.widget.components.SelectionActions
import io.legado.app.ui.widget.components.SelectionBottomBar
import io.legado.app.ui.widget.components.topbar.DynamicTopAppBar
@@ -83,7 +83,7 @@ fun <T> ListScaffold(
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
content: @Composable (PaddingValues) -> Unit
) {
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
val hazeState = remember { HazeState() }
Scaffold(
@@ -136,4 +136,4 @@ fun <T> ListScaffold(
}
}
}
}
}
@@ -1,8 +1,10 @@
package io.legado.app.ui.widget.components.modalBottomSheet
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
@@ -16,6 +18,8 @@ import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalWindowInfo
import io.legado.app.ui.theme.rememberOpaqueColorScheme
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@@ -30,6 +34,10 @@ fun GlassModalBottomSheet(
content: @Composable ColumnScope.() -> Unit
) {
val colorScheme = rememberOpaqueColorScheme()
val density = LocalDensity.current
val maxHeight = with(density) {
LocalWindowInfo.current.containerSize.height.toDp() * 0.8f
}
ModalBottomSheet(
onDismissRequest = onDismissRequest,
@@ -46,6 +54,8 @@ fun GlassModalBottomSheet(
Column(
modifier = Modifier
.fillMaxWidth()
.heightIn(max = maxHeight)
.animateContentSize()
.then(modifier),
content = content
)
@@ -1,9 +1,9 @@
package io.legado.app.ui.widget.components.settingItem
import androidx.compose.material3.Switch
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import io.legado.app.ui.widget.components.IconSwitch
@Composable
fun SwitchSettingItem(
@@ -22,7 +22,7 @@ fun SwitchSettingItem(
color = color,
onClick = { if (enabled) onCheckedChange(!checked) },
trailingContent = {
Switch(
IconSwitch(
checked = checked,
onCheckedChange = onCheckedChange,
enabled = enabled