[修复] 修复分组管理的抽动问题

[优化] 布局模式为网格时,添加仅封面样式
This commit is contained in:
HapeLee
2026-03-20 01:07:51 +08:00
parent adbbc87892
commit ab907f8062
13 changed files with 247 additions and 155 deletions
@@ -52,6 +52,7 @@ object PreferKey {
const val bookshelfLayoutModePortrait = "bookshelfLayoutPortrait"
const val bookshelfLayoutModeLandscape = "bookshelf_layout_landscape"
const val bookshelfLayoutCompact = "bookshelfLayoutCompact"
const val bookshelfGridLayout = "bookshelfGridLayout"
const val bookshelfSort = "bookshelfSort"
const val bookExportFileName = "bookExportFileName"
const val bookImportFileName = "bookImportFileName"
@@ -88,6 +88,11 @@ object BookshelfConfig {
*/
var bookshelfLayoutGridLandscape by prefDelegate(PreferKey.bookshelfLayoutGridLandscape, 7)
/**
* 网格模式布局样式: 0:标准, 1:紧凑, 2:仅封面
*/
var bookshelfGridLayout by prefDelegate(PreferKey.bookshelfGridLayout, 0)
/**
* 紧凑模式
*/
@@ -26,23 +26,30 @@ import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shadow
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import io.legado.app.R
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookGroup
import io.legado.app.help.book.isLocal
import io.legado.app.ui.config.bookshelfConfig.BookshelfConfig
import io.legado.app.ui.widget.components.cover.BookCover
import io.legado.app.ui.widget.components.cover.BookCoverWithProgress
import io.legado.app.utils.toTimeAgo
/**
* 通用的书架条目布局组件
* 支持 列表/网格 模式及 紧凑/常规 样式
* 支持 列表/网格 模式及 标准/紧凑/仅封面 样式
*/
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun BookshelfItem(
isGrid: Boolean,
isCompact: Boolean,
gridStyle: Int, // 0: Standard, 1: Compact, 2: Cover Only
isCompact: Boolean, // For List Mode
cover: @Composable (Modifier) -> Unit,
title: String,
modifier: Modifier = Modifier,
@@ -57,7 +64,6 @@ fun BookshelfItem(
onLongClick: () -> Unit
) {
if (isGrid) {
Box(
modifier = modifier
.clip(MaterialTheme.shapes.small)
@@ -80,7 +86,7 @@ fun BookshelfItem(
modifier = Modifier.clip(MaterialTheme.shapes.extraSmall)
) {
cover(coverModifier)
if (isCompact) {
if (gridStyle == 1) {
Text(
text = title,
style = (if (titleSmallFont) MaterialTheme.typography.labelSmall else MaterialTheme.typography.labelMedium).copy(
@@ -108,7 +114,7 @@ fun BookshelfItem(
}
}
if (!isCompact) {
if (gridStyle == 0) {
Text(
text = title,
style = if (titleSmallFont) MaterialTheme.typography.labelSmall else MaterialTheme.typography.labelMedium,
@@ -272,7 +278,7 @@ fun BookGroupCover(
fun BookGroupItemGrid(
group: BookGroup,
previewBooks: List<Book>,
isCompact: Boolean = false,
gridStyle: Int = 0,
titleSmallFont: Boolean = false,
titleCenter: Boolean = true,
titleMaxLines: Int = 2,
@@ -283,7 +289,8 @@ fun BookGroupItemGrid(
) {
BookshelfItem(
isGrid = true,
isCompact = isCompact,
gridStyle = gridStyle,
isCompact = false,
cover = { BookGroupCover(books = previewBooks, modifier = it) },
title = group.groupName,
modifier = modifier,
@@ -311,6 +318,7 @@ fun BookGroupItemList(
) {
BookshelfItem(
isGrid = false,
gridStyle = 0,
isCompact = isCompact,
cover = { BookGroupCover(books = previewBooks, modifier = it) },
title = group.groupName,
@@ -325,3 +333,67 @@ fun BookGroupItemList(
onLongClick = onLongClick
)
}
@Composable
fun BookItem(
book: Book,
layoutMode: Int,
gridStyle: Int = 0,
isCompact: Boolean = false,
isUpdating: Boolean = false,
titleSmallFont: Boolean = false,
titleCenter: Boolean = true,
titleMaxLines: Int = 2,
coverShadow: Boolean = false,
onClick: () -> Unit,
onLongClick: () -> Unit
) {
val unreadCount = book.getUnreadChapterNum()
BookshelfItem(
isGrid = layoutMode != 0,
gridStyle = gridStyle,
isCompact = isCompact,
cover = { modifier ->
BookCoverWithProgress(
name = book.name,
author = book.author,
path = book.getDisplayCover(),
isUpdating = isUpdating,
modifier = modifier,
badgeText = if (BookshelfConfig.showUnread && unreadCount > 0) unreadCount.toString() else null,
showBadgeDot = !BookshelfConfig.showUnread && BookshelfConfig.showUnreadNew && unreadCount > 0 && book.lastCheckCount > 0
)
},
title = book.name,
subTitle = if (layoutMode == 0 && isCompact) {
stringResource(R.string.author_read, book.author, unreadCount)
} else {
book.author
},
desc = stringResource(R.string.read_dur_progress, book.durChapterTitle ?: ""),
extra = {
if (BookshelfConfig.showLastUpdateTime && !book.isLocal) {
Text(
text = book.latestChapterTime.toTimeAgo(),
style = MaterialTheme.typography.bodySmall,
color = if (layoutMode != 0 || !isCompact) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.secondary,
modifier = Modifier.padding(end = 4.dp)
)
}
Text(
text = book.latestChapterTitle ?: "",
style = MaterialTheme.typography.bodySmall,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f)
)
},
titleSmallFont = titleSmallFont,
titleCenter = titleCenter,
titleMaxLines = titleMaxLines,
coverShadow = coverShadow,
onClick = onClick,
onLongClick = onLongClick
)
}
@@ -33,7 +33,7 @@ import io.legado.app.ui.config.bookshelfConfig.BookshelfConfig
import io.legado.app.ui.widget.components.modalBottomSheet.GlassModalBottomSheet
import io.legado.app.ui.widget.components.settingItem.CompactDropdownSettingItem
import io.legado.app.ui.widget.components.settingItem.CompactSliderSettingItem
import io.legado.app.ui.widget.components.settingItem.SwitchSettingItem
import io.legado.app.ui.widget.components.settingItem.CompactSwitchSettingItem
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -107,13 +107,24 @@ fun BookshelfConfigSheet(
}
)
SwitchSettingItem(
title = "紧凑模式",
checked = BookshelfConfig.bookshelfLayoutCompact,
imageVector = Icons.Default.ViewCompact,
color = MaterialTheme.colorScheme.surface,
onCheckedChange = { BookshelfConfig.bookshelfLayoutCompact = it }
)
if (layoutMode == 1) {
CompactDropdownSettingItem(
title = "网格样式",
selectedValue = BookshelfConfig.bookshelfGridLayout.toString(),
displayEntries = stringArrayResource(R.array.bookshelf_grid_layout),
entryValues = Array(stringArrayResource(R.array.bookshelf_grid_layout).size) { it.toString() },
imageVector = Icons.Default.ViewCompact,
onValueChange = { BookshelfConfig.bookshelfGridLayout = it.toInt() }
)
} else {
CompactSwitchSettingItem(
title = "紧凑模式",
checked = BookshelfConfig.bookshelfLayoutCompact,
imageVector = Icons.Default.ViewCompact,
color = MaterialTheme.colorScheme.surface,
onCheckedChange = { BookshelfConfig.bookshelfLayoutCompact = it }
)
}
// Grid Count
val gridCount =
@@ -129,7 +140,7 @@ fun BookshelfConfigSheet(
}
)
SwitchSettingItem(
CompactSwitchSettingItem(
title = "标题小字体",
checked = BookshelfConfig.bookshelfTitleSmallFont,
imageVector = Icons.Default.TextFormat,
@@ -137,7 +148,7 @@ fun BookshelfConfigSheet(
onCheckedChange = { BookshelfConfig.bookshelfTitleSmallFont = it }
)
SwitchSettingItem(
CompactSwitchSettingItem(
title = "标题居中",
checked = BookshelfConfig.bookshelfTitleCenter,
color = MaterialTheme.colorScheme.surface,
@@ -152,7 +163,7 @@ fun BookshelfConfigSheet(
onValueChange = { BookshelfConfig.bookshelfTitleMaxLines = it.toInt() }
)
SwitchSettingItem(
CompactSwitchSettingItem(
title = "封面阴影",
checked = BookshelfConfig.bookshelfCoverShadow,
imageVector = Icons.Default.BlurOn,
@@ -161,7 +172,7 @@ fun BookshelfConfigSheet(
)
// Switches
SwitchSettingItem(
CompactSwitchSettingItem(
title = stringResource(R.string.show_unread),
checked = BookshelfConfig.showUnread,
imageVector = Icons.Default.Notifications,
@@ -169,14 +180,14 @@ fun BookshelfConfigSheet(
onCheckedChange = { BookshelfConfig.showUnread = it }
)
SwitchSettingItem(
CompactSwitchSettingItem(
title = stringResource(R.string.show_unread_new),
checked = BookshelfConfig.showUnreadNew,
color = MaterialTheme.colorScheme.surface,
onCheckedChange = { BookshelfConfig.showUnreadNew = it }
)
SwitchSettingItem(
CompactSwitchSettingItem(
title = stringResource(R.string.show_tip),
checked = BookshelfConfig.showTip,
imageVector = Icons.Default.Info,
@@ -184,7 +195,7 @@ fun BookshelfConfigSheet(
onCheckedChange = { BookshelfConfig.showTip = it }
)
SwitchSettingItem(
CompactSwitchSettingItem(
title = stringResource(R.string.show_last_update_time),
checked = BookshelfConfig.showLastUpdateTime,
imageVector = Icons.Default.History,
@@ -192,21 +203,21 @@ fun BookshelfConfigSheet(
onCheckedChange = { BookshelfConfig.showLastUpdateTime = it }
)
SwitchSettingItem(
CompactSwitchSettingItem(
title = stringResource(R.string.show_wait_up_count),
checked = BookshelfConfig.showWaitUpCount,
color = MaterialTheme.colorScheme.surface,
onCheckedChange = { BookshelfConfig.showWaitUpCount = it }
)
SwitchSettingItem(
CompactSwitchSettingItem(
title = stringResource(R.string.show_bookshelf_fast_scroller),
checked = BookshelfConfig.showBookshelfFastScroller,
color = MaterialTheme.colorScheme.surface,
onCheckedChange = { BookshelfConfig.showBookshelfFastScroller = it }
)
SwitchSettingItem(
CompactSwitchSettingItem(
title = stringResource(R.string.show_bookshelf_tab_menu),
checked = BookshelfConfig.shouldShowExpandButton,
imageVector = Icons.Default.Menu,
@@ -57,7 +57,6 @@ 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.help.book.isLocal
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
@@ -66,14 +65,12 @@ import io.legado.app.ui.book.search.SearchActivity
import io.legado.app.ui.config.bookshelfConfig.BookshelfConfig
import io.legado.app.ui.file.HandleFileContract
import io.legado.app.ui.widget.components.GlassTopAppBarDefaults
import io.legado.app.ui.widget.components.cover.BookCoverWithProgress
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.list.ListScaffold
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
import io.legado.app.utils.readText
import io.legado.app.utils.startActivity
import io.legado.app.utils.toTimeAgo
import io.legado.app.utils.toastOnUi
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@@ -332,7 +329,7 @@ fun BookshelfScreen(
BookGroupItemGrid(
group = group,
previewBooks = uiState.groupPreviews[group.groupId] ?: emptyList(),
isCompact = BookshelfConfig.bookshelfLayoutCompact,
gridStyle = BookshelfConfig.bookshelfGridLayout,
titleSmallFont = BookshelfConfig.bookshelfTitleSmallFont,
titleCenter = BookshelfConfig.bookshelfTitleCenter,
titleMaxLines = BookshelfConfig.bookshelfTitleMaxLines,
@@ -462,6 +459,7 @@ fun BookshelfPage(
BookItem(
book = book,
layoutMode = bookshelfLayoutMode,
gridStyle = BookshelfConfig.bookshelfGridLayout,
isCompact = BookshelfConfig.bookshelfLayoutCompact,
isUpdating = uiState.updatingBooks.contains(book.bookUrl),
titleSmallFont = BookshelfConfig.bookshelfTitleSmallFont,
@@ -474,65 +472,3 @@ fun BookshelfPage(
}
}
}
@Composable
fun BookItem(
book: Book,
layoutMode: Int,
isCompact: Boolean = false,
isUpdating: Boolean = false,
titleSmallFont: Boolean = false,
titleCenter: Boolean = true,
titleMaxLines: Int = 2,
coverShadow: Boolean = false,
onClick: () -> Unit,
onLongClick: () -> Unit
) {
val unreadCount = book.getUnreadChapterNum()
BookshelfItem(
isGrid = layoutMode != 0,
isCompact = isCompact,
cover = { modifier ->
BookCoverWithProgress(
name = book.name,
author = book.author,
path = book.getDisplayCover(),
isUpdating = isUpdating,
modifier = modifier,
badgeText = if (BookshelfConfig.showUnread && unreadCount > 0) unreadCount.toString() else null,
showBadgeDot = !BookshelfConfig.showUnread && BookshelfConfig.showUnreadNew && unreadCount > 0 && book.lastCheckCount > 0
)
},
title = book.name,
subTitle = if (isCompact && layoutMode == 0) {
stringResource(R.string.author_read, book.author, unreadCount)
} else {
book.author
},
desc = stringResource(R.string.read_dur_progress, book.durChapterTitle ?: ""),
extra = {
if (BookshelfConfig.showLastUpdateTime && !book.isLocal) {
Text(
text = book.latestChapterTime.toTimeAgo(),
style = MaterialTheme.typography.bodySmall,
color = if (!isCompact) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.secondary,
modifier = Modifier.padding(end = 4.dp)
)
}
Text(
text = book.latestChapterTitle ?: "",
style = MaterialTheme.typography.bodySmall,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f)
)
},
titleSmallFont = titleSmallFont,
titleCenter = titleCenter,
titleMaxLines = titleMaxLines,
coverShadow = coverShadow,
onClick = onClick,
onLongClick = onLongClick
)
}
@@ -1,5 +1,10 @@
package io.legado.app.ui.main.bookshelf
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.SizeTransform
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -73,63 +78,71 @@ fun GroupManageSheet(
}
GlassModalBottomSheet(onDismissRequest = onDismissRequest) {
if (isEditing) {
GroupEditContent(
group = editingGroup,
onDismissRequest = { isEditing = false },
viewModel = viewModel
)
} else {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 32.dp)
) {
Row(
AnimatedContent(
targetState = isEditing,
transitionSpec = {
fadeIn() togetherWith fadeOut() using SizeTransform(clip = false)
},
label = "GroupManageState"
) { editing ->
if (editing) {
GroupEditContent(
group = editingGroup,
onDismissRequest = { isEditing = false },
viewModel = viewModel
)
} else {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.padding(vertical = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
.padding(bottom = 32.dp)
) {
Text(
text = stringResource(R.string.group_manage),
style = MaterialTheme.typography.titleLarge
)
IconButton(onClick = {
editingGroup = null
isEditing = true
}) {
Icon(
Icons.Default.Add,
contentDescription = stringResource(R.string.add)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.padding(vertical = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = stringResource(R.string.group_manage),
style = MaterialTheme.typography.titleLarge
)
IconButton(onClick = {
editingGroup = null
isEditing = true
}) {
Icon(
Icons.Default.Add,
contentDescription = stringResource(R.string.add)
)
}
}
}
LazyColumn(
state = listState,
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(listData, key = { it.groupId }) { group ->
val manageNameInfo = remember(group) { group.getManageName(context) }
ReorderableSelectionItem(
state = reorderableState,
key = group.groupId,
title = group.groupName.ifBlank { manageNameInfo.suffix.orEmpty() },
subtitle = if (group.groupName.isNotBlank()) manageNameInfo.suffix else null,
isEnabled = group.show,
containerColor = MaterialTheme.colorScheme.surface,
onEnabledChange = { isChecked ->
viewModel.upGroup(group.copy(show = isChecked))
},
onClickEdit = {
editingGroup = group
isEditing = true
}
)
LazyColumn(
state = listState,
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(listData, key = { it.groupId }) { group ->
val manageNameInfo = remember(group) { group.getManageName(context) }
ReorderableSelectionItem(
state = reorderableState,
key = group.groupId,
title = group.groupName.ifBlank { manageNameInfo.suffix.orEmpty() },
subtitle = if (group.groupName.isNotBlank()) manageNameInfo.suffix else null,
isEnabled = group.show,
containerColor = MaterialTheme.colorScheme.surface,
onEnabledChange = { isChecked ->
viewModel.upGroup(group.copy(show = isChecked))
},
onClickEdit = {
editingGroup = group
isEditing = true
}
)
}
}
}
}
@@ -8,7 +8,6 @@ import android.text.TextPaint
import android.text.TextUtils
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.aspectRatio
@@ -79,15 +78,17 @@ fun BookCover(
)
.clip(RoundedCornerShape(4.dp))
.background(MaterialTheme.colorScheme.surfaceContainerLow)
.then(
if (CoverConfig.coverShowStroke) {
Modifier.border(
0.5.dp,
MaterialTheme.colorScheme.outlineVariant,
RoundedCornerShape(4.dp)
)
} else Modifier
)
/*
.then(
if (CoverConfig.coverShowStroke) {
Modifier.border(
0.5.dp,
MaterialTheme.colorScheme.outlineVariant,
RoundedCornerShape(4.dp)
)
} else Modifier
)
*/
) {
// 1. 封面底图
AsyncImage(
@@ -1,6 +1,5 @@
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
@@ -47,7 +46,6 @@ fun GlassModalBottomSheet(
Column(
modifier = Modifier
.fillMaxWidth()
.animateContentSize()
.then(modifier),
content = content
)
@@ -12,6 +12,7 @@ import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Slider
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -21,6 +22,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.vector.ImageVector
@@ -151,3 +153,32 @@ fun CompactSliderSettingItem(
}
)
}
@Composable
fun CompactSwitchSettingItem(
title: String,
checked: Boolean,
description: String? = null,
imageVector: ImageVector? = null,
color: Color? = MaterialTheme.colorScheme.surface,
shape: Shape = MaterialTheme.shapes.small,
enabled: Boolean = true,
onCheckedChange: (Boolean) -> Unit
) {
SettingItem(
title = title,
description = description,
imageVector = imageVector,
color = color,
shape = shape,
onClick = { if (enabled) onCheckedChange(!checked) },
trailingContent = {
Switch(
modifier = Modifier.scale(0.8f),
checked = checked,
onCheckedChange = onCheckedChange,
enabled = enabled
)
}
)
}
@@ -208,5 +208,11 @@
<item>不显示</item>
</string-array>
<string-array name="bookshelf_grid_layout">
<item>标准</item>
<item>紧凑</item>
<item>仅封面</item>
</string-array>
<string name="all_version">所有版本</string>
</resources>
@@ -87,4 +87,10 @@
<item>替換規則</item>
</string-array>
<string-array name="bookshelf_grid_layout">
<item>标准</item>
<item>紧凑</item>
<item>仅封面</item>
</string-array>
</resources>
@@ -124,4 +124,10 @@
<item>取代規則</item>
</string-array>
<string-array name="bookshelf_grid_layout">
<item>标准</item>
<item>紧凑</item>
<item>仅封面</item>
</string-array>
</resources>
+7 -1
View File
@@ -286,7 +286,13 @@
<item>@string/cover_grid</item>
</string-array>
<string-array name="label_vis_mode" >
<string-array name="bookshelf_grid_layout">
<item>Standard</item>
<item>Compact</item>
<item>Cover Only</item>
</string-array>
<string-array name="label_vis_mode" >
<item>Auto</item>
<item>Selected</item>
<item>Labeled</item>