[优化] 优化界面

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,