[优化] 优化界面

This commit is contained in:
HapeLee
2026-04-19 02:34:40 +08:00
parent 85b90f77f7
commit 15f6efc42d
28 changed files with 314 additions and 266 deletions
@@ -160,6 +160,12 @@ interface ReadRecordDao {
@Query("SELECT * FROM readRecordSession WHERE deviceId = :deviceId AND bookName = :bookName AND bookAuthor = :bookAuthor")
suspend fun getSessionsByBook(deviceId: String, bookName: String, bookAuthor: String): List<ReadRecordSession>
@Query("SELECT * FROM readRecordSession WHERE deviceId = :deviceId AND bookName = :bookName AND bookAuthor = :bookAuthor ORDER BY startTime DESC")
fun getSessionsByBookFlow(deviceId: String, bookName: String, bookAuthor: String): Flow<List<ReadRecordSession>>
@Query("SELECT readTime FROM readRecord WHERE deviceId = :deviceId AND bookName = :bookName AND bookAuthor = :bookAuthor")
fun getReadTimeFlow(deviceId: String, bookName: String, bookAuthor: String): Flow<Long?>
@Delete
suspend fun deleteDetail(detail: ReadRecordDetail)
@@ -7,6 +7,7 @@ import io.legado.app.data.dao.ReadRecordDao
import io.legado.app.data.entities.readRecord.ReadRecord
import io.legado.app.data.entities.readRecord.ReadRecordDetail
import io.legado.app.data.entities.readRecord.ReadRecordSession
import io.legado.app.data.entities.readRecord.ReadRecordTimelineDay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import java.util.Date
@@ -51,6 +52,27 @@ class ReadRecordRepository(
return dao.getAllSessions(getCurrentDeviceId())
}
fun getBookSessions(bookName: String, bookAuthor: String): Flow<List<ReadRecordSession>> {
return dao.getSessionsByBookFlow(getCurrentDeviceId(), bookName, bookAuthor)
}
fun getBookTimelineDays(bookName: String, bookAuthor: String): Flow<List<ReadRecordTimelineDay>> {
return getBookSessions(bookName, bookAuthor).map { sessions ->
sessions.groupBy { DateUtil.format(Date(it.startTime), "yyyy-MM-dd") }
.toSortedMap(compareByDescending { it })
.map { (date, daySessions) ->
ReadRecordTimelineDay(
date = date,
sessions = daySessions.sortedByDescending { it.startTime }
)
}
}
}
fun getBookReadTime(bookName: String, bookAuthor: String): Flow<Long> {
return dao.getReadTimeFlow(getCurrentDeviceId(), bookName, bookAuthor).map { it ?: 0L }
}
suspend fun getMergeCandidates(targetRecord: ReadRecord): List<ReadRecord> {
return dao.getReadRecordsByNameExcludingAuthor(
targetRecord.deviceId,
@@ -53,7 +53,7 @@ import io.legado.app.ui.theme.adaptiveContentPadding
import io.legado.app.ui.theme.adaptiveHorizontalPadding
import io.legado.app.ui.widget.components.AppScaffold
import io.legado.app.ui.widget.components.EmptyMessage
import io.legado.app.ui.widget.components.SearchBarSection
import io.legado.app.ui.widget.components.SearchBar
import io.legado.app.ui.widget.components.bookmark.BookmarkEditSheet
import io.legado.app.ui.widget.components.bookmark.BookmarkItem
import io.legado.app.ui.widget.components.button.TopBarActionButton
@@ -190,7 +190,7 @@ fun AllBookmarkScreen(
enter = expandVertically() + fadeIn(),
exit = shrinkVertically() + fadeOut()
) {
SearchBarSection(
SearchBar(
query = searchText,
onQueryChange = { viewModel.onSearchQueryChanged(it) },
placeholder = "搜索...",
@@ -258,7 +258,7 @@ fun AllBookmarkScreen(
.animateItem()
.fillMaxWidth()
.padding(vertical = 4.dp),
shape = MaterialTheme.shapes.medium,
cornerRadius = 12.dp,
containerColor = LegadoTheme.colorScheme.surfaceContainer
) {
BookmarkGroupHeaderContent(
@@ -10,22 +10,16 @@ import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
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.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.grid.GridCells
@@ -35,21 +29,17 @@ import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.FormatListBulleted
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.FilterList
import androidx.compose.material.icons.filled.GridView
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material.icons.outlined.FilterAlt
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Slider
import androidx.compose.material3.Surface
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
@@ -63,14 +53,11 @@ 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.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import io.legado.app.data.entities.SearchBook
@@ -81,12 +68,11 @@ import io.legado.app.ui.theme.ThemeResolver
import io.legado.app.ui.theme.responsiveHazeEffect
import io.legado.app.ui.theme.responsiveHazeSource
import io.legado.app.ui.widget.components.AppScaffold
import io.legado.app.ui.widget.components.SearchBarSection
import io.legado.app.ui.widget.components.SearchBar
import io.legado.app.ui.widget.components.button.AnimatedTextButton
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.TextCard
import io.legado.app.ui.widget.components.cover.Cover
import io.legado.app.ui.widget.components.explore.calculateExploreKindRows
import io.legado.app.ui.widget.components.explore.ExploreKindMultiTypeItem
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
@@ -254,7 +240,7 @@ fun ExploreShowScreen(
var kindQuery by remember { mutableStateOf("") }
SearchBarSection(
SearchBar(
query = kindQuery,
backgroundColor = LegadoTheme.colorScheme.surface.copy(alpha = 0.5f),
onQueryChange = { kindQuery = it },
@@ -564,7 +564,7 @@ private fun PathNavigationBar(
GlassCard(
modifier = Modifier.weight(1f),
containerColor = LegadoTheme.colorScheme.surfaceContainer,
shape = MaterialTheme.shapes.medium
cornerRadius = 12.dp,
) {
LazyRow(
modifier = Modifier.fillMaxWidth(),
@@ -74,7 +74,7 @@ import io.legado.app.ui.theme.adaptiveHorizontalPadding
import io.legado.app.ui.widget.CollapsibleHeader
import io.legado.app.ui.widget.components.AppScaffold
import io.legado.app.ui.widget.components.EmptyMessage
import io.legado.app.ui.widget.components.SearchBarSection
import io.legado.app.ui.widget.components.SearchBar
import io.legado.app.ui.widget.components.alert.AppAlertDialog
import io.legado.app.ui.widget.components.button.AppIconButton
import io.legado.app.ui.widget.components.button.TopBarNavigationButton
@@ -102,6 +102,7 @@ import io.legado.app.ui.widget.components.swipe.SwipeActionContainer
import io.legado.app.ui.widget.components.text.AppText
import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
import io.legado.app.utils.formatReadDuration
import io.legado.app.utils.StringUtils.formatFriendlyDate
import kotlinx.coroutines.launch
import org.koin.androidx.compose.koinViewModel
@@ -226,7 +227,7 @@ fun ReadRecordScreen(
modifier = Modifier.adaptiveHorizontalPadding(),
visible = showSearch
) {
SearchBarSection(
SearchBar(
query = state.searchKey ?: "",
onQueryChange = { viewModel.setSearchKey(it) }
)
@@ -1044,13 +1045,5 @@ fun BookStackView(coverPaths: List<String?>) {
}
fun formatDuring(mss: Long): String {
val days = mss / (1000 * 60 * 60 * 24)
val hours = mss % (1000 * 60 * 60 * 24) / (1000 * 60 * 60)
val minutes = mss % (1000 * 60 * 60) / (1000 * 60)
val seconds = mss % (1000 * 60) / 1000
val d = if (days > 0) "${days}" else ""
val h = if (hours > 0) "${hours}小时" else ""
val m = if (minutes > 0) "${minutes}分钟" else ""
val s = if (seconds > 0) "${seconds}" else ""
return if ("$d$h$m$s".isBlank()) "0秒" else "$d$h$m$s"
return formatReadDuration(mss)
}
@@ -1,5 +1,6 @@
package io.legado.app.ui.book.search
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -10,6 +11,7 @@ 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.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow
@@ -68,15 +70,15 @@ import io.legado.app.ui.theme.adaptiveContentPadding
import io.legado.app.ui.theme.adaptiveContentPaddingOnlyVertical
import io.legado.app.ui.theme.adaptiveHorizontalPadding
import io.legado.app.ui.widget.components.AppFloatingActionButton
import io.legado.app.ui.widget.components.AppSearchBar
import io.legado.app.ui.widget.components.AppScaffold
import io.legado.app.ui.widget.components.SearchBar
import io.legado.app.ui.widget.components.alert.AppAlertDialog
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
import io.legado.app.ui.widget.components.book.SearchBookListItem
import io.legado.app.ui.widget.components.button.SmallIconButton
import io.legado.app.ui.widget.components.button.SmallTextButton
import io.legado.app.ui.widget.components.card.NormalCard
import io.legado.app.ui.widget.components.card.SelectionItemCard
import io.legado.app.ui.widget.components.card.TextCard
import io.legado.app.ui.widget.components.button.ToggleChip
import io.legado.app.ui.widget.components.button.TopBarActionButton
import io.legado.app.ui.widget.components.button.TopBarAnimatedActionButton
@@ -84,6 +86,7 @@ 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.AppIcon
import io.legado.app.ui.widget.components.icon.AppIcons
import io.legado.app.ui.widget.components.list.TopFloatingStickyItem
import io.legado.app.ui.widget.components.tabRow.AppTabRow
import io.legado.app.ui.widget.components.text.AppText
import io.legado.app.utils.toastOnUi
@@ -104,8 +107,8 @@ fun SearchScreen(
val state by viewModel.uiState.collectAsStateWithLifecycle()
val listState = rememberLazyListState()
var queryInput by rememberSaveable { mutableStateOf(state.query) }
var searchExpanded by rememberSaveable { mutableStateOf(false) }
var scopeSheetTab by rememberSaveable { mutableStateOf(0) }
val showSuggestionPanel = state.showSuggestions
val latestQuery by rememberUpdatedState(state.query)
val scrollBehavior = if (ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)) {
GlassTopAppBarDefaults.defaultScrollBehavior()
@@ -149,15 +152,13 @@ fun SearchScreen(
state.hasMore,
state.isManualStop,
state.showSuggestions,
searchExpanded,
) {
if (
shouldLoadMore &&
!state.isSearching &&
state.hasMore &&
!state.isManualStop &&
!state.showSuggestions &&
!searchExpanded
!state.showSuggestions
) {
viewModel.onIntent(SearchIntent.LoadMore)
}
@@ -203,104 +204,60 @@ fun SearchScreen(
)
},
actions = {
if (!searchExpanded) {
TopBarActionButton(
onClick = {
viewModel.onIntent(SearchIntent.SetScopeSheetVisible(true))
},
imageVector = AppIcons.Filter
)
TopBarActionButton(
onClick = {
viewModel.onIntent(SearchIntent.OpenSourceManage)
},
imageVector = AppIcons.Settings,
)
}
TopBarAnimatedActionButton(
checked = state.isPrecisionSearch,
onCheckedChange = { checked ->
viewModel.onIntent(SearchIntent.TogglePrecision(checked))
},
iconChecked = AppIcons.PrecisionSearch,
iconUnchecked = AppIcons.UnPrecisionSearch,
activeText = stringResource(R.string.precision_search),
inactiveText = stringResource(R.string.search),
)
TopBarActionButton(
onClick = {
viewModel.onIntent(SearchIntent.SetScopeSheetVisible(true))
},
imageVector = AppIcons.Filter
)
TopBarActionButton(
onClick = {
viewModel.onIntent(SearchIntent.OpenSourceManage)
},
imageVector = AppIcons.Settings,
)
},
scrollBehavior = scrollBehavior
)
Spacer(modifier = Modifier.height(8.dp))
AppSearchBar(
modifier = Modifier.fillMaxWidth(),
query = queryInput,
onQueryChange = { queryInput = it },
onSearch = submitSearch,
expanded = searchExpanded,
onExpandedChange = { searchExpanded = it },
label = searchLabel,
placeholder = { Text(text = searchLabel) },
leadingIcon = {
TopBarActionButton(
onClick = {
if (searchExpanded) {
searchExpanded = false
}
},
imageVector = if (searchExpanded) AppIcons.Back else AppIcons.Search,
contentDescription = if (searchExpanded) stringResource(R.string.search) else stringResource(
R.string.search
)
)
},
trailingIcon = {
Row(verticalAlignment = Alignment.CenterVertically) {
if (queryInput.isNotEmpty()) {
TopBarActionButton(
onClick = {
queryInput = ""
viewModel.onIntent(SearchIntent.UpdateQuery(""))
},
imageVector = Icons.Default.Close,
contentDescription = stringResource(R.string.clear)
)
}
TopBarAnimatedActionButton(
checked = state.isPrecisionSearch,
onCheckedChange = { checked ->
viewModel.onIntent(SearchIntent.TogglePrecision(checked))
},
iconChecked = AppIcons.PrecisionSearch,
iconUnchecked = AppIcons.UnPrecisionSearch,
activeText = stringResource(R.string.precision_search),
inactiveText = stringResource(R.string.search),
)
if (searchExpanded) {
TopBarActionButton(
onClick = {
viewModel.onIntent(SearchIntent.SetScopeSheetVisible(true))
},
imageVector = AppIcons.Filter,
contentDescription = stringResource(R.string.search_select_group)
)
TopBarActionButton(
onClick = {
viewModel.onIntent(SearchIntent.OpenSourceManage)
},
imageVector = AppIcons.Settings,
contentDescription = stringResource(R.string.book_source_manage)
)
}
}
},
Box(
modifier = Modifier
.fillMaxWidth()
.adaptiveHorizontalPadding()
) {
SearchSuggestionPanel(
state = state,
onUseHistory = { keyword ->
queryInput = keyword
viewModel.onIntent(SearchIntent.UseHistoryKeyword(keyword))
searchExpanded = false
SearchBar(
query = queryInput,
onQueryChange = { queryInput = it },
onSearch = submitSearch,
placeholder = searchLabel,
trailingIcon = {
Row(
modifier = Modifier.padding(end = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
if (queryInput.isNotEmpty()) {
TopBarActionButton(
onClick = {
queryInput = ""
viewModel.onIntent(SearchIntent.UpdateQuery(""))
},
imageVector = AppIcons.Close,
contentDescription = stringResource(R.string.clear)
)
}
}
},
onDeleteHistory = { viewModel.onIntent(SearchIntent.DeleteHistory(it)) },
onOpenBook = {
viewModel.onIntent(SearchIntent.OpenBookshelfBook(it))
searchExpanded = false
},
onClearHistory = {
viewModel.onIntent(SearchIntent.SetClearHistoryDialogVisible(true))
},
modifier = Modifier.fillMaxSize(),
)
}
}
@@ -330,73 +287,113 @@ fun SearchScreen(
.fillMaxSize()
.padding(paddingValues)
) {
if (showResultCountCard || showSourceProgressCard) {
Spacer(modifier = Modifier.height(6.dp))
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 36.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
if (showResultCountCard) {
TextCard(
modifier = Modifier.weight(1f),
icon = AppIcons.Search,
text = "搜索结果 ${state.results.size}"
)
}
if (showSourceProgressCard) {
TextCard(
modifier = Modifier.weight(1f),
icon = AppIcons.Filter,
text = "已检索源 ${state.processedSources}/${state.totalSources}"
)
}
}
}
Spacer(modifier = Modifier.height(8.dp))
if (state.results.isEmpty()) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
SearchResultFooter(
isSearching = state.isSearching,
hasMore = state.hasMore,
hasResult = false,
committedQuery = state.committedQuery,
onLoadMore = { viewModel.onIntent(SearchIntent.LoadMore) },
AnimatedContent(
targetState = showSuggestionPanel,
label = "SearchBodyTransition",
modifier = Modifier.fillMaxSize()
) { isSuggestionVisible ->
if (isSuggestionVisible) {
SearchSuggestionPanel(
state = state,
onUseHistory = { keyword ->
queryInput = keyword
viewModel.onIntent(SearchIntent.UseHistoryKeyword(keyword))
},
onDeleteHistory = { viewModel.onIntent(SearchIntent.DeleteHistory(it)) },
onOpenBook = {
viewModel.onIntent(SearchIntent.OpenBookshelfBook(it))
},
onClearHistory = {
viewModel.onIntent(SearchIntent.SetClearHistoryDialogVisible(true))
},
modifier = Modifier.fillMaxSize(),
)
}
} else {
LazyColumn(
modifier = Modifier.fillMaxSize(),
state = listState,
verticalArrangement = Arrangement.spacedBy(6.dp)
) {
items(items = state.results, key = { it.bookUrl }) { book ->
val shelfStateFlow = remember(book.bookUrl) {
viewModel.getBookShelfStateFlow(book)
}
SearchBookListItem(
book = book,
shelfState = shelfStateFlow,
onClick = {
viewModel.onIntent(SearchIntent.OpenSearchBook(book))
}
)
}
} else {
Box(modifier = Modifier.fillMaxSize()) {
Column(modifier = Modifier.fillMaxSize()) {
Spacer(modifier = Modifier.height(8.dp))
item {
SearchResultFooter(
isSearching = state.isSearching,
hasMore = state.hasMore,
hasResult = true,
committedQuery = state.committedQuery,
onLoadMore = { viewModel.onIntent(SearchIntent.LoadMore) },
)
if (state.results.isEmpty()) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
SearchResultFooter(
isSearching = state.isSearching,
hasMore = state.hasMore,
hasResult = false,
committedQuery = state.committedQuery,
onLoadMore = { viewModel.onIntent(SearchIntent.LoadMore) },
)
}
}
}
if (state.results.isNotEmpty()) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
state = listState,
contentPadding = adaptiveContentPaddingOnlyVertical(
top = 48.dp,
bottom = 8.dp
),
verticalArrangement = Arrangement.spacedBy(6.dp)
) {
items(items = state.results, key = { it.bookUrl }) { book ->
val shelfStateFlow = remember(book.bookUrl) {
viewModel.getBookShelfStateFlow(book)
}
SearchBookListItem(
book = book,
shelfState = shelfStateFlow,
onClick = {
viewModel.onIntent(SearchIntent.OpenSearchBook(book))
}
)
}
item {
SearchResultFooter(
isSearching = state.isSearching,
hasMore = state.hasMore,
hasResult = true,
committedQuery = state.committedQuery,
onLoadMore = { viewModel.onIntent(SearchIntent.LoadMore) },
)
}
}
}
TopFloatingStickyItem(
item = if (showResultCountCard || showSourceProgressCard) {
SearchFloatingSummary(
resultText = if (showResultCountCard) "结果 ${state.results.size}" else null,
sourceText = if (showSourceProgressCard) " · 进度 ${state.processedSources}/${state.totalSources}" else null,
)
} else {
null
},
modifier = Modifier
.align(Alignment.TopCenter)
.padding(top = 6.dp),
) { summary ->
NormalCard(
cornerRadius = 16.dp,
containerColor = LegadoTheme.colorScheme.surfaceContainer,
contentColor = LegadoTheme.colorScheme.onCardContainer
) {
Row(
modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp),
) {
summary.resultText?.let { text ->
AppText(text = text, style = LegadoTheme.typography.labelSmallEmphasized)
}
summary.sourceText?.let { text ->
AppText(text = text, style = LegadoTheme.typography.labelSmallEmphasized)
}
}
}
}
}
}
}
@@ -520,6 +517,11 @@ fun SearchScreen(
}
}
private data class SearchFloatingSummary(
val resultText: String?,
val sourceText: String?,
)
@Composable
private fun SearchSuggestionPanel(
state: SearchUiState,
@@ -565,7 +567,10 @@ private fun SearchSuggestionPanel(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Row(
modifier = Modifier.padding(vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
AppIcon(AppIcons.History, contentDescription = null)
Spacer(modifier = Modifier.width(6.dp))
AppText(
@@ -636,9 +641,16 @@ private fun SearchResultFooter(
) {
when {
isSearching -> {
Row(verticalAlignment = Alignment.CenterVertically) {
CircularProgressIndicator(modifier = Modifier.width(18.dp), strokeWidth = 2.dp)
Spacer(modifier = Modifier.width(8.dp))
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
Box(
modifier = Modifier.size(18.dp),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
AppText(text = stringResource(R.string.is_loading))
}
}
@@ -59,7 +59,7 @@ import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.adaptiveHorizontalPadding
import io.legado.app.ui.widget.components.AppScaffold
import io.legado.app.ui.widget.components.EmptyMessage
import io.legado.app.ui.widget.components.SearchBarSection
import io.legado.app.ui.widget.components.SearchBar
import io.legado.app.ui.widget.components.button.MediumOutlinedButton
import io.legado.app.ui.widget.components.button.SmallAnimatedActionButton
import io.legado.app.ui.widget.components.button.SmallIconButton
@@ -167,7 +167,7 @@ fun SearchContentScreen(
Box(
modifier = Modifier.adaptiveHorizontalPadding()
) {
SearchBarSection(
SearchBar(
query = searchQuery,
scrollState = listState,
onQueryChange = { viewModel.onQueryChange(it) }
@@ -104,6 +104,7 @@ 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
import io.legado.app.ui.widget.components.button.SmallOutlinedIconToggleButton
import io.legado.app.ui.widget.components.card.NormalCard
import io.legado.app.ui.widget.components.card.TextCard
import io.legado.app.ui.widget.components.divider.PillDivider
import io.legado.app.ui.widget.components.divider.PillHeaderDivider
@@ -950,13 +951,13 @@ private fun StatusIcon(
}
"SUCCESS_WORD_COUNT" -> {
Surface(
shape = MaterialTheme.shapes.medium,
NormalCard(
cornerRadius = 12.dp,
border = BorderStroke(
1.dp,
LegadoTheme.colorScheme.outlineVariant
),
color = Color.Transparent
containerColor = Color.Transparent
) {
if (wordCount != null) {
AppText(
@@ -36,7 +36,9 @@ import coil.compose.AsyncImage
import io.legado.app.R
import io.legado.app.constant.PreferKey
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.widget.components.card.NormalCard
import io.legado.app.ui.widget.components.filePicker.FilePickerSheet
import io.legado.app.ui.widget.components.icon.AppIcon
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
import io.legado.app.ui.widget.components.text.AppText
@@ -87,9 +89,8 @@ fun CoverManageSheet(
) {
items(coverList) { path ->
Box {
Surface(
shape = MaterialTheme.shapes.medium,
tonalElevation = 2.dp
NormalCard(
cornerRadius = 12.dp
) {
AsyncImage(
model = path,
@@ -122,16 +123,16 @@ fun CoverManageSheet(
}
item {
Surface(
NormalCard(
onClick = { showFilePicker = true },
shape = MaterialTheme.shapes.medium,
color = MaterialTheme.colorScheme.surfaceContainerHigh,
cornerRadius = 12.dp,
containerColor = LegadoTheme.colorScheme.surfaceContainerHigh,
modifier = Modifier
.fillMaxWidth()
.aspectRatio(0.7f)
) {
Box(contentAlignment = Alignment.Center) {
Icon(
AppIcon(
imageVector = Icons.Default.Add,
contentDescription = "Add",
modifier = Modifier.size(32.dp),
@@ -32,6 +32,7 @@ import coil.compose.AsyncImage
import io.legado.app.R
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.widget.components.button.SmallTonalIconButton
import io.legado.app.ui.widget.components.card.NormalCard
import io.legado.app.ui.widget.components.filePicker.FilePickerSheet
import io.legado.app.ui.widget.components.icon.AppIcon
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
@@ -80,10 +81,10 @@ fun BackgroundImageManageSheet(
) {
if (currentPath.isNullOrBlank()) {
Surface(
NormalCard(
onClick = { showFilePicker = true },
shape = MaterialTheme.shapes.medium,
color = LegadoTheme.colorScheme.surfaceContainerHigh,
cornerRadius = 12.dp,
containerColor = LegadoTheme.colorScheme.surfaceContainerHigh,
modifier = Modifier
.fillMaxWidth()
.aspectRatio(16f / 9f)
@@ -104,9 +105,8 @@ fun BackgroundImageManageSheet(
.fillMaxWidth()
.padding(16.dp)
) {
Surface(
shape = MaterialTheme.shapes.medium,
tonalElevation = 2.dp
NormalCard(
cornerRadius = 12.dp,
) {
AsyncImage(
model = currentPath,
@@ -92,7 +92,7 @@ fun ThemeListDialog(
},
modifier = Modifier
.fillMaxWidth(),
shape = RoundedCornerShape(20.dp),
cornerRadius = 20.dp,
containerColor = if (item.primaryColor.toColorInt() == context.primaryColor) {
MaterialTheme.colorScheme.secondaryContainer
} else {
@@ -315,7 +315,6 @@ fun MainScreen(
AppNavigationBar(
modifier = Modifier
.regularHazeEffect(state = hazeState)
.height(if (isUnlabeled) 64.dp else 80.dp)
) {
destinations.forEachIndexed { index, destination ->
val selected = pagerState.targetPage == index
@@ -77,6 +77,7 @@ import io.legado.app.ui.theme.adaptiveHorizontalPadding
import io.legado.app.ui.theme.adaptiveHorizontalPaddingTab
import io.legado.app.ui.widget.components.EmptyMessage
import io.legado.app.ui.widget.components.button.SmallOutlinedIconToggleButton
import io.legado.app.ui.widget.components.card.NormalCard
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
@@ -568,9 +569,9 @@ fun BookshelfScreen(
if (uiState.isLoading) {
Dialog(onDismissRequest = {}) {
Surface(
shape = MaterialTheme.shapes.medium,
color = MaterialTheme.colorScheme.surfaceContainerHigh
NormalCard(
cornerRadius = 12.dp,
containerColor = LegadoTheme.colorScheme.surfaceContainerHigh
) {
Column(
modifier = Modifier.padding(24.dp),
@@ -345,7 +345,7 @@ fun ExploreSourceHeader(
modifier = modifier
.fillMaxWidth()
.padding(vertical = 4.dp),
shape = MaterialTheme.shapes.medium,
cornerRadius = 12.dp,
containerColor = containerColor
) {
ListItem(
@@ -180,7 +180,8 @@ fun AppTheme(
surfaceContainer = miuixColorScheme.surfaceContainer,
surfaceContainerHigh = miuixColorScheme.surfaceContainerHigh,
surfaceContainerHighest = miuixColorScheme.surfaceContainerHighest,
surfaceContainerLow = miuixColorScheme.secondaryContainer,
surfaceContainerLow = miuixColorScheme.secondaryContainer.copy(alpha = 0.32f)
.compositeOver(miuixColorScheme.surface),
surfaceContainerLowest = miuixColorScheme.background,
primaryFixed = miuixColorScheme.primaryContainer,
@@ -39,7 +39,7 @@ fun CollapsibleHeader(
modifier = modifier
.fillMaxWidth()
.adaptiveHorizontalPadding(vertical = 4.dp),
shape = MaterialTheme.shapes.medium,
cornerRadius = 12.dp,
containerColor = LegadoTheme.colorScheme.surfaceContainer,
onClick = onToggle
) {
@@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.input.TextFieldLineLimits
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -15,6 +16,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.dp
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.ThemeResolver
@@ -26,10 +28,11 @@ import top.yukonga.miuix.kmp.theme.MiuixTheme
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SearchBarSection(
fun SearchBar(
modifier: Modifier = Modifier,
query: String,
onQueryChange: (String) -> Unit,
onSearch: (String) -> Unit = {},
placeholder: String = "搜索...",
leadingIcon: @Composable (() -> Unit)? = {
AppIcon(
@@ -79,10 +82,15 @@ fun SearchBarSection(
placeholder = { AppText(placeholder) },
leadingIcon = leadingIcon,
trailingIcon = trailingIcon,
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
onKeyboardAction = {
onSearch(textFieldState.text.toString())
},
lineLimits = TextFieldLineLimits.SingleLine,
backgroundColor = resolvedBackgroundColor,
miuixUseSearchBarInputField = true,
miuixSearchBarLabel = placeholder
miuixSearchBarLabel = placeholder,
miuixOnSearch = onSearch,
)
} else {
Surface(
@@ -98,6 +106,10 @@ fun SearchBarSection(
placeholder = { AppText(placeholder) },
leadingIcon = leadingIcon,
trailingIcon = trailingIcon,
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
onKeyboardAction = {
onSearch(textFieldState.text.toString())
},
lineLimits = TextFieldLineLimits.SingleLine,
backgroundColor = Color.Transparent
)
@@ -17,6 +17,8 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Shuffle
import androidx.compose.material.icons.filled.SwapHoriz
import androidx.compose.material.icons.filled.Warning
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
@@ -59,7 +61,7 @@ fun SearchBookListItem(
BookShelfState.SAME_NAME_AUTHOR -> {
{
androidx.compose.material3.Icon(
imageVector = Icons.Default.Warning,
imageVector = Icons.Default.Shuffle,
contentDescription = "同名书籍",
modifier = Modifier.size(12.dp),
)
@@ -1,17 +1,20 @@
package io.legado.app.ui.widget.components.card
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CardElevation
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import io.legado.app.ui.config.themeConfig.ThemeConfig
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.ThemeResolver
@@ -25,12 +28,12 @@ import top.yukonga.miuix.kmp.basic.CardDefaults as MiuixCardDefaults
private fun BaseCard(
modifier: Modifier = Modifier,
onClick: (() -> Unit)? = null,
shape: Shape = CardDefaults.shape,
onLongClick: (() -> Unit)? = null,
cornerRadius: Dp = MiuixCardDefaults.CornerRadius,
pressFeedbackType: PressFeedbackType = PressFeedbackType.None,
containerColor: Color? = null,
contentColor: Color? = null,
elevation: CardElevation = CardDefaults.cardElevation(),
elevation: Dp = 0.dp,
border: BorderStroke? = null,
alpha: Float = 1f,
content: @Composable ColumnScope.() -> Unit
@@ -47,6 +50,7 @@ private fun BaseCard(
pressFeedbackType = pressFeedbackType,
showIndication = true,
onClick = onClick,
onLongPress = onLongClick,
content = content,
colors = colors
)
@@ -67,25 +71,26 @@ private fun BaseCard(
disabledContainerColor = LegadoTheme.colorScheme.onSecondaryContainer.copy(alpha = alpha * 0.38f),
disabledContentColor = LegadoTheme.colorScheme.onSecondaryContainer.copy(alpha = alpha * 0.38f)
)
if (onClick != null) {
Card(
modifier = modifier,
shape = RoundedCornerShape(cornerRadius),
colors = colors,
elevation = elevation,
border = border,
onClick = onClick,
content = content
)
val clickableModifier = if (onClick != null || onLongClick != null) {
modifier
.clip(RoundedCornerShape(cornerRadius))
.combinedClickable(
onClick = { onClick?.invoke() },
onLongClick = onLongClick
)
} else {
Card(
modifier = modifier,
shape = RoundedCornerShape(cornerRadius),
colors = colors,
elevation = elevation,
border = border,
content = content
)
modifier
}
Surface(
modifier = clickableModifier,
shape = RoundedCornerShape(cornerRadius),
color = colors.containerColor,
contentColor = colors.contentColor,
tonalElevation = 0.dp,
shadowElevation = elevation,
border = border
) {
Column(content = content)
}
}
}
@@ -95,19 +100,19 @@ private fun BaseCard(
fun GlassCard(
modifier: Modifier = Modifier,
onClick: (() -> Unit)? = null,
shape: Shape = CardDefaults.shape,
onLongClick: (() -> Unit)? = null,
cornerRadius: Dp = MiuixCardDefaults.CornerRadius,
pressFeedbackType: PressFeedbackType = PressFeedbackType.None,
containerColor: Color? = null,
contentColor: Color? = null,
elevation: CardElevation = CardDefaults.cardElevation(),
elevation: Dp = 0.dp,
border: BorderStroke? = null,
content: @Composable ColumnScope.() -> Unit
) {
BaseCard(
modifier = modifier,
onClick = onClick,
shape = shape,
onLongClick = onLongClick,
cornerRadius = cornerRadius,
pressFeedbackType = pressFeedbackType,
containerColor = containerColor,
@@ -124,19 +129,19 @@ fun GlassCard(
fun NormalCard(
modifier: Modifier = Modifier,
onClick: (() -> Unit)? = null,
shape: Shape = CardDefaults.shape,
onLongClick: (() -> Unit)? = null,
cornerRadius: Dp = MiuixCardDefaults.CornerRadius,
pressFeedbackType: PressFeedbackType = PressFeedbackType.None,
containerColor: Color? = null,
contentColor: Color? = null,
elevation: CardElevation = CardDefaults.cardElevation(),
elevation: Dp = 0.dp,
border: BorderStroke? = null,
content: @Composable ColumnScope.() -> Unit
) {
BaseCard(
modifier = modifier,
onClick = onClick,
shape = shape,
onLongClick = onLongClick,
cornerRadius = cornerRadius,
pressFeedbackType = pressFeedbackType,
containerColor = containerColor,
@@ -19,7 +19,6 @@ import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Checkbox
import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults
@@ -88,9 +87,9 @@ fun SelectionItemCard(
onClick = onToggleSelection,
modifier = modifier
.fillMaxWidth(),
shape = MaterialTheme.shapes.medium,
cornerRadius = 12.dp,
containerColor = animatedContainerColor,
elevation = CardDefaults.cardElevation(defaultElevation = elevation)
elevation = elevation
) {
SelectionItemCardContent(
title = title,
@@ -36,12 +36,12 @@ fun ExploreKindItem(
LocalMinimumInteractiveComponentSize provides Dp.Unspecified
) {
val shape = MaterialTheme.shapes.medium
val cornerRadius = 12.dp
if (isClickable) {
GlassCard(
onClick = onClick,
shape = shape,
cornerRadius = cornerRadius,
containerColor = backgroundColor,
contentColor = LegadoTheme.colorScheme.onSurface,
modifier = modifier
@@ -54,7 +54,7 @@ fun ExploreKindItem(
}
} else {
GlassCard(
shape = shape,
cornerRadius = cornerRadius,
containerColor = backgroundColor,
contentColor = LegadoTheme.colorScheme.primary,
modifier = modifier,
@@ -18,6 +18,7 @@ import androidx.compose.ui.Modifier
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.widget.components.card.NormalCard
import io.legado.app.ui.widget.components.icon.AppIcon
import io.legado.app.ui.widget.components.text.AppText
@@ -58,14 +59,14 @@ fun RowScope.OptionCard(
text: String,
onClick: () -> Unit
) {
Surface(
NormalCard(
onClick = onClick,
modifier = Modifier
.weight(1f)
.height(100.dp),
shape = MaterialTheme.shapes.medium,
color = LegadoTheme.colorScheme.surfaceContainerLow,
tonalElevation = 2.dp
cornerRadius = 12.dp,
containerColor = LegadoTheme.colorScheme.surfaceContainerLow,
elevation = 2.dp
) {
Column(
modifier = Modifier.fillMaxSize(),
@@ -21,7 +21,7 @@ import androidx.compose.runtime.setValue
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.SearchBarSection
import io.legado.app.ui.widget.components.SearchBar
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.icon.AppIcons
@@ -107,7 +107,7 @@ fun <T> DynamicTopAppBar(
enter = expandVertically() + fadeIn(),
exit = shrinkVertically() + fadeOut()
) {
SearchBarSection(
SearchBar(
query = state.searchKey,
onQueryChange = onSearchQueryChange,
placeholder = searchPlaceholder,
@@ -246,6 +246,7 @@
<string name="threads_num_title">更新和搜索线程数</string>
<string name="change_icon">切换图标</string>
<string name="remove_from_bookshelf">已在书架</string>
<string name="already_in_bookshelf">已在书架</string>
<string name="start_read">开始阅读</string>
<string name="data_loading">加载数据中…</string>
<string name="load_error_retry">加载失败</string>
@@ -395,6 +396,7 @@
<string name="imm_navigation_bar_s">导航栏颜色透明</string>
<string name="add_to_bookshelf">放入书架</string>
<string name="click_to_remove">点击删除</string>
<string name="long_press_group">长按分组</string>
<string name="continue_read">继续阅读</string>
<string name="cover_path">封面地址</string>
<string name="page_anim_cover">覆盖</string>
@@ -223,6 +223,7 @@
<string name="threads_num_title">更新/搜尋線程數,太多會卡頓</string>
<string name="change_icon">切換圖標</string>
<string name="remove_from_bookshelf">刪除書輯</string>
<string name="already_in_bookshelf">已在書架</string>
<string name="start_read">開始閲讀</string>
<string name="data_loading">數據載入中…</string>
<string name="load_error_retry">載入失敗,點擊重試</string>
@@ -225,6 +225,7 @@
<string name="threads_num_title">更新和搜尋執行緒數,太多會卡頓</string>
<string name="change_icon">切換圖示</string>
<string name="remove_from_bookshelf">刪除書籍</string>
<string name="already_in_bookshelf">已在書架</string>
<string name="start_read">開始閱讀</string>
<string name="data_loading">載入資料中…</string>
<string name="load_error_retry">載入失敗,點擊重試</string>
@@ -373,6 +374,7 @@
<string name="imm_navigation_bar_s">導航欄顏色透明</string>
<string name="add_to_bookshelf">放入書架</string>
<string name="click_to_remove">點擊刪除</string>
<string name="long_press_group">長按分組</string>
<string name="continue_read">繼續閱讀</string>
<string name="cover_path">封面地址</string>
<string name="page_anim_cover">覆蓋</string>
+2
View File
@@ -249,6 +249,7 @@
<string name="threads_num_title">Number of concurrent tasks</string>
<string name="change_icon">Change icon</string>
<string name="remove_from_bookshelf">In library</string>
<string name="already_in_bookshelf">Already in Bookshelf</string>
<string name="start_read">Start reading</string>
<string name="data_loading">Loading…</string>
<string name="load_error_retry">Load failed, tap to retry</string>
@@ -398,6 +399,7 @@
<string name="imm_navigation_bar_s">The navigation bar becomes transparent</string>
<string name="add_to_bookshelf">Add to Bookshelf</string>
<string name="click_to_remove">Tap to remove</string>
<string name="long_press_group">Long press to group</string>
<string name="continue_read">Continue reading</string>
<string name="cover_path">Cover path</string>
<string name="page_anim_cover">Cover</string>