From c6ed112aa388ebdc13526c251c45080914a2821f Mon Sep 17 00:00:00 2001 From: HapeLee <63206378+HapeLee@users.noreply.github.com> Date: Sat, 18 Apr 2026 02:18:42 +0800 Subject: [PATCH] =?UTF-8?q?[=E4=BC=98=E5=8C=96]=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E7=95=8C=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/io/legado/app/di/appModule.kt | 5 + .../app/ui/book/bookmark/AllBookmarkScreen.kt | 7 +- .../app/ui/book/explore/ExploreShowScreen.kt | 180 +---- .../app/ui/book/group/GroupEditSheet.kt | 221 +++--- .../ui/book/import/local/ImportBookScreen.kt | 20 +- .../ui/book/import/remote/RemoteBookScreen.kt | 75 +- .../legado/app/ui/book/info/BookInfoScreen.kt | 241 ++++--- .../legado/app/ui/book/info/BookInfoSheets.kt | 91 +-- .../ui/book/readRecord/ReadRecordScreen.kt | 7 +- .../app/ui/book/search/SearchActivity.kt | 673 +----------------- .../legado/app/ui/book/search/SearchScope.kt | 93 ++- .../app/ui/book/search/SearchViewModel.kt | 531 ++++++++++---- .../book/searchContent/SearchContentScreen.kt | 13 +- .../io/legado/app/ui/book/toc/TocScreen.kt | 4 +- .../app/ui/book/toc/rule/TxtRuleScreen.kt | 11 +- .../otherConfig/CheckSourceBottomSheet.kt | 162 ++--- .../DirectLinkUploadBottomSheet.kt | 131 ++-- .../app/ui/config/readConfig/PageKeySheet.kt | 68 +- .../themeConfig/BackgroundImageManageSheet.kt | 34 +- .../legado/app/ui/dict/rule/DictRuleScreen.kt | 3 +- .../legado/app/ui/main/bookshelf/BookItem.kt | 2 + .../app/ui/main/bookshelf/BookshelfScreen.kt | 28 +- .../app/ui/main/bookshelf/GroupManageSheet.kt | 34 +- .../app/ui/main/explore/ExploreScreen.kt | 14 + .../app/ui/replace/edit/ReplaceEditScreen.kt | 14 +- .../ui/rss/favorites/RssFavoritesScreen.kt | 6 +- .../app/ui/rss/subscription/RuleSubScreen.kt | 7 +- .../io/legado/app/ui/theme/AdaptivePadding.kt | 7 + .../java/io/legado/app/ui/theme/AppTheme.kt | 37 +- .../java/io/legado/app/ui/theme/HazeStyle.kt | 23 + .../app/ui/theme/ImageSeedColorExtractor.kt | 8 +- .../io/legado/app/ui/theme/LegadoTheme.kt | 3 +- .../app/ui/theme/ThemeColorSchemeOverride.kt | 3 +- .../io/legado/app/ui/theme/ThemeOverride.kt | 18 +- .../java/io/legado/app/ui/theme/Typography.kt | 30 + .../app/ui/theme/hazeStyle/HazeLegado.kt | 10 + .../components/AppLinearProgressIndicator.kt | 34 + .../app/ui/widget/components/AppSearchBar.kt | 168 +++++ .../app/ui/widget/components/AppTextField.kt | 40 +- .../{EmptyMessageView.kt => EmptyMessage.kt} | 6 +- .../ui/widget/components/SearchBarSection.kt | 42 +- .../widget/components/alert/AppAlertDialog.kt | 2 + .../components/button/SmallIconButton.kt | 3 +- .../components/button/SmallTextButton.kt | 37 +- .../ui/widget/components/button/ToggleChip.kt | 10 +- .../ui/widget/components/card/GlassCard.kt | 32 +- .../components/card/SelectionItemCard.kt | 221 +++--- .../app/ui/widget/components/card/TextCard.kt | 3 +- .../components/checkBox/CheckboxItem.kt | 53 +- .../ui/widget/components/cover/BookCover.kt | 154 ++-- .../widget/components/cover/BookshelfCover.kt | 13 +- .../app/ui/widget/components/cover/Cover.kt | 19 +- .../components/cover/CoverImageRequest.kt | 21 + .../app/ui/widget/components/icon/AppIcons.kt | 48 ++ .../components/menuItem/RoundDropdownMenu.kt | 2 +- .../menuItem/RoundDropdownMenuItem.kt | 2 +- .../components/rules/RuleListScaffold.kt | 2 +- .../settingItem/SliderSettingItem.kt | 10 +- .../components/topbar/DynamicTopAppBar.kt | 2 +- gradle/libs.versions.toml | 2 +- 60 files changed, 1908 insertions(+), 1832 deletions(-) create mode 100644 app/src/main/java/io/legado/app/ui/widget/components/AppLinearProgressIndicator.kt create mode 100644 app/src/main/java/io/legado/app/ui/widget/components/AppSearchBar.kt rename app/src/main/java/io/legado/app/ui/widget/components/{EmptyMessageView.kt => EmptyMessage.kt} (98%) create mode 100644 app/src/main/java/io/legado/app/ui/widget/components/cover/CoverImageRequest.kt diff --git a/app/src/main/java/io/legado/app/di/appModule.kt b/app/src/main/java/io/legado/app/di/appModule.kt index fe6939991..195ecddfe 100644 --- a/app/src/main/java/io/legado/app/di/appModule.kt +++ b/app/src/main/java/io/legado/app/di/appModule.kt @@ -13,6 +13,8 @@ import io.legado.app.data.repository.ExploreRepository import io.legado.app.data.repository.ExploreRepositoryImpl import io.legado.app.data.repository.ReadRecordRepository import io.legado.app.data.repository.RemoteBookRepository +import io.legado.app.data.repository.SearchRepository +import io.legado.app.data.repository.SearchRepositoryImpl import io.legado.app.data.repository.SearchContentRepository import io.legado.app.data.repository.UploadRepository import io.legado.app.domain.usecase.ExploreKindUiUseCase @@ -30,6 +32,7 @@ import io.legado.app.ui.book.import.local.ImportBookViewModel import io.legado.app.ui.book.import.remote.RemoteBookViewModel import io.legado.app.ui.book.info.BookInfoViewModel import io.legado.app.ui.book.readRecord.ReadRecordViewModel +import io.legado.app.ui.book.search.SearchViewModel import io.legado.app.ui.book.searchContent.SearchContentViewModel import io.legado.app.ui.book.toc.TocViewModel import io.legado.app.ui.book.toc.rule.TxtTocRuleViewModel @@ -69,6 +72,7 @@ val appModule = module { single { DirectLinkUploadRepository() } single { ExploreRepositoryImpl(get()) } + single { SearchRepositoryImpl(get()) } single { ImageLoader.Builder(get()) @@ -111,6 +115,7 @@ val appModule = module { viewModelOf(::ChangeBookSourceViewModel) viewModelOf(::ExploreViewModel) viewModelOf(::RssViewModel) + viewModelOf(::SearchViewModel) viewModel { (route: ReplaceEditRoute) -> ReplaceEditViewModel( diff --git a/app/src/main/java/io/legado/app/ui/book/bookmark/AllBookmarkScreen.kt b/app/src/main/java/io/legado/app/ui/book/bookmark/AllBookmarkScreen.kt index 31bf37a06..ddfe1ec9f 100644 --- a/app/src/main/java/io/legado/app/ui/book/bookmark/AllBookmarkScreen.kt +++ b/app/src/main/java/io/legado/app/ui/book/bookmark/AllBookmarkScreen.kt @@ -50,10 +50,9 @@ import io.legado.app.data.entities.Bookmark import io.legado.app.ui.theme.LegadoTheme import io.legado.app.ui.theme.ThemeResolver 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.AppScaffold -import io.legado.app.ui.widget.components.EmptyMessageView +import io.legado.app.ui.widget.components.EmptyMessage import io.legado.app.ui.widget.components.SearchBarSection import io.legado.app.ui.widget.components.bookmark.BookmarkEditSheet import io.legado.app.ui.widget.components.bookmark.BookmarkItem @@ -212,7 +211,7 @@ fun AllBookmarkScreen( ) { state -> when (state) { "LOADING" -> { - EmptyMessageView( + EmptyMessage( message = "加载中...", isLoading = true, modifier = Modifier @@ -225,7 +224,7 @@ fun AllBookmarkScreen( } "EMPTY" -> { - EmptyMessageView( + EmptyMessage( message = "没有书签!", modifier = Modifier .fillMaxSize() diff --git a/app/src/main/java/io/legado/app/ui/book/explore/ExploreShowScreen.kt b/app/src/main/java/io/legado/app/ui/book/explore/ExploreShowScreen.kt index 229275c88..ce14b3329 100644 --- a/app/src/main/java/io/legado/app/ui/book/explore/ExploreShowScreen.kt +++ b/app/src/main/java/io/legado/app/ui/book/explore/ExploreShowScreen.kt @@ -92,6 +92,8 @@ import io.legado.app.ui.widget.components.explore.ExploreKindMultiTypeItem import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet +import io.legado.app.ui.widget.components.book.SearchBookGridItem +import io.legado.app.ui.widget.components.book.SearchBookListItem 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 @@ -521,111 +523,12 @@ fun ExploreBookItem( onClick: () -> Unit, modifier: Modifier = Modifier ) { - val shelfState by shelfState.collectAsState(initial = BookShelfState.NOT_IN_SHELF) - - val badge: (@Composable RowScope.() -> Unit)? - = when (shelfState) { - - BookShelfState.IN_SHELF -> { - { - Icon( - imageVector = Icons.Default.Check, - contentDescription = "已在书架", - modifier = Modifier.size(12.dp) - ) - } - } - - BookShelfState.SAME_NAME_AUTHOR -> { - { - Icon( - imageVector = Icons.Default.Warning, - contentDescription = "同名书籍", - modifier = Modifier.size(12.dp) - ) - - } - } - - BookShelfState.NOT_IN_SHELF -> null - } - - Row( + SearchBookListItem( + book = book, + shelfState = shelfState, + onClick = onClick, modifier = modifier - .fillMaxWidth() - .clickable(onClick = onClick) - .padding(horizontal = 16.dp, vertical = 8.dp) - ) { - - Cover( - path = book.coverUrl, - modifier = Modifier.width(72.dp), - badgeContent = badge) - - Spacer(modifier = Modifier.width(8.dp)) - - Column(modifier = Modifier - .weight(1f) - .align(Alignment.CenterVertically)) { - - AppText( - text = book.name, - style = LegadoTheme.typography.titleSmall, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - - Row { - AppText( - text = book.author, - style = LegadoTheme.typography.bodySmall, - maxLines = 1 - ) - - val latestChapter = book.latestChapterTitle - if (!latestChapter.isNullOrEmpty()) { - AppText( - text = " • ", - style = LegadoTheme.typography.bodySmall, - color = Color.Gray, - maxLines = 1 - ) - - AppText( - text = "最新: $latestChapter", - style = LegadoTheme.typography.bodySmall, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - } - } - - Spacer(modifier = Modifier.height(4.dp)) - - val intro = book.intro?.replace("\\s+".toRegex(), "") ?: "" - if (intro.isNotEmpty()) { - AppText( - text = intro, - style = LegadoTheme.typography.labelSmall, - color = Color.Gray, - maxLines = 2, - minLines = 2, - overflow = TextOverflow.Ellipsis - ) - } - - val kinds = book.getKindList() - if (kinds.isNotEmpty()) { - Spacer(modifier = Modifier.height(4.dp)) - Row(modifier = Modifier.horizontalScroll(rememberScrollState())) { - kinds.forEach { kind -> - TagChip(text = kind) - Spacer(modifier = Modifier.width(6.dp)) - } - } - } - } - } + ) } @Composable @@ -635,71 +538,12 @@ fun ExploreBookGridItem( shelfState: Flow, modifier: Modifier = Modifier ) { - - val shelfState by shelfState.collectAsState(initial = BookShelfState.NOT_IN_SHELF) - - val badgeText: String? = when (shelfState) { - BookShelfState.IN_SHELF -> "已在书架" - BookShelfState.SAME_NAME_AUTHOR -> "同名书籍" - BookShelfState.NOT_IN_SHELF -> null - } - - val content: (@Composable RowScope.() -> Unit)? = if (!badgeText.isNullOrBlank()) { - { - AppText( - text = badgeText, - style = LegadoTheme.typography.labelSmall.copy( - fontWeight = FontWeight.Bold, - fontSize = 9.sp - ) - ) - } - } else { - null - } - - Column( + SearchBookGridItem( + book = book, + shelfState = shelfState, + onClick = onClick, modifier = modifier - .width(IntrinsicSize.Min) - .clip(RoundedCornerShape(4.dp)) - .clickable(onClick = onClick) - .padding(4.dp) - ) { - - Cover( - path = book.coverUrl, - modifier = Modifier - .fillMaxWidth() - .aspectRatio(12 / 17f), - badgeContent = content - ) - - Spacer(modifier = Modifier.height(4.dp)) - - AppText( - text = book.name, - style = LegadoTheme.typography.bodySmall, - fontWeight = FontWeight.Bold, - maxLines = 2, - overflow = TextOverflow.Ellipsis - ) - } -} - -// 简单的标签组件 -@Composable -fun TagChip(text: String) { - Surface( - color = LegadoTheme.colorScheme.cardContainer, - shape = RoundedCornerShape(4.dp) - ) { - AppText( - text = text, - modifier = Modifier.padding(horizontal = 4.dp, vertical = 2.dp), - style = LegadoTheme.typography.labelSmall, - color = LegadoTheme.colorScheme.onCardContainer - ) - } + ) } @OptIn(ExperimentalMaterial3ExpressiveApi::class) diff --git a/app/src/main/java/io/legado/app/ui/book/group/GroupEditSheet.kt b/app/src/main/java/io/legado/app/ui/book/group/GroupEditSheet.kt index 39330c835..2239457e3 100644 --- a/app/src/main/java/io/legado/app/ui/book/group/GroupEditSheet.kt +++ b/app/src/main/java/io/legado/app/ui/book/group/GroupEditSheet.kt @@ -8,18 +8,16 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Restore -import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon -import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedIconButton -import androidx.compose.material3.OutlinedTextField import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf @@ -34,12 +32,16 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import io.legado.app.R import io.legado.app.data.entities.BookGroup -import io.legado.app.lib.dialogs.alert +import io.legado.app.ui.theme.LegadoTheme +import io.legado.app.ui.widget.components.AppTextField +import io.legado.app.ui.widget.components.alert.AppAlertDialog +import io.legado.app.ui.widget.components.button.ConfirmDismissButtonsRow +import io.legado.app.ui.widget.components.button.MediumIconButton +import io.legado.app.ui.widget.components.card.GlassCard import io.legado.app.ui.widget.components.cover.BookCover import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet import io.legado.app.ui.widget.components.settingItem.CompactDropdownSettingItem import io.legado.app.ui.widget.components.settingItem.CompactSwitchSettingItem -import io.legado.app.ui.widget.components.text.AppText import io.legado.app.utils.FileUtils import io.legado.app.utils.MD5Utils import io.legado.app.utils.SelectImageContract @@ -58,10 +60,35 @@ fun GroupEditSheet( onDismissRequest: () -> Unit, viewModel: GroupViewModel = koinViewModel() ) { - AppModalBottomSheet(show = show, onDismissRequest = onDismissRequest) { + var coverPath by remember(group) { mutableStateOf(group?.cover) } + + AppModalBottomSheet( + show = show, + onDismissRequest = onDismissRequest, + startAction = if (group != null && (group.groupId > 0 || group.groupId == Long.MIN_VALUE)) { + { + GroupDeleteAction( + group = group, + onDismissRequest = onDismissRequest, + viewModel = viewModel + ) + } + } else { + null + }, + endAction = { + GroupResetCoverAction( + group = group, + onCoverPathChange = { coverPath = it }, + viewModel = viewModel + ) + } + ) { GroupEditContent( group = group, onDismissRequest = onDismissRequest, + coverPath = coverPath, + onCoverPathChange = { coverPath = it }, viewModel = viewModel ) } @@ -71,11 +98,12 @@ fun GroupEditSheet( fun GroupEditContent( group: BookGroup? = null, onDismissRequest: () -> Unit, + coverPath: String?, + onCoverPathChange: (String?) -> Unit, viewModel: GroupViewModel = koinViewModel() ) { val context = LocalContext.current var groupName by remember(group) { mutableStateOf(group?.groupName ?: "") } - var coverPath by remember(group) { mutableStateOf(group?.cover) } var enableRefresh by remember(group) { mutableStateOf(group?.enableRefresh ?: true) } var selectedSortIndex by remember(group) { mutableIntStateOf(group?.bookSort ?: -1) } @@ -96,7 +124,7 @@ fun GroupEditContent( FileOutputStream(file).use { output -> context.contentResolver.openInputStream(uri)?.use { it.copyTo(output) } } - coverPath = file.absolutePath + onCoverPathChange(file.absolutePath) } } catch (e: Exception) { appCtx.toastOnUi(e.localizedMessage) @@ -107,6 +135,7 @@ fun GroupEditContent( Column( modifier = Modifier .fillMaxWidth() + .padding(bottom = 16.dp) .verticalScroll(rememberScrollState()), horizontalAlignment = Alignment.CenterHorizontally, ) { @@ -129,23 +158,29 @@ fun GroupEditContent( modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(8.dp) ) { - OutlinedTextField( + AppTextField( value = groupName, onValueChange = { groupName = it }, - label = { AppText(stringResource(R.string.group_name)) }, + backgroundColor = LegadoTheme.colorScheme.onSheetContent, + label = stringResource(R.string.group_name), modifier = Modifier.fillMaxWidth(), singleLine = true ) - CompactDropdownSettingItem( - title = stringResource(R.string.sort), - selectedValue = selectedSortIndex.toString(), - displayEntries = sortOptions, - entryValues = sortEntryValues, - onValueChange = { - selectedSortIndex = it.toInt() - } - ) + GlassCard( + containerColor = LegadoTheme.colorScheme.onSheetContent, + ) { + CompactDropdownSettingItem( + title = stringResource(R.string.sort), + selectedValue = selectedSortIndex.toString(), + color = LegadoTheme.colorScheme.onSheetContent, + displayEntries = sortOptions, + entryValues = sortEntryValues, + onValueChange = { + selectedSortIndex = it.toInt() + } + ) + } } } @@ -159,79 +194,91 @@ fun GroupEditContent( Spacer(modifier = Modifier.height(16.dp)) - Row( + ConfirmDismissButtonsRow( modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - if (group != null && (group.groupId > 0 || group.groupId == Long.MIN_VALUE)) { - OutlinedIconButton(onClick = { - context.alert(R.string.delete, R.string.sure_del) { - yesButton { - viewModel.delGroup(group) { - onDismissRequest() - } - } - noButton() - } - }) { - Icon( - Icons.Default.Delete, - contentDescription = stringResource(R.string.delete) - ) - } - } - - OutlinedIconButton(onClick = { - if (group != null) { - viewModel.clearCover(group) { - coverPath = null - appCtx.toastOnUi("封面已重置") + onDismiss = onDismissRequest, + onConfirm = { + if (groupName.isEmpty()) { + appCtx.toastOnUi("分组名称不能为空") + } else { + if (group != null && group.groupId != 0b1L) { + viewModel.upGroup( + group.copy( + groupName = groupName, + cover = coverPath, + bookSort = selectedSortIndex, + enableRefresh = enableRefresh + ) + ) { + onDismissRequest() } } else { - coverPath = null - } - }) { - Icon(Icons.Default.Restore, contentDescription = stringResource(R.string.reset)) - } - } - - Spacer(modifier = Modifier.weight(1f)) - - Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - OutlinedButton(onClick = onDismissRequest) { - AppText(stringResource(R.string.cancel)) - } - Button(onClick = { - if (groupName.isEmpty()) { - appCtx.toastOnUi("分组名称不能为空") - } else { - if (group != null) { - viewModel.upGroup( - group.copy( - groupName = groupName, - cover = coverPath, - bookSort = selectedSortIndex, - enableRefresh = enableRefresh - ) - ) { - onDismissRequest() - } - } else { - viewModel.addGroup( - groupName, - selectedSortIndex, - enableRefresh, - coverPath - ) { - onDismissRequest() - } + viewModel.addGroup( + groupName, + selectedSortIndex, + enableRefresh, + coverPath + ) { + onDismissRequest() } } - }) { - AppText(stringResource(R.string.ok)) } - } - } + }, + dismissText = stringResource(R.string.cancel), + confirmText = stringResource(R.string.ok) + ) } } + +@Composable +fun GroupDeleteAction( + group: BookGroup, + onDismissRequest: () -> Unit, + viewModel: GroupViewModel = koinViewModel() +) { + var showDeleteDialog by remember { mutableStateOf(false) } + + MediumIconButton( + onClick = { + showDeleteDialog = true + }, + imageVector = Icons.Default.Delete + ) + + AppAlertDialog( + show = showDeleteDialog, + onDismissRequest = { showDeleteDialog = false }, + title = stringResource(R.string.delete), + text = stringResource(R.string.sure_del), + confirmText = stringResource(android.R.string.ok), + onConfirm = { + showDeleteDialog = false + viewModel.delGroup(group) { + onDismissRequest() + } + }, + dismissText = stringResource(android.R.string.cancel), + onDismiss = { showDeleteDialog = false } + ) +} + +@Composable +fun GroupResetCoverAction( + group: BookGroup? = null, + onCoverPathChange: (String?) -> Unit, + viewModel: GroupViewModel = koinViewModel() +) { + MediumIconButton( + onClick = { + if (group != null) { + viewModel.clearCover(group) { + onCoverPathChange(null) + appCtx.toastOnUi("封面已重置") + } + } else { + onCoverPathChange(null) + } + }, + imageVector = Icons.Default.Restore + ) +} diff --git a/app/src/main/java/io/legado/app/ui/book/import/local/ImportBookScreen.kt b/app/src/main/java/io/legado/app/ui/book/import/local/ImportBookScreen.kt index a8322569f..7e11f31b1 100644 --- a/app/src/main/java/io/legado/app/ui/book/import/local/ImportBookScreen.kt +++ b/app/src/main/java/io/legado/app/ui/book/import/local/ImportBookScreen.kt @@ -1,6 +1,5 @@ package io.legado.app.ui.book.import.local -import android.net.Uri import androidx.activity.compose.BackHandler import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts @@ -29,7 +28,6 @@ import androidx.compose.material.icons.filled.CloudDownload import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Folder import androidx.compose.material.icons.filled.FolderOpen -import androidx.compose.material.icons.outlined.AddCircleOutline import androidx.compose.material.icons.outlined.Book import androidx.compose.material.icons.outlined.Description import androidx.compose.material3.CircularProgressIndicator @@ -53,23 +51,15 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import androidx.core.net.toUri -import androidx.documentfile.provider.DocumentFile import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.legado.app.R import io.legado.app.constant.AppConst -import io.legado.app.constant.AppPattern -import io.legado.app.data.appDb -import io.legado.app.data.entities.Book import io.legado.app.ui.config.importBookConfig.ImportBookConfig import io.legado.app.lib.dialogs.alert import io.legado.app.lib.dialogs.selector -import io.legado.app.lib.permission.Permissions -import io.legado.app.lib.permission.PermissionsCompat -import io.legado.app.model.localBook.LocalBook import io.legado.app.ui.theme.LegadoTheme import io.legado.app.ui.widget.components.ActionItem -import io.legado.app.ui.widget.components.EmptyMessageView +import io.legado.app.ui.widget.components.EmptyMessage import io.legado.app.ui.widget.components.SelectionActions import io.legado.app.ui.widget.components.alert.AppAlertDialog import io.legado.app.ui.widget.components.button.SmallIconButton @@ -83,15 +73,9 @@ import io.legado.app.ui.widget.components.list.ListScaffold import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem import io.legado.app.ui.widget.components.text.AppText import io.legado.app.utils.ConvertUtils -import io.legado.app.utils.ArchiveUtils -import io.legado.app.utils.FileDoc -import io.legado.app.utils.isContentScheme -import io.legado.app.utils.isUri import io.legado.app.utils.startActivityForBook -import io.legado.app.utils.takePersistablePermissionSafely import io.legado.app.utils.toastOnUi import org.koin.androidx.compose.koinViewModel -import java.io.File @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -222,7 +206,7 @@ private fun ImportBookContent( } state.items.isEmpty() -> { - EmptyMessageView( + EmptyMessage( modifier = Modifier.fillMaxSize(), message = stringResource(R.string.empty_msg_import_book) ) diff --git a/app/src/main/java/io/legado/app/ui/book/import/remote/RemoteBookScreen.kt b/app/src/main/java/io/legado/app/ui/book/import/remote/RemoteBookScreen.kt index 7f9d30584..a9540d6c8 100644 --- a/app/src/main/java/io/legado/app/ui/book/import/remote/RemoteBookScreen.kt +++ b/app/src/main/java/io/legado/app/ui/book/import/remote/RemoteBookScreen.kt @@ -39,16 +39,11 @@ import androidx.compose.material.icons.outlined.AddCircleOutline import androidx.compose.material.icons.outlined.Book import androidx.compose.material.icons.outlined.CloudSync import androidx.compose.material.icons.outlined.Description -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Button import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.TextButton import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState import androidx.compose.runtime.Composable @@ -80,11 +75,14 @@ import io.legado.app.ui.theme.LegadoTheme import io.legado.app.ui.widget.components.ActionItem import io.legado.app.ui.widget.components.AppRadioButton import io.legado.app.ui.widget.components.AppTextField -import io.legado.app.ui.widget.components.EmptyMessageView +import io.legado.app.ui.widget.components.EmptyMessage import io.legado.app.ui.widget.components.SelectionActions import io.legado.app.ui.widget.components.alert.AppAlertDialog +import io.legado.app.ui.widget.components.button.ConfirmDismissButtonsRow +import io.legado.app.ui.widget.components.button.MediumIconButton import io.legado.app.ui.widget.components.button.SmallIconButton import io.legado.app.ui.widget.components.button.SmallTonalIconButton +import io.legado.app.ui.widget.components.button.TopBarActionButton import io.legado.app.ui.widget.components.card.GlassCard import io.legado.app.ui.widget.components.card.SelectionItemCard import io.legado.app.ui.widget.components.card.TextCard @@ -93,7 +91,6 @@ import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet import io.legado.app.ui.widget.components.text.AppText import io.legado.app.utils.ConvertUtils -import io.legado.app.utils.FileDoc import io.legado.app.utils.isUri import io.legado.app.utils.startActivityForBook import io.legado.app.utils.toastOnUi @@ -173,9 +170,10 @@ fun RemoteBookScreen( }, endAction = if (showSheet is RemoteBookSheet.Servers) { { - IconButton(onClick = { showSheet = RemoteBookSheet.ServerConfig(null) }) { - Icon(Icons.Default.Add, contentDescription = stringResource(R.string.add)) - } + MediumIconButton( + onClick = { showSheet = RemoteBookSheet.ServerConfig(null) }, + imageVector = Icons.Default.Add + ) } } else { null @@ -265,9 +263,10 @@ fun RemoteBookScreen( onSearchQueryChange = { viewModel.dispatch(RemoteBookIntent.SearchChange(it)) }, searchPlaceholder = "搜索", topBarActions = { - IconButton(onClick = { showSheet = RemoteBookSheet.Servers }) { - Icon(Icons.Default.Storage, contentDescription = "配置服务器") - } + TopBarActionButton( + onClick = { showSheet = RemoteBookSheet.Servers }, + imageVector = Icons.Default.Storage + ) }, dropDownMenuContent = { dismiss -> RoundDropdownMenuItem( @@ -335,7 +334,7 @@ fun RemoteBookScreen( if (uiState.isLoading) { CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) } else { - EmptyMessageView( + EmptyMessage( modifier = Modifier .fillMaxSize() .verticalScroll(rememberScrollState()), @@ -453,7 +452,7 @@ private fun ServerItem( } else { null }, - containerColor = LegadoTheme.colorScheme.surface, + containerColor = LegadoTheme.colorScheme.onSheetContent, selectedContainerColor = LegadoTheme.colorScheme.secondaryContainer, modifier = modifier .fillMaxWidth() @@ -483,6 +482,7 @@ private fun ServerConfigSheetContent( AppTextField( value = name, onValueChange = { name = it }, + backgroundColor = LegadoTheme.colorScheme.onSheetContent, label = "名称", modifier = Modifier.fillMaxWidth(), singleLine = true @@ -491,6 +491,7 @@ private fun ServerConfigSheetContent( AppTextField( value = url, onValueChange = { url = it }, + backgroundColor = LegadoTheme.colorScheme.onSheetContent, label = "URL", modifier = Modifier.fillMaxWidth(), singleLine = true @@ -499,6 +500,7 @@ private fun ServerConfigSheetContent( AppTextField( value = username, onValueChange = { username = it }, + backgroundColor = LegadoTheme.colorScheme.onSheetContent, label = "用户名", modifier = Modifier.fillMaxWidth(), singleLine = true @@ -507,6 +509,7 @@ private fun ServerConfigSheetContent( AppTextField( value = password, onValueChange = { password = it }, + backgroundColor = LegadoTheme.colorScheme.onSheetContent, label = "密码", modifier = Modifier.fillMaxWidth(), singleLine = true, @@ -523,33 +526,23 @@ private fun ServerConfigSheetContent( Spacer(modifier = Modifier.height(24.dp)) - Row( + ConfirmDismissButtonsRow( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - OutlinedButton( - onClick = onCancel, - modifier = Modifier.weight(1f) - ) { - AppText(stringResource(android.R.string.cancel)) - } - Button( - onClick = { - val newServer = server?.copy(name = name) ?: Server(name = name) - val newConfig = JSONObject().apply { - put("url", url) - put("username", username) - put("password", password) - } - newServer.config = newConfig.toString() - onSave(newServer) - }, - modifier = Modifier.weight(1f), - enabled = name.isNotBlank() && url.isNotBlank() - ) { - AppText("保存") - } - } + onDismiss = onCancel, + onConfirm = { + val newServer = server?.copy(name = name) ?: Server(name = name) + val newConfig = JSONObject().apply { + put("url", url) + put("username", username) + put("password", password) + } + newServer.config = newConfig.toString() + onSave(newServer) + }, + dismissText = stringResource(android.R.string.cancel), + confirmText = "保存", + confirmEnabled = name.isNotBlank() && url.isNotBlank() + ) } } diff --git a/app/src/main/java/io/legado/app/ui/book/info/BookInfoScreen.kt b/app/src/main/java/io/legado/app/ui/book/info/BookInfoScreen.kt index 215bb4f3a..cf6a72489 100644 --- a/app/src/main/java/io/legado/app/ui/book/info/BookInfoScreen.kt +++ b/app/src/main/java/io/legado/app/ui/book/info/BookInfoScreen.kt @@ -3,7 +3,6 @@ package io.legado.app.ui.book.info import android.net.Uri import androidx.activity.compose.BackHandler import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.compose.animation.animateColorAsState import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -43,6 +42,7 @@ import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.Share import androidx.compose.material.icons.filled.Star import androidx.compose.material.icons.outlined.Book +import androidx.compose.material.icons.outlined.Bookmark import androidx.compose.material.icons.outlined.CollectionsBookmark import androidx.compose.material.icons.outlined.FolderZip import androidx.compose.material.icons.outlined.Image @@ -53,13 +53,14 @@ import androidx.compose.material3.CardDefaults import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ElevatedCard import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.MediumFlexibleTopAppBar import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.material3.TopAppBar import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState @@ -78,6 +79,7 @@ import androidx.compose.ui.draw.blur import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.lerp +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalDensity @@ -89,6 +91,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil.ImageLoader import coil.compose.AsyncImage import io.legado.app.R +import io.legado.app.constant.BookType import io.legado.app.data.appDb import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookChapter @@ -105,11 +108,14 @@ import io.legado.app.ui.book.source.edit.BookSourceEditActivity import io.legado.app.ui.book.source.manage.BookSourceActivity import io.legado.app.ui.config.coverConfig.CoverConfig import io.legado.app.ui.theme.LegadoTheme +import io.legado.app.ui.theme.LocalHazeState import io.legado.app.ui.theme.ProvideThemeOverride import io.legado.app.ui.theme.ThemeResolver import io.legado.app.ui.theme.ThemeOverrideState import io.legado.app.ui.theme.rememberImageSeedColor import io.legado.app.ui.theme.rememberThemeOverride +import io.legado.app.ui.theme.responsiveHazeEffect +import io.legado.app.ui.theme.responsiveHazeEffectFixedStyle import io.legado.app.ui.widget.components.AppScaffold import io.legado.app.ui.widget.components.AppTextField import io.legado.app.ui.widget.components.alert.AppAlertDialog @@ -123,10 +129,12 @@ import io.legado.app.ui.widget.components.icon.AppIcon import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet +import io.legado.app.ui.widget.components.cover.buildCoverImageRequest import io.legado.app.ui.widget.components.text.AnimatedTextLine 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.GlassTopAppBarScrollBehavior import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults +import io.legado.app.ui.widget.components.topbar.M3GlassScrollBehavior import io.legado.app.ui.widget.components.topbar.MiuixGlassScrollBehavior import io.legado.app.utils.StartActivityContract import io.legado.app.utils.startActivity @@ -134,6 +142,7 @@ import io.legado.app.utils.toastOnUi import kotlinx.coroutines.delay import org.koin.compose.koinInject import org.koin.androidx.compose.koinViewModel +import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior import top.yukonga.miuix.kmp.basic.TopAppBar as MiuixTopAppBar @OptIn(ExperimentalMaterial3Api::class) @@ -162,32 +171,12 @@ private fun BookInfoScreenContent( onBack: () -> Unit, ) { val isMiuix = ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine) - val miuixScrollBehavior = if (isMiuix) { - GlassTopAppBarDefaults.defaultScrollBehavior() as? MiuixGlassScrollBehavior + val scrollBehavior = if (isMiuix) { + MiuixGlassScrollBehavior(MiuixScrollBehavior()) } else { - null + M3GlassScrollBehavior(TopAppBarDefaults.exitUntilCollapsedScrollBehavior()) } - val miuixCollapsedFraction = miuixScrollBehavior?.collapsedFraction ?: 0f val listState = rememberLazyListState() - val colorThresholdPx = with(LocalDensity.current) { 6.dp.roundToPx() } - val showAppBarColor by remember(listState, isMiuix, colorThresholdPx, miuixCollapsedFraction) { - derivedStateOf { - if (isMiuix) { - miuixCollapsedFraction > 0.01f - } else { - listState.firstVisibleItemIndex > 0 || - listState.firstVisibleItemScrollOffset > colorThresholdPx - } - } - } - val appBarColor by animateColorAsState( - targetValue = if (showAppBarColor) { - LegadoTheme.colorScheme.surfaceContainer - } else { - LegadoTheme.colorScheme.surfaceContainer.copy(alpha = 0f) - }, - label = "book-info-top-bar-color", - ) val pullState = rememberPullToRefreshState() var showMenu by rememberSaveable { mutableStateOf(false) } @@ -196,63 +185,16 @@ private fun BookInfoScreenContent( AppScaffold( modifier = Modifier .fillMaxSize() - .then( - if (miuixScrollBehavior != null) { - Modifier.nestedScroll(miuixScrollBehavior.nestedScrollConnection) - } else { - Modifier - } - ), + .nestedScroll(scrollBehavior.nestedScrollConnection), topBar = { - if (isMiuix) { - MiuixTopAppBar( - title = "", - color = appBarColor, - navigationIcon = { - TopBarNavigationButton( - onClick = { onIntent(BookInfoIntent.BackPressed) } - ) - }, - actions = { - BookInfoTopBarActions( - state = state, - showMenu = showMenu, - onShowMenuChange = { showMenu = it }, - onMenuAction = { onIntent(BookInfoIntent.MenuAction(it)) }, - ) - }, - scrollBehavior = miuixScrollBehavior?.miuixBehavior - ) - } else { - TopAppBar( - title = { - Text( - text = "", - maxLines = 1, - ) - }, - navigationIcon = { - TopBarNavigationButton( - onClick = { onIntent(BookInfoIntent.BackPressed) } - ) - }, - actions = { - BookInfoTopBarActions( - state = state, - showMenu = showMenu, - onShowMenuChange = { showMenu = it }, - onMenuAction = { onIntent(BookInfoIntent.MenuAction(it)) }, - ) - }, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = appBarColor, - scrolledContainerColor = appBarColor, - navigationIconContentColor = LegadoTheme.colorScheme.onSurface, - titleContentColor = LegadoTheme.colorScheme.onSurface, - actionIconContentColor = LegadoTheme.colorScheme.onSurface, - ), - ) - } + BookInfoTransparentTopAppBar( + state = state, + showMenu = showMenu, + onShowMenuChange = { showMenu = it }, + onMenuAction = { onIntent(BookInfoIntent.MenuAction(it)) }, + onBackPressed = { onIntent(BookInfoIntent.BackPressed) }, + scrollBehavior = scrollBehavior, + ) }, floatingActionButton = { ExtendedFloatingActionButton( @@ -322,6 +264,7 @@ private fun BookInfoScreenContent( .padding(bottom = 24.dp) ) { BookInfoActions( + book = book, inBookshelf = state.inBookshelf, onShelfClick = { onIntent(BookInfoIntent.ShelfClick) }, onTocClick = { onIntent(BookInfoIntent.TocClick) }, @@ -416,6 +359,70 @@ private fun BookInfoColorTheme( ProvideThemeOverride(theme = theme, content = content) } +@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) +@Composable +private fun BookInfoTransparentTopAppBar( + state: BookInfoUiState, + showMenu: Boolean, + onShowMenuChange: (Boolean) -> Unit, + onMenuAction: (BookInfoMenuAction) -> Unit, + onBackPressed: () -> Unit, + scrollBehavior: GlassTopAppBarScrollBehavior, +) { + val hazeState = LocalHazeState.current + val isMiuix = ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine) + val collapsedColor = if (isMiuix) { + GlassTopAppBarDefaults.getMiuixAppBarColor() + } else { + GlassTopAppBarDefaults.scrolledContainerColor() + } + val isAtTop = scrollBehavior.collapsedFraction <= 0.001f + val resolvedColor = if (isAtTop) Color.Transparent else collapsedColor + val topBarColors = TopAppBarDefaults.topAppBarColors( + containerColor = resolvedColor, + scrolledContainerColor = resolvedColor, + ) + + if (isMiuix) { + MiuixTopAppBar( + modifier = hazeState?.let { Modifier.responsiveHazeEffectFixedStyle(it) } ?: Modifier, + title = "", + subtitle = "", + navigationIcon = { + TopBarNavigationButton(onClick = onBackPressed) + }, + actions = { + BookInfoTopBarActions( + state = state, + showMenu = showMenu, + onShowMenuChange = onShowMenuChange, + onMenuAction = onMenuAction, + ) + }, + color = resolvedColor, + scrollBehavior = (scrollBehavior as? MiuixGlassScrollBehavior)?.miuixBehavior, + ) + } else { + MediumFlexibleTopAppBar( + modifier = hazeState?.let { Modifier.responsiveHazeEffectFixedStyle(it) } ?: Modifier, + title = { Text(text = "", maxLines = 1) }, + navigationIcon = { + TopBarNavigationButton(onClick = onBackPressed) + }, + actions = { + BookInfoTopBarActions( + state = state, + showMenu = showMenu, + onShowMenuChange = onShowMenuChange, + onMenuAction = onMenuAction, + ) + }, + scrollBehavior = (scrollBehavior as? M3GlassScrollBehavior)?.m3Behavior, + colors = topBarColors, + ) + } +} + @Composable private fun rememberBookInfoColorTheme(book: Book?): ThemeOverrideState? { val imageLoader = koinInject() @@ -470,6 +477,19 @@ private fun BookInfoTopBarActions( @Composable private fun BookInfoBackdrop(book: Book) { val cover = book.getDisplayCover() + val sourceOrigin = book.origin + val loadOnlyWifi = CoverConfig.loadCoverOnlyWifi + val context = LocalContext.current + val imageLoader = koinInject() + val backdropRequest = remember(cover, sourceOrigin, loadOnlyWifi, context) { + buildCoverImageRequest( + context = context, + data = cover, + sourceOrigin = sourceOrigin, + loadOnlyWifi = loadOnlyWifi, + crossfade = false, + ) + } val seedOverlay = lerp( LegadoTheme.colorScheme.secondaryContainer, LegadoTheme.seedColor, @@ -478,7 +498,8 @@ private fun BookInfoBackdrop(book: Book) { Box(modifier = Modifier.fillMaxSize()) { if (!cover.isNullOrBlank()) { AsyncImage( - model = cover, + model = backdropRequest, + imageLoader = imageLoader, contentDescription = null, modifier = Modifier .fillMaxSize() @@ -567,22 +588,24 @@ private fun BookInfoOverflowMenu( text = stringResource(R.string.to_top), onClick = { onMenuAction(BookInfoMenuAction.Top) } ) - RoundDropdownMenuItem( - text = stringResource(R.string.allow_update), - onClick = { onMenuAction(BookInfoMenuAction.ToggleCanUpdate) }, - trailingIcon = { if (book?.canUpdate == true) Icon(Icons.Default.Star, null) } - ) - if (book?.isLocal == true && book.type and io.legado.app.constant.BookType.text > 0) { + if (book?.isLocal == false ){ + RoundDropdownMenuItem( + text = stringResource(R.string.allow_update), + onClick = { onMenuAction(BookInfoMenuAction.ToggleCanUpdate) }, + isSelected = book.canUpdate + ) + } + if (book?.isLocal == true && book.type and BookType.text > 0) { RoundDropdownMenuItem( text = stringResource(R.string.split_long_chapter), onClick = { onMenuAction(BookInfoMenuAction.ToggleSplitLongChapter) }, - trailingIcon = { if (book.getSplitLongChapter()) Icon(Icons.Default.Star, null) } + isSelected = book.getSplitLongChapter() ) } RoundDropdownMenuItem( text = stringResource(R.string.delete_alert), onClick = { onMenuAction(BookInfoMenuAction.ToggleDeleteAlert) }, - trailingIcon = { if (io.legado.app.help.config.LocalConfig.bookInfoDeleteAlert) Icon(Icons.Default.Star, null) } + isSelected = io.legado.app.help.config.LocalConfig.bookInfoDeleteAlert ) RoundDropdownMenuItem( text = stringResource(R.string.clear_cache), @@ -606,11 +629,6 @@ private fun BookInfoHeader( onBookNameClick: (Boolean) -> Unit, onOriginClick: () -> Unit, ) { - val labelBackground = lerp( - LegadoTheme.colorScheme.surface, - LegadoTheme.seedColor, - 0.1f - ).copy(alpha = 0.82f) Column( modifier = Modifier .fillMaxWidth() @@ -688,6 +706,16 @@ private fun BookInfoHeader( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp), ) { + groupNames?.takeIf { it.isNotBlank() }?.let { + item(key = "group-$it") { + TextCard( + text = stringResource(R.string.group_s, it), + textStyle = LegadoTheme.typography.labelLargeEmphasized, + backgroundColor = LegadoTheme.colorScheme.surfaceContainer, + contentColor = LegadoTheme.colorScheme.onSurface, + ) + } + } itemsIndexed( items = kindLabels, key = { index, label -> "kind-$index-$label" } @@ -695,20 +723,10 @@ private fun BookInfoHeader( TextCard( text = label, textStyle = LegadoTheme.typography.labelLargeEmphasized, - backgroundColor = labelBackground, + backgroundColor = LegadoTheme.colorScheme.surfaceContainer, contentColor = LegadoTheme.colorScheme.onSurface, ) } - groupNames?.takeIf { it.isNotBlank() }?.let { - item(key = "group-$it") { - TextCard( - text = stringResource(R.string.group_s, it), - textStyle = LegadoTheme.typography.labelLargeEmphasized, - backgroundColor = labelBackground, - contentColor = LegadoTheme.colorScheme.onSurface, - ) - } - } } } } @@ -717,6 +735,7 @@ private fun BookInfoHeader( @Composable private fun BookInfoActions( + book: Book, inBookshelf: Boolean, onShelfClick: () -> Unit, onTocClick: () -> Unit, @@ -747,7 +766,7 @@ private fun BookInfoActions( modifier = Modifier .fillMaxWidth() .background(LegadoTheme.colorScheme.surface) - .padding(horizontal = 16.dp, vertical = 4.dp), + .padding(horizontal = 16.dp, vertical = 8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp), ) { BookInfoActionCard( @@ -772,7 +791,7 @@ private fun BookInfoActions( ) BookInfoActionCard( modifier = Modifier.weight(1f), - icon = Icons.Default.Bookmark, + icon = if (book.group > 0) Icons.Default.Bookmark else Icons.Outlined.Bookmark, label = stringResource(R.string.change_group), onClick = onGroupClick ) @@ -788,14 +807,14 @@ private fun BookInfoActions( @Composable private fun BookInfoActionCard( modifier: Modifier = Modifier, - icon: androidx.compose.ui.graphics.vector.ImageVector, + icon: ImageVector, label: String, onClick: () -> Unit ) { GlassCard( modifier = modifier, onClick = onClick, - containerColor = LegadoTheme.colorScheme.surfaceContainer.copy(alpha = 0.5f), + containerColor = LegadoTheme.colorScheme.surfaceContainerLow.copy(alpha = 0.2f), contentColor = LegadoTheme.colorScheme.onSurface, ) { Column( diff --git a/app/src/main/java/io/legado/app/ui/book/info/BookInfoSheets.kt b/app/src/main/java/io/legado/app/ui/book/info/BookInfoSheets.kt index 274d03e07..34b4333f7 100644 --- a/app/src/main/java/io/legado/app/ui/book/info/BookInfoSheets.kt +++ b/app/src/main/java/io/legado/app/ui/book/info/BookInfoSheets.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.grid.GridCells @@ -65,6 +66,7 @@ import io.legado.app.ui.book.group.GroupEditSheet import io.legado.app.ui.book.source.edit.BookSourceEditActivity import io.legado.app.ui.book.source.manage.BookSourceActivity import io.legado.app.ui.theme.LegadoTheme +import io.legado.app.ui.widget.components.AppLinearProgressIndicator import io.legado.app.ui.widget.components.AppTextField import io.legado.app.ui.widget.components.alert.AppAlertDialog import io.legado.app.ui.widget.components.button.ConfirmDismissButtonsRow @@ -134,53 +136,58 @@ fun GroupSelectSheet( title = stringResource(R.string.group_select), endAction = { IconButton(onClick = { editingGroup = BookGroup() }) { Icon(Icons.Default.Add, null) } } ) { - LazyColumn(modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(8.dp)) { - items(groups, key = { it.groupId }) { group -> - val isSelected = selectedGroupId and group.groupId > 0 - SelectionItemCard( - title = group.groupName, - isSelected = isSelected, - onToggleSelection = { - selectedGroupId = if (isSelected) { - selectedGroupId - group.groupId - } else { - selectedGroupId + group.groupId - } - }, - leadingContent = { - AppCheckbox( - checked = isSelected, - onCheckedChange = { - selectedGroupId = if (it) { - selectedGroupId + group.groupId - } else { - selectedGroupId - group.groupId - } + Column(modifier = Modifier.fillMaxWidth()) { + LazyColumn( + modifier = Modifier.heightIn(max = 600.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + items(groups, key = { it.groupId }) { group -> + val isSelected = selectedGroupId and group.groupId > 0 + SelectionItemCard( + title = group.groupName, + isSelected = isSelected, + onToggleSelection = { + selectedGroupId = if (isSelected) { + selectedGroupId - group.groupId + } else { + selectedGroupId + group.groupId } - ) - }, - trailingAction = { - SmallIconButton( - onClick = { editingGroup = group }, - imageVector = Icons.Default.Edit - ) - }, - containerColor = LegadoTheme.colorScheme.surfaceContainerLow - ) + }, + leadingContent = { + AppCheckbox( + checked = isSelected, + onCheckedChange = { + selectedGroupId = if (it) { + selectedGroupId + group.groupId + } else { + selectedGroupId - group.groupId + } + } + ) + }, + trailingAction = { + SmallIconButton( + onClick = { editingGroup = group }, + imageVector = Icons.Default.Edit + ) + }, + containerColor = LegadoTheme.colorScheme.surfaceContainerLow + ) + } } + Spacer(modifier = Modifier.height(12.dp)) + ConfirmDismissButtonsRow( + onDismiss = onDismissRequest, + onConfirm = { onConfirm(selectedGroupId) }, + dismissText = stringResource(R.string.cancel), + confirmText = stringResource(R.string.ok), + ) + Spacer(modifier = Modifier.height(12.dp)) } - Spacer(modifier = Modifier.height(12.dp)) - ConfirmDismissButtonsRow( - onDismiss = onDismissRequest, - onConfirm = { onConfirm(selectedGroupId) }, - dismissText = stringResource(R.string.cancel), - confirmText = stringResource(R.string.ok), - ) - Spacer(modifier = Modifier.height(12.dp)) } - GroupEditSheet(show = editingGroup != null, group = editingGroup, onDismissRequest = { editingGroup = null }) } + @OptIn(ExperimentalFoundationApi::class) @Composable fun ChangeCoverSheet( @@ -376,7 +383,7 @@ fun ChangeSourceSheet( ) Spacer(modifier = Modifier.height(12.dp)) if (isSearching) { - LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) + AppLinearProgressIndicator(modifier = Modifier.fillMaxWidth()) Spacer(modifier = Modifier.height(8.dp)) AppText( text = "${progress.first} / ${viewModel.totalSourceCount} · ${items.size}", diff --git a/app/src/main/java/io/legado/app/ui/book/readRecord/ReadRecordScreen.kt b/app/src/main/java/io/legado/app/ui/book/readRecord/ReadRecordScreen.kt index 318e45ed5..65bc3885c 100644 --- a/app/src/main/java/io/legado/app/ui/book/readRecord/ReadRecordScreen.kt +++ b/app/src/main/java/io/legado/app/ui/book/readRecord/ReadRecordScreen.kt @@ -69,12 +69,11 @@ import cn.hutool.core.date.DateUtil import io.legado.app.data.entities.readRecord.ReadRecord import io.legado.app.data.entities.readRecord.ReadRecordDetail import io.legado.app.ui.theme.LegadoTheme -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.CollapsibleHeader import io.legado.app.ui.widget.components.AppScaffold -import io.legado.app.ui.widget.components.EmptyMessageView +import io.legado.app.ui.widget.components.EmptyMessage import io.legado.app.ui.widget.components.SearchBarSection import io.legado.app.ui.widget.components.alert.AppAlertDialog import io.legado.app.ui.widget.components.button.AppIconButton @@ -248,7 +247,7 @@ fun ReadRecordScreen( ) { targetState -> when (targetState) { "LOADING" -> { - EmptyMessageView( + EmptyMessage( modifier = Modifier .fillMaxSize() .padding( @@ -261,7 +260,7 @@ fun ReadRecordScreen( } "EMPTY" -> { - EmptyMessageView( + EmptyMessage( modifier = Modifier .fillMaxSize() .padding( diff --git a/app/src/main/java/io/legado/app/ui/book/search/SearchActivity.kt b/app/src/main/java/io/legado/app/ui/book/search/SearchActivity.kt index 4eafc5949..28d096d07 100644 --- a/app/src/main/java/io/legado/app/ui/book/search/SearchActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/search/SearchActivity.kt @@ -1,674 +1,73 @@ package io.legado.app.ui.book.search -import android.annotation.SuppressLint import android.content.Context import android.content.Intent import android.os.Bundle -import android.view.KeyEvent -import android.view.Menu -import android.view.MenuItem -import android.view.View.VISIBLE -import android.view.inputmethod.EditorInfo -import androidx.activity.viewModels -import androidx.appcompat.widget.PopupMenu -import androidx.core.os.bundleOf -import androidx.core.view.isVisible -import androidx.core.widget.doAfterTextChanged -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle -import androidx.recyclerview.widget.DefaultItemAnimator -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import androidx.transition.TransitionManager -import com.google.android.flexbox.FlexboxLayoutManager -import com.google.android.material.search.SearchBar -import com.google.android.material.search.SearchView -import io.legado.app.R -import io.legado.app.base.VMBaseActivity -import io.legado.app.constant.AppLog -import io.legado.app.constant.PreferKey -import io.legado.app.data.appDb -import io.legado.app.data.entities.Book -import io.legado.app.data.entities.SearchKeyword -import io.legado.app.databinding.ActivityBookSearchBinding -import io.legado.app.lib.dialogs.alert -import io.legado.app.model.BookShelfState -import io.legado.app.ui.about.AppLogDialog +import androidx.compose.runtime.Composable +import io.legado.app.base.BaseComposeActivity import io.legado.app.ui.book.info.BookInfoActivity import io.legado.app.ui.book.source.manage.BookSourceActivity -import io.legado.app.utils.applyNavigationBarMargin -import io.legado.app.utils.getPrefBoolean -import io.legado.app.utils.gone -import io.legado.app.utils.invisible -import io.legado.app.utils.putPrefBoolean -import io.legado.app.utils.showDialogFragment import io.legado.app.utils.startActivity -import io.legado.app.utils.transaction -import io.legado.app.utils.viewbindingdelegate.viewBinding -import io.legado.app.utils.visible -import kotlinx.coroutines.Dispatchers.IO -import kotlinx.coroutines.Job -import kotlinx.coroutines.awaitCancellation -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.conflate -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import splitties.init.appCtx -import kotlin.math.abs +import org.koin.androidx.viewmodel.ext.android.viewModel -class SearchActivity : VMBaseActivity(), - BookAdapter.CallBack, - HistoryKeyAdapter.CallBack, - SearchScopeDialog.Callback, - SearchAdapter.CallBack { +class SearchActivity : BaseComposeActivity() { - override val binding by viewBinding(ActivityBookSearchBinding::inflate) - override val viewModel by viewModels() - - private val adapter by lazy { SearchAdapter(this, this) } - private val bookAdapter by lazy { - BookAdapter(this, this).apply { - setHasStableIds(true) - } - } - private val historyKeyAdapter by lazy { - HistoryKeyAdapter(this, this).apply { - setHasStableIds(true) - } - } - - private val searchBar: SearchBar by lazy { binding.searchBar } - private val searchView: SearchView by lazy { binding.searchView } - - private var menu: Menu? = null - private var groups: List? = null - private var historyFlowJob: Job? = null - private var booksFlowJob: Job? = null - private var precisionSearchMenuItem: MenuItem? = null - private var isManualStopSearch = false + private val viewModel by viewModel() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setSupportActionBar(binding.searchBar) - initRecyclerView() - initMaterialSearch() - initOtherView() - initData() - updateSelectedGroup() - receiptIntent(intent) + dispatchInit(intent) } override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) - receiptIntent(intent) + setIntent(intent) + dispatchInit(intent) } - override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { - menuInflater.inflate(R.menu.book_search, menu) - this.menu = menu - precisionSearchMenuItem = menu.findItem(R.id.menu_precision_search) - precisionSearchMenuItem?.isChecked = getPrefBoolean(PreferKey.precisionSearch) - return super.onCompatCreateOptionsMenu(menu) + override fun onResume() { + super.onResume() + viewModel.onIntent(SearchIntent.ResumeEngine) } - override fun onMenuOpened(featureId: Int, menu: Menu): Boolean { - buildSearchScopeMenu(menu) - updateSearchScopeChip() - return super.onMenuOpened(featureId, menu) + override fun onPause() { + viewModel.onIntent(SearchIntent.PauseEngine) + super.onPause() } - override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - R.id.menu_precision_search -> { - val newValue = !getPrefBoolean(PreferKey.precisionSearch) - putPrefBoolean(PreferKey.precisionSearch, newValue) - precisionSearchMenuItem?.isChecked = newValue - binding.chipPrecisionSearch.isChecked = newValue - binding.searchView.text.toString().trim().let { - binding.searchView.setText(it) + @Composable + override fun Content() { + SearchScreen( + viewModel = viewModel, + onBack = { finish() }, + onOpenBookInfo = { name, author, bookUrl -> + startActivity { + putExtra("name", name) + putExtra("author", author) + putExtra("bookUrl", bookUrl) } + }, + onOpenSourceManage = { + startActivity() } - - R.id.menu_search_scope -> alertSearchScope() - R.id.menu_source_manage -> startActivity() - R.id.menu_log -> showDialogFragment(AppLogDialog()) - R.id.menu_1 -> { - viewModel.searchScope.update("") - updateSelectedGroup() - } - else -> { - if (item.groupId == R.id.menu_group_1) { - viewModel.searchScope.remove(item.title.toString()) - } else if (item.groupId == R.id.menu_group_2) { - viewModel.searchScope.update(item.title.toString()) - } - updateSelectedGroup() - } - } - return super.onCompatOptionsItemSelected(item) + ) } - private fun initMaterialSearch() { - binding.chipPrecisionSearch.isChecked = getPrefBoolean(PreferKey.precisionSearch) - binding.chipPrecisionSearch.setOnCheckedChangeListener { chip, isChecked -> - putPrefBoolean(PreferKey.precisionSearch, isChecked) - precisionSearchMenuItem?.isChecked = isChecked - binding.searchView.text.toString().trim().let { - binding.searchView.setText(it) - } - } - binding.chipSearchScope.setOnClickListener { - alertSearchScope() - } - binding.chipGroup.setOnClickListener { view -> - binding.chipGroup.isChecked = true - val popup = PopupMenu(this, view) - val menu = popup.menu - buildSearchScopeMenu(menu) - updateSearchScopeChip() - var fromGroup1 = false - popup.setOnMenuItemClickListener { item -> - when (item.groupId) { - R.id.menu_group_1 -> { - viewModel.searchScope.remove(item.title.toString()) - fromGroup1 = true - } - R.id.menu_group_2 -> { - if (item.itemId == R.id.menu_1) { - viewModel.searchScope.update("") - fromGroup1 = true - } else { - viewModel.searchScope.update(item.title.toString()) - fromGroup1 = false - } - } - else -> { - viewModel.searchScope.update(item.title.toString()) - fromGroup1 = false - } - } - - updateSelectedGroup() - true - } - popup.setOnDismissListener { - binding.chipGroup.isChecked = !fromGroup1 - updateSearchScopeChip() - } - popup.show() - } - - searchBar.setOnMenuItemClickListener { item -> - onCompatOptionsItemSelected(item) - } - - searchBar.setNavigationOnClickListener { - finish() - } - - searchView.setupWithSearchBar(searchBar) - - binding.searchView.show() - binding.searchView.editText.requestFocus() - - searchView.editText.hint = getString(R.string.search_book_key) - searchView.editText.setOnEditorActionListener { _, actionId, _ -> - if (actionId == EditorInfo.IME_ACTION_SEARCH) { - doSearch() - true - } else { - false - } - } - - searchView.editText.setOnKeyListener { _, keyCode, event -> - if (keyCode == KeyEvent.KEYCODE_ENTER && event.action == KeyEvent.ACTION_DOWN) { - doSearch() - true - } else { - false - } - } - - searchView.editText.doAfterTextChanged { - viewModel.stop() - binding.fbStartStop.invisible() - upHistory(it?.toString()?.trim().orEmpty()) - } - - searchView.addTransitionListener { _, _, newState -> - if (newState == SearchView.TransitionState.HIDDEN) { - searchBar.setText(searchView.text) - } - } - - searchView.addTransitionListener { _, oldState, newState -> - if (oldState == SearchView.TransitionState.HIDDEN && newState == SearchView.TransitionState.SHOWN) { - searchView.editText.requestFocus() - } - } - - visibleInputHelp() - } - - private fun doSearch() { - val key = searchView.text.toString().trim() - if (key.isNotEmpty()) { - isManualStopSearch = false - viewModel.saveSearchKey(key) - viewModel.searchKey = "" - viewModel.search(key) - visibleInputHelp() - searchView.hide() - } - } - - private fun updateSelectedGroup() { - val displayNames = viewModel.searchScope.displayNames - val groupName = displayNames.firstOrNull().orEmpty() - binding.chipGroup.text = groupName.ifEmpty { getString(R.string.search_select_group) } - binding.chipGroup.isChecked = groupName.isNotEmpty() - } - - private fun buildSearchScopeMenu(menu: Menu) { - menu.transaction { - menu.removeGroup(R.id.menu_group_1) - menu.removeGroup(R.id.menu_group_2) - var hasChecked = false - val searchScopeNames = viewModel.searchScope.displayNames - - if (viewModel.searchScope.isSource()) { - menu.add(R.id.menu_group_1, Menu.NONE, Menu.NONE, searchScopeNames.first()).apply { - isChecked = true - hasChecked = true - } - } - - val allSourceMenu = - menu.add(R.id.menu_group_2, R.id.menu_1, Menu.NONE, getString(R.string.all_source)) - .apply { - if (searchScopeNames.isEmpty()) { - isChecked = true - hasChecked = true - } - } - - groups?.forEach { groupName -> - if (searchScopeNames.contains(groupName)) { - menu.add(R.id.menu_group_1, Menu.NONE, Menu.NONE, groupName).apply { - isChecked = true - hasChecked = true - } - } else { - menu.add(R.id.menu_group_2, Menu.NONE, Menu.NONE, groupName) - } - } - - if (!hasChecked) { - viewModel.searchScope.update("") - allSourceMenu.isChecked = true - } - - menu.setGroupCheckable(R.id.menu_group_1, true, false) - menu.setGroupCheckable(R.id.menu_group_2, true, true) - } - } - - - private fun initRecyclerView() { - binding.rvBookshelfSearch.layoutManager = FlexboxLayoutManager(this) - binding.rvBookshelfSearch.adapter = bookAdapter - binding.rvBookshelfSearch.itemAnimator = DefaultItemAnimator() - - binding.rvHistoryKey.layoutManager = FlexboxLayoutManager(this) - binding.rvHistoryKey.adapter = historyKeyAdapter - binding.rvHistoryKey.itemAnimator = DefaultItemAnimator() - - binding.recyclerView.layoutManager = LinearLayoutManager(this) - binding.recyclerView.adapter = adapter - binding.recyclerView.itemAnimator = DefaultItemAnimator() - - adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { - override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { - super.onItemRangeInserted(positionStart, itemCount) - if (positionStart == 0) { - binding.recyclerView.scrollToPosition(0) - } - } - - override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) { - super.onItemRangeMoved(fromPosition, toPosition, itemCount) - if (toPosition == 0) { - binding.recyclerView.scrollToPosition(0) - } - } - }) - binding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { - override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { - super.onScrolled(recyclerView, dx, dy) - if (!recyclerView.canScrollVertically(1)) { - val layoutManager = recyclerView.layoutManager as LinearLayoutManager - val lastPosition = layoutManager.findLastCompletelyVisibleItemPosition() - if (lastPosition == RecyclerView.NO_POSITION) { - return - } - val lastView = layoutManager.findViewByPosition(lastPosition) - if (lastView == null) { - scrollToBottom() - return - } - val bottom = - abs(lastView.bottom - recyclerView.height) - recyclerView.paddingBottom - if (bottom <= 1) { - scrollToBottom() - } - } - } - }) - } - - private fun initOtherView() { - binding.llStop.applyNavigationBarMargin() - binding.fbStartStop.setOnClickListener { - if (viewModel.isSearchLiveData.value == true) { - isManualStopSearch = true - viewModel.stop() - } else { - viewModel.search("") - } - } - binding.tvClearHistory.setOnClickListener { alertClearHistory() } - } - - private fun initData() { - viewModel.searchScope.stateLiveData.observe(this) { - if (!binding.llInputHelp.isVisible) { - searchView.text.toString().trim().let { - searchView.setText(it) - } - } - } - viewModel.isSearchLiveData.observe(this) { - if (it) { - startSearch() - } else { - searchFinally() - } - } - viewModel.searchBookLiveData.observe(this) { - adapter.setItems(it) - if (it.isNullOrEmpty()) { - binding.tvEmptyMsg.visible() - } else { - binding.tvEmptyMsg.gone() - } - } - lifecycleScope.launch { - appDb.bookSourceDao.flowEnabledGroups().collect { - groups = it - } - } - lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.RESUMED) { - viewModel.resume() - try { - awaitCancellation() - } finally { - viewModel.pause() - } - } - } - } - - /** - * 处理传入数据 - */ - private fun receiptIntent(intent: Intent? = null) { - val searchScope = intent?.getStringExtra("searchScope") - searchScope?.let { - viewModel.searchScope.update(searchScope, false) - updateSearchScopeChip() - } - val key = intent?.getStringExtra("key") - if (key.isNullOrBlank()) { - searchView.editText.requestFocus() - } else { - searchView.setText(key) - searchView.postDelayed({doSearch()},500) - } - } - - /** - * 滚动到底部事件 - */ - private fun scrollToBottom() { - if (isManualStopSearch) { - return - } - if (viewModel.isSearchLiveData.value == false - && viewModel.searchKey.isNotEmpty() - && viewModel.hasMore - ) { - viewModel.search("") - } - } - - /** - * 打开关闭输入帮助 - */ - private fun visibleInputHelp() { - upHistory(searchView.text.toString()) - binding.llInputHelp.visibility = VISIBLE - } - - /** - * 更新搜索历史 - */ - private fun upHistory(key: String? = null) { - booksFlowJob?.cancel() - booksFlowJob = lifecycleScope.launch { - if (key.isNullOrBlank()) { - binding.tvBookShow.gone() - binding.rvBookshelfSearch.gone() - } else { - appDb.bookDao.flowSearch(key).conflate().collect { - if (it.isEmpty()) { - binding.tvBookShow.gone() - binding.rvBookshelfSearch.gone() - } else { - binding.tvBookShow.visible() - binding.rvBookshelfSearch.visible() - } - bookAdapter.setItems(it) - } - } - } - historyFlowJob?.cancel() - historyFlowJob = lifecycleScope.launch { - when { - key.isNullOrBlank() -> appDb.searchKeywordDao.flowByTime() - else -> appDb.searchKeywordDao.flowSearch(key) - }.catch { - AppLog.put("搜索界面获取搜索历史数据失败\n${it.localizedMessage}", it) - }.flowOn(IO).conflate().collect { - historyKeyAdapter.setItems(it) - if (it.isEmpty()) { - binding.tvClearHistory.invisible() - } else { - binding.tvClearHistory.visible() - } - } - } - } - - /** - * 开始搜索 - */ - private fun startSearch() { - binding.refreshProgressBar.visible() - binding.cdProgress.visible() - //binding.refreshProgressBar.isAutoLoading = true - binding.fbStartStop.setImageResource(R.drawable.ic_stop_black_24dp) - binding.fbStartStop.visible() - } - - /** - * 搜索结束 - */ - private fun searchFinally() { - TransitionManager.beginDelayedTransition(binding.root) - binding.refreshProgressBar.gone() - binding.cdProgress.gone() - if (!isManualStopSearch && viewModel.hasMore) { - binding.fbStartStop.setImageResource(R.drawable.ic_play) - } else { - binding.fbStartStop.invisible() - } - } - - @SuppressLint("SetTextI18n") - override fun observeLiveBus() { - viewModel.upAdapterLiveData.observe(this) { - adapter.notifyItemRangeChanged(0, adapter.itemCount, bundleOf(it to null)) - } - viewModel.searchProgressLiveData.observe(this) { (processed, total) -> - val progress = (processed * 100 / total).coerceAtMost(100) - binding.refreshProgressBar.setProgress(progress, true) - binding.tvProgress.text = "$processed / $total" - } - viewModel.searchFinishLiveData.observe(this) { isEmpty -> - if (!isEmpty || viewModel.searchScope.isAll()) return@observe - alert("搜索结果为空") { - val precisionSearch = appCtx.getPrefBoolean(PreferKey.precisionSearch) - val displayScope = viewModel.searchScope.display - if (precisionSearch) { - setMessage("${displayScope}分组搜索结果为空,是否关闭精准搜索?") - yesButton { - appCtx.putPrefBoolean(PreferKey.precisionSearch, false) - precisionSearchMenuItem?.isChecked = false - viewModel.searchKey = "" - viewModel.search(searchView.text.toString()) - doSearch() - } - } else { - setMessage("${displayScope}分组搜索结果为空,是否切换到全部分组?") - yesButton { - viewModel.searchScope.update("") - doSearch() - } - } - noButton() - } - } - } - - /** - * 显示书籍详情 - */ - override fun showBookInfo(name: String, author: String, bookUrl: String) { - startActivity { - putExtra("name", name) - putExtra("author", author) - putExtra("bookUrl", bookUrl) - } - } - - /** - * 是否已经加入书架 - */ - override fun getBookShelfState(name: String, author: String, url: String?): BookShelfState { - return viewModel.isInBookShelf(name, author, url) - } - - /** - * 显示书籍详情 - */ - override fun showBookInfo(book: Book) { - showBookInfo(book.name, book.author, book.bookUrl) - } - - /** - * 点击历史关键字 - */ - override fun searchHistory(key: String) { - lifecycleScope.launch { - when { - searchView.text.toString() == key -> { - searchView.setText(key) - } - - withContext(IO) { appDb.bookDao.findByName(key).isEmpty() } -> { - searchView.setText(key) - } - - else -> { - searchView.setText(key) - } - } - } - } - - /** - * 删除搜索记录 - */ - override fun deleteHistory(searchKeyword: SearchKeyword) { - viewModel.deleteHistory(searchKeyword) - } - - - override fun onSearchScopeOk(searchScope: SearchScope) { - viewModel.searchScope.update(searchScope.toString()) - updateSearchScopeChip() - } - - private fun updateSearchScopeChip() { - binding.chipSearchScope.isCheckable = true - val scopeNames = viewModel.searchScope.displayNames - if (scopeNames.isEmpty() || viewModel.searchScope.isAll()) { - binding.chipSearchScope.apply { - isChecked = false - text = getString(R.string.all_source) - } - } else { - binding.chipSearchScope.apply { - isChecked = true - text = scopeNames.joinToString(", ") - } - } - binding.chipSearchScope.isCheckable = false - } - - private fun alertSearchScope() { - showDialogFragment() - } - - private fun alertClearHistory() { - alert(R.string.draw) { - setMessage(R.string.sure_clear_search_history) - yesButton { - viewModel.clearHistory() - } - noButton() - } - } - - override fun finish() { - if (searchView.hasFocus()) { - searchView.clearFocus() - } - super.finish() + private fun dispatchInit(intent: Intent?) { + viewModel.onIntent( + SearchIntent.Initialize( + key = intent?.getStringExtra("key"), + scopeRaw = intent?.getStringExtra("searchScope"), + ) + ) } companion object { - fun start(context: Context, key: String?, searchScope: String? = null) { context.startActivity { putExtra("key", key) putExtra("searchScope", searchScope) } } - } -} \ No newline at end of file +} diff --git a/app/src/main/java/io/legado/app/ui/book/search/SearchScope.kt b/app/src/main/java/io/legado/app/ui/book/search/SearchScope.kt index 7490b62f6..fc953da39 100644 --- a/app/src/main/java/io/legado/app/ui/book/search/SearchScope.kt +++ b/app/src/main/java/io/legado/app/ui/book/search/SearchScope.kt @@ -18,11 +18,11 @@ data class SearchScope(private var scope: String) { constructor(groups: List) : this(groups.joinToString(",")) constructor(source: BookSource) : this( - "${source.bookSourceName.replace(":", "")}::${source.bookSourceUrl}" + encodeSourceToken(source.bookSourceName, source.bookSourceUrl) ) constructor(source: BookSourcePart) : this( - "${source.bookSourceName.replace(":", "")}::${source.bookSourceUrl}" + encodeSourceToken(source.bookSourceName, source.bookSourceUrl) ) override fun toString(): String { @@ -44,19 +44,35 @@ data class SearchScope(private var scope: String) { } fun update(source: BookSource) { - scope = "${source.bookSourceName}::${source.bookSourceUrl}" + scope = encodeSourceToken(source.bookSourceName, source.bookSourceUrl) + stateLiveData.postValue(scope) + save() + } + + fun update(source: BookSourcePart) { + scope = encodeSourceToken(source.bookSourceName, source.bookSourceUrl) + stateLiveData.postValue(scope) + save() + } + + fun updateSources(sources: List) { + scope = encodeSourceScope(sources) stateLiveData.postValue(scope) save() } fun isSource(): Boolean { - return scope.contains("::") + val items = scopeItems() + if (items.isEmpty()) return false + return parseSourceItems(items).size == items.size } val display: String get() { - if (scope.contains("::")) { - return scope.substringBefore("::") + if (isSource()) { + val sourceNames = parseSourceItems().map { it.name } + if (sourceNames.isEmpty()) return appCtx.getString(R.string.all_source) + return sourceNames.joinToString(",") } if (scope.isEmpty()) { return appCtx.getString(R.string.all_source) @@ -70,22 +86,30 @@ data class SearchScope(private var scope: String) { val displayNames: List get() { val list = arrayListOf() - if (scope.contains("::")) { - list.add(scope.substringBefore("::")) + if (isSource()) { + parseSourceItems().forEach { + list.add(it.name) + } } else { - scope.splitNotBlank(",").forEach { + scopeItems().forEach { list.add(it) } } return list } + val sourceUrls: List + get() = parseSourceItems().map { it.url } + fun remove(scope: String) { if (isSource()) { - this.scope = "" + val sourceItems = parseSourceItems().filterNot { + it.name == scope || it.url == scope + } + this.scope = sourceItems.joinToString(",") { "${it.name}::${it.url}" } } else { val stringBuilder = StringBuilder() - this.scope.split(",").forEach { + scopeItems().forEach { if (it != scope) { if (stringBuilder.isNotEmpty()) { stringBuilder.append(",") @@ -106,14 +130,14 @@ data class SearchScope(private var scope: String) { if (scope.isEmpty()) { list.addAll(appDb.bookSourceDao.allEnabledPart) } else { - if (scope.contains("::")) { - scope.substringAfter("::").let { - appDb.bookSourceDao.getBookSourcePart(it)?.let { source -> + if (isSource()) { + parseSourceItems().forEach { sourceItem -> + appDb.bookSourceDao.getBookSourcePart(sourceItem.url)?.let { source -> list.add(source) } } } else { - val oldScope = scope.splitNotBlank(",") + val oldScope = scopeItems() val newScope = oldScope.filter { val bookSources = appDb.bookSourceDao.getEnabledPartByGroup(it) list.addAll(bookSources) @@ -150,4 +174,43 @@ data class SearchScope(private var scope: String) { } } + private fun scopeItems(): List = scope.splitNotBlank(",").toList() + + private fun parseSourceItems( + items: List = scopeItems() + ): List { + return items.mapNotNull { item -> + val splitIndex = item.indexOf("::") + if (splitIndex <= 0 || splitIndex >= item.lastIndex) { + null + } else { + ScopeSourceItem( + name = item.substring(0, splitIndex), + url = item.substring(splitIndex + 2) + ) + } + } + } + + private data class ScopeSourceItem( + val name: String, + val url: String, + ) + + companion object { + private fun sanitizeSourceName(name: String): String { + return name.replace(":", "").replace(",", "") + } + + private fun encodeSourceToken(name: String, url: String): String { + return "${sanitizeSourceName(name)}::${url}" + } + + private fun encodeSourceScope(sources: List): String { + return sources.joinToString(",") { source -> + encodeSourceToken(source.bookSourceName, source.bookSourceUrl) + } + } + } + } diff --git a/app/src/main/java/io/legado/app/ui/book/search/SearchViewModel.kt b/app/src/main/java/io/legado/app/ui/book/search/SearchViewModel.kt index b882558b9..85470ced0 100644 --- a/app/src/main/java/io/legado/app/ui/book/search/SearchViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/search/SearchViewModel.kt @@ -1,166 +1,423 @@ package io.legado.app.ui.book.search -import android.app.Application -import android.os.Handler -import android.os.Looper -import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import io.legado.app.base.BaseViewModel -import io.legado.app.constant.AppLog -import io.legado.app.data.appDb +import io.legado.app.constant.PreferKey +import io.legado.app.data.entities.BookSourcePart import io.legado.app.data.entities.SearchBook -import io.legado.app.data.entities.SearchKeyword -import io.legado.app.help.book.isNotShelf +import io.legado.app.data.repository.SearchRepository +import io.legado.app.data.repository.SearchSessionEvent import io.legado.app.help.config.AppConfig import io.legado.app.model.BookShelfState -import io.legado.app.model.webBook.SearchModel -import io.legado.app.utils.ConflateLiveData -import io.legado.app.utils.toastOnUi +import io.legado.app.ui.main.bookshelf.BookShelfItem +import io.legado.app.utils.getPrefBoolean +import io.legado.app.utils.putPrefBoolean import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.mapLatest -import java.util.concurrent.ConcurrentHashMap - -data class BookKey(val name: String, val author: String, val url: String?) +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import splitties.init.appCtx @OptIn(ExperimentalCoroutinesApi::class) -class SearchViewModel(application: Application) : BaseViewModel(application) { - val handler = Handler(Looper.getMainLooper()) - val bookshelf: MutableSet = ConcurrentHashMap.newKeySet() - val upAdapterLiveData = MutableLiveData() - var searchBookLiveData = ConflateLiveData>(1000) - val searchScope: SearchScope = SearchScope(AppConfig.searchScope) - var searchFinishLiveData = MutableLiveData() - var isSearchLiveData = MutableLiveData() +class SearchViewModel( + private val repository: SearchRepository, +) : ViewModel() { - val searchProgressLiveData = MutableLiveData>() + private val _uiState = MutableStateFlow( + SearchUiState( + isPrecisionSearch = appCtx.getPrefBoolean(PreferKey.precisionSearch), + scopeDisplay = SearchScope(AppConfig.searchScope).display, + scopeDisplayNames = SearchScope(AppConfig.searchScope).displayNames, + isAllScope = SearchScope(AppConfig.searchScope).isAll(), + isSourceScope = SearchScope(AppConfig.searchScope).isSource(), + ) + ) + val uiState = _uiState.asStateFlow() - var searchKey: String = "" - var hasMore = true - private var searchID = 0L - private val searchModel = SearchModel(viewModelScope, object : SearchModel.CallBack { + private val _effects = MutableSharedFlow(extraBufferCapacity = 16) + val effects = _effects.asSharedFlow() - override fun getSearchScope(): SearchScope { - return searchScope - } + private val queryFlow = MutableStateFlow("") + private val bookshelfKeys = MutableStateFlow>(emptySet()) + private val searchScope = SearchScope(AppConfig.searchScope) + private val searchSession = repository.createSearchSession { searchScope } - override fun onSearchStart() { - isSearchLiveData.postValue(true) - } - - override fun onSearchSuccess( - searchBooks: List, processedSources: Int, totalSources: Int - ) { - searchBookLiveData.postValue(searchBooks) - searchProgressLiveData.postValue(Pair(processedSources, totalSources)) - } - - override fun onSearchFinish(isEmpty: Boolean, hasMore: Boolean) { - this@SearchViewModel.hasMore = hasMore - isSearchLiveData.postValue(false) - searchFinishLiveData.postValue(isEmpty) - } - - override fun onSearchCancel(exception: Throwable?) { - isSearchLiveData.postValue(false) - exception?.let { - context.toastOnUi(it.localizedMessage) - } - } - - }) + private var currentSearchId = 0L init { - execute { - appDb.bookDao.flowAll().mapLatest { books -> - books.filterNot { it.isNotShelf } - }.catch { - AppLog.put("搜索界面获取书籍列表失败\n${it.localizedMessage}", it) - }.collect { books -> - bookshelf.clear() - bookshelf.addAll(books.map { BookKey(it.name, it.author, it.bookUrl) }) - upAdapterLiveData.postValue("isInBookshelf") + syncScopeState() + observeEnabledGroups() + observeEnabledSources() + observeBookshelf() + observeQueryHistory() + observeQueryBookshelfHints() + observeSearchSession() + } + + fun onIntent(intent: SearchIntent) { + when (intent) { + is SearchIntent.Initialize -> initialize(intent.key, intent.scopeRaw) + is SearchIntent.UpdateQuery -> updateQuery(intent.query, showSuggestions = true) + SearchIntent.SubmitSearch -> submitSearch() + SearchIntent.LoadMore -> loadMore() + SearchIntent.StopSearch -> stopSearch() + SearchIntent.PauseEngine -> searchSession.pause() + SearchIntent.ResumeEngine -> searchSession.resume() + is SearchIntent.UseHistoryKeyword -> { + updateQuery(intent.keyword, showSuggestions = false) + submitSearch(intent.keyword) } - }.onError { - AppLog.put("加载书架数据失败", it) + + is SearchIntent.OpenSearchBook -> { + emitEffect( + SearchEffect.OpenBookInfo( + name = intent.book.name, + author = intent.book.author, + bookUrl = intent.book.bookUrl, + ) + ) + } + + is SearchIntent.OpenBookshelfBook -> { + emitEffect( + SearchEffect.OpenBookInfo( + name = intent.book.name, + author = intent.book.author, + bookUrl = intent.book.bookUrl, + ) + ) + } + + is SearchIntent.DeleteHistory -> viewModelScope.launch { + repository.deleteSearchKeyword(intent.item) + } + + is SearchIntent.SetClearHistoryDialogVisible -> { + _uiState.update { it.copy(showClearHistoryDialog = intent.visible) } + } + + SearchIntent.ConfirmClearHistory -> { + _uiState.update { it.copy(showClearHistoryDialog = false) } + viewModelScope.launch { + repository.clearSearchKeywords() + } + } + + is SearchIntent.SetScopeSheetVisible -> { + _uiState.update { it.copy(showScopeSheet = intent.visible) } + } + + SearchIntent.SelectAllScope -> { + searchScope.update("") + syncScopeState() + } + + is SearchIntent.ToggleScopeGroup -> toggleScopeGroup(intent.groupName) + is SearchIntent.ToggleScopeSource -> toggleScopeSource(intent.source) + is SearchIntent.RemoveScopeItem -> { + searchScope.remove(intent.scopeName) + syncScopeState() + } + + is SearchIntent.TogglePrecision -> { + appCtx.putPrefBoolean(PreferKey.precisionSearch, intent.enabled) + _uiState.update { it.copy(isPrecisionSearch = intent.enabled) } + restartCommittedSearchIfNeeded() + } + + SearchIntent.ConfirmEmptyScopeAction -> handleEmptyScopeActionConfirmed() + SearchIntent.DismissEmptyScopeAction -> { + _uiState.update { it.copy(emptyScopeAction = null) } + } + + SearchIntent.OpenSourceManage -> emitEffect(SearchEffect.OpenSourceManage) } } - fun isInBookShelf(name: String, author: String, url: String?): BookShelfState { - val exactMatch = bookshelf.any { it.name == name && it.author == author && it.url == url } + fun getBookShelfStateFlow(item: SearchBook): Flow { + return bookshelfKeys + .map { keys -> resolveBookShelfState(item, keys) } + .distinctUntilChanged() + } + + override fun onCleared() { + super.onCleared() + searchSession.close() + } + + private fun initialize(key: String?, scopeRaw: String?) { + scopeRaw?.let { + searchScope.update(it, postValue = false) + } + syncScopeState() + + val initKey = key?.trim().orEmpty() + if (initKey.isNotEmpty()) { + updateQuery(initKey, showSuggestions = false) + submitSearch(initKey) + } else if (key != null) { + updateQuery(initKey, showSuggestions = true) + } + } + + private fun observeEnabledGroups() { + viewModelScope.launch { + repository.enabledGroups + .catch { emit(emptyList()) } + .collect { groups -> + _uiState.update { it.copy(enabledGroups = groups) } + } + } + } + + private fun observeEnabledSources() { + viewModelScope.launch { + repository.enabledSources + .catch { emit(emptyList()) } + .collect { sources -> + _uiState.update { it.copy(enabledSources = sources) } + } + } + } + + private fun observeBookshelf() { + viewModelScope.launch { + repository.bookshelfKeys + .catch { emit(emptySet()) } + .collect { keys -> + bookshelfKeys.value = keys + } + } + } + + private fun observeQueryHistory() { + viewModelScope.launch { + queryFlow + .map { it.trim() } + .distinctUntilChanged() + .flatMapLatest { repository.searchHistory(it) } + .catch { emit(emptyList()) } + .collect { history -> + _uiState.update { it.copy(history = history) } + } + } + } + + private fun observeQueryBookshelfHints() { + viewModelScope.launch { + queryFlow + .map { it.trim() } + .distinctUntilChanged() + .flatMapLatest { repository.searchBookshelf(it) } + .catch { emit(emptyList()) } + .collect { books -> + _uiState.update { it.copy(bookshelfHints = books) } + } + } + } + + private fun observeSearchSession() { + viewModelScope.launch { + searchSession.events.collect { event -> + when (event) { + SearchSessionEvent.Started -> { + _uiState.update { it.copy(isSearching = true) } + } + + is SearchSessionEvent.Progress -> { + _uiState.update { + it.copy( + results = event.books, + processedSources = event.processedSources, + totalSources = event.totalSources, + ) + } + } + + is SearchSessionEvent.Finished -> { + _uiState.update { state -> + val emptyAction = if (event.isEmpty && !searchScope.isAll()) { + SearchEmptyScopeAction( + scopeDisplay = searchScope.display, + wasPrecisionSearch = state.isPrecisionSearch, + ) + } else { + null + } + state.copy( + isSearching = false, + hasMore = event.hasMore, + emptyScopeAction = emptyAction, + ) + } + } + + is SearchSessionEvent.Canceled -> { + _uiState.update { it.copy(isSearching = false) } + event.throwable?.localizedMessage + ?.takeIf { it.isNotBlank() } + ?.let { emitEffect(SearchEffect.ShowMessage(it)) } + } + } + } + } + } + + private fun updateQuery(query: String, showSuggestions: Boolean) { + if (showSuggestions && _uiState.value.isSearching) { + searchSession.stop() + } + queryFlow.value = query + _uiState.update { + it.copy( + query = query, + showSuggestions = showSuggestions, + isManualStop = false, + emptyScopeAction = null, + ) + } + } + + private fun submitSearch(keyOverride: String? = null) { + val keyword = keyOverride?.trim() ?: queryFlow.value.trim() + if (keyword.isBlank()) return + + updateQuery(keyword, showSuggestions = false) + + currentSearchId = System.currentTimeMillis() + _uiState.update { + it.copy( + committedQuery = keyword, + results = emptyList(), + isManualStop = false, + hasMore = true, + processedSources = 0, + totalSources = 0, + emptyScopeAction = null, + ) + } + + viewModelScope.launch { + repository.saveSearchKeyword(keyword) + } + searchSession.search(currentSearchId, keyword) + } + + private fun loadMore() { + val state = _uiState.value + if (state.isSearching) return + if (state.committedQuery.isBlank()) return + if (!state.hasMore) return + + _uiState.update { + it.copy( + isManualStop = false, + showSuggestions = false, + ) + } + searchSession.search(currentSearchId, state.committedQuery) + } + + private fun stopSearch() { + _uiState.update { it.copy(isManualStop = true) } + searchSession.stop() + } + + private fun toggleScopeGroup(groupName: String) { + if (searchScope.isSource()) { + searchScope.update("") + } + val selected = searchScope.displayNames.toMutableSet() + if (selected.contains(groupName)) { + selected.remove(groupName) + } else { + selected.add(groupName) + } + searchScope.update(selected.toList()) + syncScopeState() + } + + private fun toggleScopeSource(source: BookSourcePart) { + val selectedUrls = if (searchScope.isSource()) { + searchScope.sourceUrls.toMutableSet() + } else { + mutableSetOf() + } + + if (selectedUrls.contains(source.bookSourceUrl)) { + selectedUrls.remove(source.bookSourceUrl) + } else { + selectedUrls.add(source.bookSourceUrl) + } + + if (selectedUrls.isEmpty()) { + searchScope.update("") + } else { + val selectedSources = _uiState.value.enabledSources.filter { + selectedUrls.contains(it.bookSourceUrl) + } + searchScope.updateSources(selectedSources) + } + syncScopeState() + } + + private fun handleEmptyScopeActionConfirmed() { + val action = _uiState.value.emptyScopeAction ?: return + _uiState.update { it.copy(emptyScopeAction = null) } + + if (action.wasPrecisionSearch) { + appCtx.putPrefBoolean(PreferKey.precisionSearch, false) + _uiState.update { it.copy(isPrecisionSearch = false) } + } else { + searchScope.update("") + syncScopeState() + } + + restartCommittedSearchIfNeeded() + } + + private fun restartCommittedSearchIfNeeded() { + val committed = _uiState.value.committedQuery + if (committed.isNotBlank()) { + submitSearch(committed) + } + } + + private fun syncScopeState() { + _uiState.update { + it.copy( + scopeDisplay = searchScope.display, + scopeDisplayNames = searchScope.displayNames, + selectedScopeSourceUrls = searchScope.sourceUrls.toSet(), + isAllScope = searchScope.isAll(), + isSourceScope = searchScope.isSource(), + ) + } + } + + private fun resolveBookShelfState(item: SearchBook, shelf: Set): BookShelfState { + val exactMatch = shelf.any { + it.name == item.name && it.author == item.author && it.url == item.bookUrl + } if (exactMatch) return BookShelfState.IN_SHELF - val sameNameAuthor = bookshelf.any { it.name == name && it.author == author && it.url != url } + val sameNameAuthor = shelf.any { + it.name == item.name && it.author == item.author && it.url != item.bookUrl + } if (sameNameAuthor) return BookShelfState.SAME_NAME_AUTHOR return BookShelfState.NOT_IN_SHELF } - /** - * 开始搜索 - */ - fun search(key: String) { - execute { - if ((searchKey == key) || key.isNotEmpty()) { - searchModel.cancelSearch() - searchID = System.currentTimeMillis() - searchBookLiveData.postValue(emptyList()) - searchKey = key - hasMore = true - } - if (searchKey.isEmpty()) { - return@execute - } - searchModel.search(searchID, searchKey) - } + private fun emitEffect(effect: SearchEffect) { + _effects.tryEmit(effect) } - - /** - * 停止搜索 - */ - fun stop() { - searchModel.cancelSearch() - } - - fun pause() { - searchModel.pause() - } - - fun resume() { - searchModel.resume() - } - - /** - * 保存搜索关键字 - */ - fun saveSearchKey(key: String) { - execute { - appDb.searchKeywordDao.get(key)?.let { - it.usage += 1 - it.lastUseTime = System.currentTimeMillis() - appDb.searchKeywordDao.update(it) - } ?: appDb.searchKeywordDao.insert(SearchKeyword(key, 1)) - } - } - - /** - * 清楚搜索关键字 - */ - fun clearHistory() { - execute { - appDb.searchKeywordDao.deleteAll() - } - } - - fun deleteHistory(searchKeyword: SearchKeyword) { - execute { - appDb.searchKeywordDao.delete(searchKeyword) - } - } - - override fun onCleared() { - super.onCleared() - searchModel.close() - } - } diff --git a/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentScreen.kt b/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentScreen.kt index ac90bcc8d..88f387085 100644 --- a/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentScreen.kt +++ b/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentScreen.kt @@ -15,8 +15,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items @@ -42,8 +40,6 @@ import androidx.compose.material3.Icon import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.ListItem import androidx.compose.material3.ListItemDefaults -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton import androidx.compose.material3.animateFloatingActionButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -62,10 +58,9 @@ import io.legado.app.data.entities.SearchContentHistory 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.EmptyMessageView +import io.legado.app.ui.widget.components.EmptyMessage import io.legado.app.ui.widget.components.SearchBarSection import io.legado.app.ui.widget.components.button.MediumOutlinedButton -import io.legado.app.ui.widget.components.button.MediumOutlinedIconButton import io.legado.app.ui.widget.components.button.SmallAnimatedActionButton import io.legado.app.ui.widget.components.button.SmallIconButton import io.legado.app.ui.widget.components.button.TopBarAnimatedActionButton @@ -225,7 +220,7 @@ fun SearchContentScreen( ) { state -> when (state) { is SearchContentState.Error -> { - EmptyMessageView( + EmptyMessage( message = state.throwable.localizedMessage ?: "发生未知错误", modifier = Modifier .fillMaxSize() @@ -244,7 +239,7 @@ fun SearchContentScreen( ) } SearchContentState.EmptyResult -> { - EmptyMessageView( + EmptyMessage( message = "没有找到相关内容!", modifier = Modifier .fillMaxSize() @@ -319,7 +314,7 @@ fun SearchHistoryList( } if (history.isEmpty()) { - EmptyMessageView( + EmptyMessage( message = "暂无搜索历史", modifier = Modifier .fillMaxSize() diff --git a/app/src/main/java/io/legado/app/ui/book/toc/TocScreen.kt b/app/src/main/java/io/legado/app/ui/book/toc/TocScreen.kt index b025d7903..515c654b5 100644 --- a/app/src/main/java/io/legado/app/ui/book/toc/TocScreen.kt +++ b/app/src/main/java/io/legado/app/ui/book/toc/TocScreen.kt @@ -99,7 +99,7 @@ import io.legado.app.ui.theme.adaptiveHorizontalPadding import io.legado.app.ui.widget.CollapsibleHeader import io.legado.app.ui.widget.components.ActionItem import io.legado.app.ui.widget.components.AppScaffold -import io.legado.app.ui.widget.components.EmptyMessageView +import io.legado.app.ui.widget.components.EmptyMessage 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 @@ -864,7 +864,7 @@ fun BookmarkListContent( ), contentAlignment = Alignment.Center ) { - EmptyMessageView( + EmptyMessage( message = "暂无书签" ) } diff --git a/app/src/main/java/io/legado/app/ui/book/toc/rule/TxtRuleScreen.kt b/app/src/main/java/io/legado/app/ui/book/toc/rule/TxtRuleScreen.kt index 09d87006d..db7d49be9 100644 --- a/app/src/main/java/io/legado/app/ui/book/toc/rule/TxtRuleScreen.kt +++ b/app/src/main/java/io/legado/app/ui/book/toc/rule/TxtRuleScreen.kt @@ -51,6 +51,7 @@ import io.legado.app.ui.widget.components.alert.AppAlertDialog import io.legado.app.ui.widget.components.button.SmallIconButton import io.legado.app.ui.widget.components.card.ReorderableSelectionItem import io.legado.app.ui.widget.components.filePicker.FilePickerSheet +import io.legado.app.ui.widget.components.icon.AppIcons import io.legado.app.ui.widget.components.importComponents.BatchImportDialog import io.legado.app.ui.widget.components.importComponents.SourceInputDialog import io.legado.app.ui.widget.components.lazylist.FastScrollLazyColumn @@ -204,17 +205,17 @@ fun TxtRuleScreen( } AppAlertDialog( - data = showDeleteRuleDialog, // 传入 nullable ?rule 对象 + data = showDeleteRuleDialog, onDismissRequest = { showDeleteRuleDialog = null }, title = stringResource(R.string.delete), + text = stringResource(R.string.sure_del), confirmText = stringResource(R.string.ok), onConfirm = { rule -> viewModel.delete(rule) showDeleteRuleDialog = null }, dismissText = stringResource(R.string.cancel), - onDismiss = { showDeleteRuleDialog = null }, - content = { Text(text = stringResource(R.string.del_msg)) } + onDismiss = { showDeleteRuleDialog = null } ) RuleEditSheet( @@ -228,7 +229,7 @@ fun TxtRuleScreen( editingRule = null }, onSave = { updatedRule -> - //TODO:我很想把他改为自增主键,但为了兼容性日后再? + //TODO:我很想把他改为自增主键,但为了兼容性日后再�? if (editingRule == null) { viewModel.insert(updatedRule) } else { @@ -346,7 +347,7 @@ fun TxtRuleScreen( trailingAction = { SmallIconButton( onClick = { showDeleteRuleDialog = item.rule }, - imageVector = Icons.Default.Delete + imageVector = AppIcons.Delete ) } ) diff --git a/app/src/main/java/io/legado/app/ui/config/otherConfig/CheckSourceBottomSheet.kt b/app/src/main/java/io/legado/app/ui/config/otherConfig/CheckSourceBottomSheet.kt index e01b733c5..84e75c424 100644 --- a/app/src/main/java/io/legado/app/ui/config/otherConfig/CheckSourceBottomSheet.kt +++ b/app/src/main/java/io/legado/app/ui/config/otherConfig/CheckSourceBottomSheet.kt @@ -18,6 +18,8 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import io.legado.app.R import io.legado.app.ui.theme.LegadoTheme +import io.legado.app.ui.widget.components.button.ConfirmDismissButtonsRow +import io.legado.app.ui.widget.components.card.GlassCard import io.legado.app.ui.widget.components.checkBox.CheckboxGroupContainer import io.legado.app.ui.widget.components.checkBox.CheckboxItem import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet @@ -36,116 +38,102 @@ fun CheckSourceBottomSheet( AppModalBottomSheet( show = show, - onDismissRequest = onDismiss + onDismissRequest = onDismiss, + title = stringResource(R.string.check_source_config) ) { Column( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp) .padding(bottom = 32.dp) ) { - AppText( - text = stringResource(R.string.check_source_config), - style = LegadoTheme.typography.titleLarge, - modifier = Modifier.padding(vertical = 16.dp) - ) - SliderSettingItem( - title = stringResource(R.string.check_source_timeout), - color = MaterialTheme.colorScheme.surface, - value = viewModel.checkSourceTimeout.toFloat(), - defaultValue = 180f, - onValueChange = { viewModel.checkSourceTimeout = it.toLong() }, - valueRange = 0f..300f, - ) + GlassCard { + SliderSettingItem( + title = stringResource(R.string.check_source_timeout), + value = viewModel.checkSourceTimeout.toFloat(), + defaultValue = 180f, + onValueChange = { viewModel.checkSourceTimeout = it.toLong() }, + valueRange = 0f..300f, + ) + } + Spacer(modifier = Modifier.padding(8.dp)) - CheckboxGroupContainer(columns = 2) { - item { - CheckboxItem( - title = stringResource(R.string.search), - checked = viewModel.checkSearch, - onCheckedChange = { - viewModel.checkSearch = it - if (!it && !viewModel.checkDiscovery) { - viewModel.checkDiscovery = true - } + Column( + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + CheckboxItem( + title = stringResource(R.string.search), + checked = viewModel.checkSearch, + onCheckedChange = { + viewModel.checkSearch = it + if (!it && !viewModel.checkDiscovery) { + viewModel.checkDiscovery = true } - ) - } + } + ) - item { - CheckboxItem( - title = stringResource(R.string.discovery), - checked = viewModel.checkDiscovery, - onCheckedChange = { - viewModel.checkDiscovery = it - if (!it && !viewModel.checkSearch) { - viewModel.checkSearch = true - } + CheckboxItem( + title = stringResource(R.string.discovery), + checked = viewModel.checkDiscovery, + onCheckedChange = { + viewModel.checkDiscovery = it + if (!it && !viewModel.checkSearch) { + viewModel.checkSearch = true } - ) - } + } + ) + + + + CheckboxItem( + title = stringResource(R.string.source_tab_info), + checked = viewModel.checkInfo, + onCheckedChange = { + viewModel.checkInfo = it + if (!it) { + viewModel.checkCategory = false + viewModel.checkContent = false + } + } + ) + + + CheckboxItem( + title = stringResource(R.string.chapter_list), + checked = viewModel.checkCategory, + enabled = viewModel.checkInfo, + onCheckedChange = { + viewModel.checkCategory = it + if (!it) viewModel.checkContent = false + } + ) + + + CheckboxItem( + title = stringResource(R.string.source_tab_content), + checked = viewModel.checkContent, + enabled = viewModel.checkCategory, + onCheckedChange = { viewModel.checkContent = it } + ) } - - CheckboxGroupContainer(columns = 3) { - item { - CheckboxItem( - title = stringResource(R.string.source_tab_info), - checked = viewModel.checkInfo, - onCheckedChange = { - viewModel.checkInfo = it - if (!it) { - viewModel.checkCategory = false - viewModel.checkContent = false - } - } - ) - } - - item { - CheckboxItem( - title = stringResource(R.string.chapter_list), - checked = viewModel.checkCategory, - enabled = viewModel.checkInfo, - onCheckedChange = { - viewModel.checkCategory = it - if (!it) viewModel.checkContent = false - } - ) - } - - item { - CheckboxItem( - title = stringResource(R.string.source_tab_content), - checked = viewModel.checkContent, - enabled = viewModel.checkCategory, - onCheckedChange = { viewModel.checkContent = it } - ) - } - } - - Row( + ConfirmDismissButtonsRow( modifier = Modifier .fillMaxWidth() .padding(top = 16.dp), - horizontalArrangement = Arrangement.End - ) { - TextButton(onClick = onDismiss) { - AppText(stringResource(R.string.cancel)) - } - Button(onClick = { + onDismiss = onDismiss, + onConfirm = { if (viewModel.saveCheckSourceConfig()) { onDismiss() } else { Toast.makeText(context, R.string.error, Toast.LENGTH_SHORT).show() } - }) { - AppText(stringResource(R.string.ok)) - } - } + }, + dismissText = stringResource(R.string.cancel), + confirmText = stringResource(R.string.ok) + ) } } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/config/otherConfig/DirectLinkUploadBottomSheet.kt b/app/src/main/java/io/legado/app/ui/config/otherConfig/DirectLinkUploadBottomSheet.kt index 10f0ff114..9a8ed530b 100644 --- a/app/src/main/java/io/legado/app/ui/config/otherConfig/DirectLinkUploadBottomSheet.kt +++ b/app/src/main/java/io/legado/app/ui/config/otherConfig/DirectLinkUploadBottomSheet.kt @@ -1,5 +1,6 @@ package io.legado.app.ui.config.otherConfig +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -10,6 +11,7 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Checklist import androidx.compose.material.icons.filled.ContentCopy import androidx.compose.material.icons.filled.ContentPaste import androidx.compose.material.icons.filled.Download @@ -33,8 +35,10 @@ import androidx.compose.ui.unit.dp import io.legado.app.R import io.legado.app.help.DirectLinkUpload import io.legado.app.lib.dialogs.selector +import io.legado.app.ui.theme.LegadoTheme import io.legado.app.ui.widget.components.AppTextField import io.legado.app.ui.widget.components.alert.AppAlertDialog +import io.legado.app.ui.widget.components.button.ConfirmDismissButtonsRow import io.legado.app.ui.widget.components.button.MediumIconButton import io.legado.app.ui.widget.components.checkBox.CheckboxItem import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu @@ -65,57 +69,68 @@ fun DirectLinkUploadBottomSheet( AppModalBottomSheet( show = show, title = stringResource(R.string.direct_link_upload_config), - endAction = { + startAction = { MediumIconButton( - onClick = { showMenu = true }, - imageVector = Icons.Default.MoreVert + onClick = { + viewModel.testRule { result -> showTestResult = result } + }, + imageVector = Icons.Default.Checklist ) - RoundDropdownMenu( - expanded = showMenu, - onDismissRequest = { showMenu = false } - ) { - RoundDropdownMenuItem( - text = "导入默认", - leadingIcon = { Icon(Icons.Default.Download, null) }, - onClick = { - showMenu = false - context.selector(DirectLinkUpload.defaultRules) { _, rule, _ -> - viewModel.upView(rule) - } - } + }, + endAction = { + Box { + MediumIconButton( + onClick = { showMenu = true }, + imageVector = Icons.Default.MoreVert ) - RoundDropdownMenuItem( - text = stringResource(R.string.copy_rule), - leadingIcon = { Icon(Icons.Default.ContentCopy, null) }, - onClick = { - showMenu = false - val rule = DirectLinkUpload.Rule( - viewModel.uploadUrl, - viewModel.downloadUrlRule, - viewModel.summary, - viewModel.compress - ) - context.sendToClip(GSON.toJson(rule)) - } - ) - RoundDropdownMenuItem( - text = stringResource(R.string.paste_rule), - leadingIcon = { Icon(Icons.Default.ContentPaste, null) }, - onClick = { - showMenu = false - runCatching { - context.getClipText()?.let { - val rule = - GSON.fromJsonObject(it) - .getOrThrow() + RoundDropdownMenu( + expanded = showMenu, + onDismissRequest = { showMenu = false } + ) { + RoundDropdownMenuItem( + text = "导入默认", + leadingIcon = { Icon(Icons.Default.Download, null) }, + onClick = { + showMenu = false + context.selector(DirectLinkUpload.defaultRules) { _, rule, _ -> viewModel.upView(rule) } - }.onFailure { - context.toastOnUi("剪贴板为空或格式不对") } - } - ) + ) + RoundDropdownMenuItem( + text = stringResource(R.string.copy_rule), + leadingIcon = { Icon(Icons.Default.ContentCopy, null) }, + onClick = { + showMenu = false + val rule = DirectLinkUpload.Rule( + viewModel.uploadUrl, + viewModel.downloadUrlRule, + viewModel.summary, + viewModel.compress + ) + context.sendToClip(GSON.toJson(rule)) + } + ) + RoundDropdownMenuItem( + text = stringResource(R.string.paste_rule), + leadingIcon = { Icon(Icons.Default.ContentPaste, null) }, + onClick = { + showMenu = false + runCatching { + context.getClipText()?.let { + val rule = + GSON.fromJsonObject(it) + .getOrThrow() + viewModel.upView(rule) + } + }.onFailure { + context.toastOnUi("剪贴板为空或格式不对") + } + } + ) + } } + }, onDismissRequest = onDismiss ) { @@ -128,6 +143,7 @@ fun DirectLinkUploadBottomSheet( AppTextField( value = viewModel.uploadUrl, onValueChange = { viewModel.uploadUrl = it }, + backgroundColor = LegadoTheme.colorScheme.onSheetContent, label = stringResource(R.string.upload_url), modifier = Modifier.fillMaxWidth(), ) @@ -137,6 +153,7 @@ fun DirectLinkUploadBottomSheet( AppTextField( value = viewModel.downloadUrlRule, onValueChange = { viewModel.downloadUrlRule = it }, + backgroundColor = LegadoTheme.colorScheme.onSheetContent, label = stringResource(R.string.download_url_rule), modifier = Modifier.fillMaxWidth(), ) @@ -146,6 +163,7 @@ fun DirectLinkUploadBottomSheet( AppTextField( value = viewModel.summary, onValueChange = { viewModel.summary = it }, + backgroundColor = LegadoTheme.colorScheme.onSheetContent, label = stringResource(R.string.summary), modifier = Modifier.fillMaxWidth(), ) @@ -154,33 +172,26 @@ fun DirectLinkUploadBottomSheet( CheckboxItem( title = stringResource(R.string.is_compress), - color = MaterialTheme.colorScheme.surface, + color = LegadoTheme.colorScheme.onSheetContent, checked = viewModel.compress, onCheckedChange = { viewModel.compress = it } ) Spacer(Modifier.height(24.dp)) - Row(modifier = Modifier.fillMaxWidth()) { - TextButton(onClick = { - viewModel.testRule { result -> showTestResult = result } - }) { - AppText("测试") - } - Spacer(Modifier.weight(1f)) - OutlinedButton(onClick = onDismiss) { - AppText(stringResource(R.string.cancel)) - } - Button(onClick = { + ConfirmDismissButtonsRow( + modifier = Modifier.fillMaxWidth(), + onDismiss = onDismiss, + onConfirm = { if (viewModel.saveDirectLinkRule()) { onDismiss() } else { context.toastOnUi("请填写完整信息") } - }) { - AppText(stringResource(R.string.ok)) - } - } + }, + dismissText = stringResource(R.string.cancel), + confirmText = stringResource(R.string.ok) + ) } } diff --git a/app/src/main/java/io/legado/app/ui/config/readConfig/PageKeySheet.kt b/app/src/main/java/io/legado/app/ui/config/readConfig/PageKeySheet.kt index 52b6d9888..16b5acf90 100644 --- a/app/src/main/java/io/legado/app/ui/config/readConfig/PageKeySheet.kt +++ b/app/src/main/java/io/legado/app/ui/config/readConfig/PageKeySheet.kt @@ -23,6 +23,8 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import io.legado.app.R import io.legado.app.ui.theme.LegadoTheme +import io.legado.app.ui.widget.components.AppTextField +import io.legado.app.ui.widget.components.button.ConfirmDismissButtonsRow import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet import io.legado.app.ui.widget.components.text.AppText @@ -35,24 +37,22 @@ fun PageKeySheet( var prevKeys by remember { mutableStateOf(ReadConfig.prevKeys) } var nextKeys by remember { mutableStateOf(ReadConfig.nextKeys) } - AppModalBottomSheet(show = show, onDismissRequest = onDismissRequest) { + AppModalBottomSheet( + show = show, + onDismissRequest = onDismissRequest, + title = stringResource(R.string.custom_page_key) + ) { Column( - modifier = Modifier.Companion + modifier = Modifier .fillMaxWidth() .padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp) ) { - AppText( - text = stringResource(R.string.custom_page_key), - style = LegadoTheme.typography.titleLarge, - fontWeight = FontWeight.Companion.Bold - ) - - OutlinedTextField( + AppTextField( value = prevKeys, onValueChange = { prevKeys = it }, - label = { AppText(stringResource(R.string.prev_page_key)) }, - modifier = Modifier.Companion + label = stringResource(R.string.prev_page_key), + modifier = Modifier .fillMaxWidth() .onPreviewKeyEvent { event -> if (event.nativeKeyEvent.action == KeyEvent.ACTION_DOWN) { @@ -71,10 +71,10 @@ fun PageKeySheet( singleLine = true ) - OutlinedTextField( + AppTextField( value = nextKeys, onValueChange = { nextKeys = it }, - label = { AppText(stringResource(R.string.next_page_key)) }, + label = stringResource(R.string.next_page_key), modifier = Modifier.Companion .fillMaxWidth() .onPreviewKeyEvent { event -> @@ -96,35 +96,23 @@ fun PageKeySheet( AppText( text = stringResource(R.string.page_key_set_help), - style = LegadoTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant + style = LegadoTheme.typography.bodyMedium ) - Row( - modifier = Modifier.Companion.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - OutlinedButton( - onClick = { - prevKeys = "" - nextKeys = "" - }, - modifier = Modifier.Companion.weight(1f) - ) { - AppText(stringResource(R.string.reset)) - } - - Button( - onClick = { - ReadConfig.prevKeys = prevKeys - ReadConfig.nextKeys = nextKeys - onDismissRequest() - }, - modifier = Modifier.Companion.weight(1f) - ) { - AppText(stringResource(R.string.ok)) - } - } + ConfirmDismissButtonsRow( + modifier = Modifier.fillMaxWidth(), + onDismiss = { + prevKeys = "" + nextKeys = "" + }, + onConfirm = { + ReadConfig.prevKeys = prevKeys + ReadConfig.nextKeys = nextKeys + onDismissRequest() + }, + dismissText = stringResource(R.string.reset), + confirmText = stringResource(R.string.ok) + ) } } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/config/themeConfig/BackgroundImageManageSheet.kt b/app/src/main/java/io/legado/app/ui/config/themeConfig/BackgroundImageManageSheet.kt index e1a95bff2..a4e5add30 100644 --- a/app/src/main/java/io/legado/app/ui/config/themeConfig/BackgroundImageManageSheet.kt +++ b/app/src/main/java/io/legado/app/ui/config/themeConfig/BackgroundImageManageSheet.kt @@ -31,7 +31,9 @@ import androidx.compose.ui.unit.dp 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.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 import kotlinx.coroutines.launch @@ -68,35 +70,31 @@ fun BackgroundImageManageSheet( AppModalBottomSheet( show = show, - onDismissRequest = onDismissRequest + onDismissRequest = onDismissRequest, + title = stringResource(R.string.background_image), ) { Column( modifier = Modifier .fillMaxWidth() .padding(bottom = 24.dp) ) { - AppText( - text = stringResource(R.string.background_image), - style = LegadoTheme.typography.titleMedium, - modifier = Modifier.padding(16.dp) - ) if (currentPath.isNullOrBlank()) { Surface( onClick = { showFilePicker = true }, shape = MaterialTheme.shapes.medium, - color = MaterialTheme.colorScheme.surfaceContainerHigh, + color = LegadoTheme.colorScheme.surfaceContainerHigh, modifier = Modifier .fillMaxWidth() .aspectRatio(16f / 9f) .padding(16.dp) ) { Box(contentAlignment = Alignment.Center) { - Icon( + AppIcon( imageVector = Icons.Default.Add, contentDescription = "Add", modifier = Modifier.size(48.dp), - tint = MaterialTheme.colorScheme.primary + tint = LegadoTheme.colorScheme.primary ) } } @@ -119,24 +117,14 @@ fun BackgroundImageManageSheet( contentScale = ContentScale.Crop ) } - IconButton( + SmallTonalIconButton( onClick = { viewModel.removeBackground(isDarkTheme) }, modifier = Modifier .align(Alignment.TopEnd) .padding(8.dp) - .size(32.dp) - ) { - Surface( - shape = CircleShape, - color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.7f) - ) { - Icon( - imageVector = Icons.Default.Close, - contentDescription = "Remove", - modifier = Modifier.size(20.dp) - ) - } - } + .size(32.dp), + imageVector = Icons.Default.Close + ) } } } diff --git a/app/src/main/java/io/legado/app/ui/dict/rule/DictRuleScreen.kt b/app/src/main/java/io/legado/app/ui/dict/rule/DictRuleScreen.kt index 79c276cd4..22c41a364 100644 --- a/app/src/main/java/io/legado/app/ui/dict/rule/DictRuleScreen.kt +++ b/app/src/main/java/io/legado/app/ui/dict/rule/DictRuleScreen.kt @@ -44,6 +44,7 @@ import io.legado.app.ui.widget.components.alert.AppAlertDialog import io.legado.app.ui.widget.components.button.SmallIconButton import io.legado.app.ui.widget.components.card.ReorderableSelectionItem import io.legado.app.ui.widget.components.filePicker.FilePickerSheet +import io.legado.app.ui.widget.components.icon.AppIcons import io.legado.app.ui.widget.components.importComponents.BatchImportDialog import io.legado.app.ui.widget.components.importComponents.SourceInputDialog import io.legado.app.ui.widget.components.lazylist.FastScrollLazyColumn @@ -320,7 +321,7 @@ fun DictRuleScreen( trailingAction = { SmallIconButton( onClick = { showDeleteRuleDialog = item.rule }, - imageVector = Icons.Default.Delete + imageVector = AppIcons.Delete ) } ) diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookItem.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookItem.kt index 2a8e48c26..68bb09c73 100644 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookItem.kt +++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookItem.kt @@ -376,6 +376,7 @@ fun BookGroupItemList( fun BookItem( book: BookShelfItem, layoutMode: Int, + modifier: Modifier = Modifier, gridStyle: Int = 0, isCompact: Boolean = false, isUpdating: Boolean = false, @@ -403,6 +404,7 @@ fun BookItem( isGrid = layoutMode != 0, gridStyle = gridStyle, isCompact = isCompact, + modifier = modifier, cover = { modifier -> BookshelfCover( name = book.name, diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt index 5bb726c4e..08025e12a 100644 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt +++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt @@ -74,6 +74,8 @@ import io.legado.app.ui.theme.LegadoTheme import io.legado.app.ui.theme.ThemeResolver import io.legado.app.ui.theme.adaptiveContentPadding import io.legado.app.ui.theme.adaptiveHorizontalPadding +import io.legado.app.ui.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.filePicker.FilePickerSheet import io.legado.app.ui.widget.components.importComponents.SourceInputDialog @@ -197,20 +199,18 @@ fun BookshelfScreen( } val bookGroupStyle = BookshelfConfig.bookGroupStyle - // 控制是否处于“文件夹列表”根视图,还是“文件夹内部”书籍视? + // 控制是否处于“文件夹列表”根视图,还是“文件夹内部”书籍视图 var isInFolderRoot by remember(bookGroupStyle) { mutableStateOf(bookGroupStyle == 2) } - val baseTitle = when { - bookGroupStyle == 1 -> { + val baseTitle = when (bookGroupStyle) { + 1 -> { uiState.groups.getOrNull(pagerState.currentPage)?.groupName ?: stringResource(R.string.bookshelf) } - - bookGroupStyle == 2 && uiState.groups.isNotEmpty() -> { + 2 if uiState.groups.isNotEmpty() -> { if (isInFolderRoot) stringResource(R.string.bookshelf) else uiState.groups.getOrNull(pagerState.currentPage)?.groupName ?: stringResource(R.string.bookshelf) } - else -> stringResource(R.string.bookshelf) } val title = if (uiState.upBooksCount > 0) { @@ -329,7 +329,7 @@ fun BookshelfScreen( Row( modifier = Modifier .fillMaxWidth() - .adaptiveHorizontalPadding(), + .adaptiveHorizontalPaddingTab(), verticalAlignment = Alignment.CenterVertically ) { val tabTitles = remember(uiState.groups) { @@ -600,6 +600,19 @@ fun BookshelfPage( onBookClick: (BookShelfItem) -> Unit, onBookLongClick: (BookShelfItem) -> Unit ) { + if (books.isEmpty()) { + EmptyMessage( + modifier = Modifier + .fillMaxSize() + .padding( + top = paddingValues.calculateTopPadding(), + bottom = paddingValues.calculateBottomPadding() + ), + messageResId = R.string.bookshelf_empty + ) + return + } + val columns = if (bookshelfLayoutMode == 0) bookshelfLayoutList else bookshelfLayoutGrid val isGridMode = bookshelfLayoutMode != 0 val totalHorizontalPadding = @@ -623,6 +636,7 @@ fun BookshelfPage( items(books, key = { it.bookUrl }) { book -> BookItem( book = book, + modifier = Modifier.animateItem(), layoutMode = bookshelfLayoutMode, gridStyle = BookshelfConfig.bookshelfGridLayout, isCompact = BookshelfConfig.bookshelfLayoutCompact, diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/GroupManageSheet.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/GroupManageSheet.kt index dbeec5e34..a25e76897 100644 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/GroupManageSheet.kt +++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/GroupManageSheet.kt @@ -27,6 +27,8 @@ import androidx.compose.ui.unit.dp import io.legado.app.R import io.legado.app.data.entities.BookGroup import io.legado.app.ui.book.group.GroupEditContent +import io.legado.app.ui.book.group.GroupDeleteAction +import io.legado.app.ui.book.group.GroupResetCoverAction import io.legado.app.ui.book.group.GroupViewModel import io.legado.app.ui.theme.LegadoTheme import io.legado.app.ui.widget.components.button.SmallIconButton @@ -48,6 +50,7 @@ fun GroupManageSheet( val groups by bookshelfViewModel.allGroupsFlow.collectAsState() var editingGroup by remember { mutableStateOf(null) } var isEditing by remember { mutableStateOf(false) } + var coverPath by remember(editingGroup) { mutableStateOf(editingGroup?.cover) } var listData by remember { mutableStateOf(groups) } val listState = rememberLazyListState() @@ -76,15 +79,36 @@ fun GroupManageSheet( show = show, onDismissRequest = onDismissRequest, title = if (!isEditing) stringResource(R.string.group_manage) else stringResource(R.string.group_edit), + startAction = editingGroup?.takeIf { + isEditing && (it.groupId > 0 || it.groupId == Long.MIN_VALUE) + }?.let { group -> + { + GroupDeleteAction( + group = group, + onDismissRequest = { + editingGroup = null + isEditing = false + }, + viewModel = viewModel + ) + } + }, endAction = { if (!isEditing) { SmallIconButton( onClick = { editingGroup = null + coverPath = null isEditing = true }, imageVector = Icons.Default.Add ) + } else { + GroupResetCoverAction( + group = editingGroup, + onCoverPathChange = { coverPath = it }, + viewModel = viewModel + ) } } ) { @@ -98,7 +122,12 @@ fun GroupManageSheet( if (editing) { GroupEditContent( group = editingGroup, - onDismissRequest = { isEditing = false }, + onDismissRequest = { + editingGroup = null + isEditing = false + }, + coverPath = coverPath, + onCoverPathChange = { coverPath = it }, viewModel = viewModel ) } else { @@ -115,12 +144,13 @@ fun GroupManageSheet( title = group.groupName.ifBlank { manageNameInfo.suffix.orEmpty() }, subtitle = if (group.groupName.isNotBlank()) manageNameInfo.suffix else null, isEnabled = group.show, - containerColor = LegadoTheme.colorScheme.surface, + containerColor = LegadoTheme.colorScheme.onSheetContent, onEnabledChange = { isChecked -> viewModel.upGroup(group.copy(show = isChecked)) }, onClickEdit = { editingGroup = group + coverPath = group.cover isEditing = true } ) diff --git a/app/src/main/java/io/legado/app/ui/main/explore/ExploreScreen.kt b/app/src/main/java/io/legado/app/ui/main/explore/ExploreScreen.kt index 36cc2fe0e..bce917616 100644 --- a/app/src/main/java/io/legado/app/ui/main/explore/ExploreScreen.kt +++ b/app/src/main/java/io/legado/app/ui/main/explore/ExploreScreen.kt @@ -67,6 +67,7 @@ import io.legado.app.ui.widget.components.card.TextCard import io.legado.app.ui.widget.components.divider.PillHeaderDivider 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.EmptyMessage import io.legado.app.ui.widget.components.lazylist.FastScrollLazyColumn import io.legado.app.ui.widget.components.list.ListScaffold import io.legado.app.ui.widget.components.list.TopFloatingStickyItem @@ -159,6 +160,19 @@ fun ExploreScreen( } ) { paddingValues -> Box(modifier = Modifier.fillMaxSize()) { + if (uiState.items.isEmpty()) { + EmptyMessage( + modifier = Modifier + .fillMaxSize() + .padding( + top = paddingValues.calculateTopPadding(), + bottom = paddingValues.calculateBottomPadding() + ), + messageResId = R.string.explore_empty + ) + return@Box + } + FastScrollLazyColumn( state = listState, modifier = Modifier.fillMaxSize(), diff --git a/app/src/main/java/io/legado/app/ui/replace/edit/ReplaceEditScreen.kt b/app/src/main/java/io/legado/app/ui/replace/edit/ReplaceEditScreen.kt index c668e6e2b..c9b5a4140 100644 --- a/app/src/main/java/io/legado/app/ui/replace/edit/ReplaceEditScreen.kt +++ b/app/src/main/java/io/legado/app/ui/replace/edit/ReplaceEditScreen.kt @@ -72,6 +72,7 @@ import io.legado.app.ui.widget.components.AppScaffold import io.legado.app.ui.widget.components.AppTextField import io.legado.app.ui.widget.components.alert.AppAlertDialog import io.legado.app.ui.widget.components.button.AlertButton +import io.legado.app.ui.widget.components.button.MediumIconButton 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.TopBarNavigationButton @@ -368,21 +369,20 @@ fun GroupSelector( } } } - IconButton(onClick = onManageClick) { - Icon(Icons.Default.Settings, "Manage") - } + MediumIconButton( + onClick = onManageClick, + imageVector = Icons.Default.Settings + ) } } @Composable fun ManageGroupDialog( - show: Boolean, // 💡 新增 show 参数,外层直接传值,不要用 if 包裹! + show: Boolean, groups: List, onDismiss: () -> Unit, onDelete: (List) -> Unit ) { - // 💡 使用 Set 来管理多选状态,并且以 show 作为 key。 - // 这样每次重新打开弹窗时,选中状态会自动清空,防止上次的残留 var selectedGroups by remember(show) { mutableStateOf(emptySet()) } AppAlertDialog( @@ -395,7 +395,6 @@ fun ManageGroupDialog( } else { Column( modifier = Modifier.verticalScroll(rememberScrollState()), - // 去掉了 spacedBy(8.dp),因为 CheckboxItem 内部已经有了合适的 padding ) { groups.forEach { group -> val isSelected = selectedGroups.contains(group) @@ -418,7 +417,6 @@ fun ManageGroupDialog( confirmText = "删除选中", onConfirm = { onDelete(selectedGroups.toList()) - // 注意:外层需要在 onDelete 回调中将 show 置为 false }, dismissText = "关闭", onDismiss = onDismiss diff --git a/app/src/main/java/io/legado/app/ui/rss/favorites/RssFavoritesScreen.kt b/app/src/main/java/io/legado/app/ui/rss/favorites/RssFavoritesScreen.kt index b32a8a4b1..be95fa348 100644 --- a/app/src/main/java/io/legado/app/ui/rss/favorites/RssFavoritesScreen.kt +++ b/app/src/main/java/io/legado/app/ui/rss/favorites/RssFavoritesScreen.kt @@ -34,7 +34,7 @@ import io.legado.app.R import io.legado.app.data.entities.RssStar import io.legado.app.ui.rss.read.ReadRssActivity import io.legado.app.ui.widget.components.ActionItem -import io.legado.app.ui.widget.components.EmptyMessageView +import io.legado.app.ui.widget.components.EmptyMessage import io.legado.app.ui.widget.components.SourceIcon import io.legado.app.ui.widget.components.button.SmallIconButton import io.legado.app.ui.widget.components.card.SelectionItemCard @@ -186,7 +186,7 @@ fun RssFavoritesScreen( .padding(paddingValues), contentAlignment = Alignment.Center ) { - EmptyMessageView( + EmptyMessage( message = "还没有收藏订阅!", isLoading = state.isLoading ) @@ -203,7 +203,7 @@ fun RssFavoritesScreen( SelectionItemCard( title = rssStar.title, subtitle = if (rssStar.group.isNotBlank()) { - "${rssStar.group} ?${rssStar.pubDate ?: ""}" + "${rssStar.group} �?${rssStar.pubDate ?: ""}" } else { rssStar.pubDate }, diff --git a/app/src/main/java/io/legado/app/ui/rss/subscription/RuleSubScreen.kt b/app/src/main/java/io/legado/app/ui/rss/subscription/RuleSubScreen.kt index 973669832..60935e1b7 100644 --- a/app/src/main/java/io/legado/app/ui/rss/subscription/RuleSubScreen.kt +++ b/app/src/main/java/io/legado/app/ui/rss/subscription/RuleSubScreen.kt @@ -16,15 +16,12 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.Sort import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Edit -import androidx.compose.material3.AlertDialog import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import io.legado.app.ui.widget.components.AppFloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.TextButton import androidx.compose.material3.animateFloatingActionButton import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState @@ -49,7 +46,7 @@ import io.legado.app.ui.association.ImportReplaceRuleDialog import io.legado.app.ui.association.ImportRssSourceDialog import io.legado.app.ui.theme.LegadoTheme import io.legado.app.ui.widget.components.AppTextField -import io.legado.app.ui.widget.components.EmptyMessageView +import io.legado.app.ui.widget.components.EmptyMessage import io.legado.app.ui.widget.components.alert.AppAlertDialog import io.legado.app.ui.widget.components.card.SelectionItemCard import io.legado.app.ui.widget.components.checkBox.CheckboxGroupContainer @@ -123,7 +120,7 @@ fun RuleSubScreen( .padding(paddingValues), contentAlignment = Alignment.Center ) { - EmptyMessageView( + EmptyMessage( message = stringResource(R.string.rule_sub_empty_msg), isLoading = state.isLoading ) diff --git a/app/src/main/java/io/legado/app/ui/theme/AdaptivePadding.kt b/app/src/main/java/io/legado/app/ui/theme/AdaptivePadding.kt index 7d282484a..e5653a135 100644 --- a/app/src/main/java/io/legado/app/ui/theme/AdaptivePadding.kt +++ b/app/src/main/java/io/legado/app/ui/theme/AdaptivePadding.kt @@ -14,6 +14,13 @@ fun Modifier.adaptiveHorizontalPadding(): Modifier { return this.padding(horizontal = horizontal) } +@Composable +fun Modifier.adaptiveHorizontalPaddingTab(): Modifier { + val start = if (ThemeResolver.isMiuixEngine(composeEngine)) 12.dp else 0.dp + val end = if (ThemeResolver.isMiuixEngine(composeEngine)) 12.dp else 16.dp + return this.padding(start = start, end = end) +} + @Composable fun Modifier.adaptiveHorizontalPadding( vertical: Dp, diff --git a/app/src/main/java/io/legado/app/ui/theme/AppTheme.kt b/app/src/main/java/io/legado/app/ui/theme/AppTheme.kt index 677b85bf5..1c328c0c8 100644 --- a/app/src/main/java/io/legado/app/ui/theme/AppTheme.kt +++ b/app/src/main/java/io/legado/app/ui/theme/AppTheme.kt @@ -11,9 +11,11 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.remember import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.compositeOver import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.colorResource import io.legado.app.ui.config.themeConfig.ThemeConfig +import io.legado.app.ui.theme.LegadoTheme import top.yukonga.miuix.kmp.theme.MiuixTheme import top.yukonga.miuix.kmp.theme.ThemeController @@ -194,8 +196,10 @@ fun AppTheme( onTertiaryFixed = miuixColorScheme.onTertiaryContainer, onTertiaryFixedVariant = miuixColorScheme.onTertiaryContainer, - cardContainer = miuixColorScheme.disabledPrimary, - onCardContainer = miuixColorScheme.primary + cardContainer = miuixColorScheme.primaryContainer.copy(alpha = 0.32f) + .compositeOver(miuixColorScheme.surfaceContainer), + onCardContainer = miuixColorScheme.primary, + onSheetContent = miuixColorScheme.surface.copy(alpha = 0.5f), ) } @@ -229,32 +233,3 @@ fun AppTheme( } } } - -private fun Typography.toLegadoTypography(): LegadoTypography { - return LegadoTypography( - headlineLarge = headlineLarge, - headlineLargeEmphasized = headlineLargeEmphasized, - headlineMedium = headlineMedium, - headlineMediumEmphasized = headlineMediumEmphasized, - headlineSmall = headlineSmall, - headlineSmallEmphasized = headlineSmallEmphasized, - titleLarge = titleLarge, - titleLargeEmphasized = titleLargeEmphasized, - titleMedium = titleMedium, - titleMediumEmphasized = titleMediumEmphasized, - titleSmall = titleSmall, - titleSmallEmphasized = titleSmallEmphasized, - bodyLarge = bodyLarge, - bodyLargeEmphasized = bodyLargeEmphasized, - bodyMedium = bodyMedium, - bodyMediumEmphasized = bodyMediumEmphasized, - bodySmall = bodySmall, - bodySmallEmphasized = bodySmallEmphasized, - labelLarge = labelLarge, - labelLargeEmphasized = labelLargeEmphasized, - labelMedium = labelMedium, - labelMediumEmphasized = labelMediumEmphasized, - labelSmall = labelSmall, - labelSmallEmphasized = labelSmallEmphasized - ) -} diff --git a/app/src/main/java/io/legado/app/ui/theme/HazeStyle.kt b/app/src/main/java/io/legado/app/ui/theme/HazeStyle.kt index 3fa41bc07..8eb331326 100644 --- a/app/src/main/java/io/legado/app/ui/theme/HazeStyle.kt +++ b/app/src/main/java/io/legado/app/ui/theme/HazeStyle.kt @@ -51,6 +51,29 @@ fun Modifier.responsiveHazeEffect( } } +@OptIn(ExperimentalHazeMaterialsApi::class) +@Composable +fun Modifier.responsiveHazeEffectFixedStyle( + state: HazeState +): Modifier { + val enableBlur = ThemeConfig.enableBlur + + if (!enableBlur) return this + + val style = HazeLegado.ultraThinPlus() + + return this.hazeEffect( + state = state, + style = style + ) { + progressive = + HazeProgressive.verticalGradient( + startIntensity = 1f, + endIntensity = 0f + ) + } +} + /** * 仅判断 enableBlur 的简单 HazeEffect */ diff --git a/app/src/main/java/io/legado/app/ui/theme/ImageSeedColorExtractor.kt b/app/src/main/java/io/legado/app/ui/theme/ImageSeedColorExtractor.kt index 483df3f66..7ccb55ce3 100644 --- a/app/src/main/java/io/legado/app/ui/theme/ImageSeedColorExtractor.kt +++ b/app/src/main/java/io/legado/app/ui/theme/ImageSeedColorExtractor.kt @@ -14,14 +14,15 @@ import androidx.core.graphics.scale import coil.ImageLoader import coil.request.ImageRequest import coil.request.SuccessResult +import coil.size.Size import com.materialkolor.quantize.QuantizerCelebi import com.materialkolor.score.Score import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -private const val IMAGE_COLOR_EXTRACT_SIZE_PX = 256 -private const val IMAGE_QUANTIZE_BITMAP_MAX_SIZE = 128 -private const val IMAGE_MAX_QUANTIZE_COLORS = 128 +private const val IMAGE_COLOR_EXTRACT_SIZE_PX = 128 +private const val IMAGE_QUANTIZE_BITMAP_MAX_SIZE = 64 +private const val IMAGE_MAX_QUANTIZE_COLORS = 64 private const val IMAGE_FALLBACK_SEED_COLOR = 0xFF4285F4.toInt() suspend fun ImageLoader.extractSeedColor( @@ -32,6 +33,7 @@ suspend fun ImageLoader.extractSeedColor( val request = ImageRequest.Builder(context) .data(data) .allowHardware(false) + .size(Size(IMAGE_COLOR_EXTRACT_SIZE_PX, IMAGE_COLOR_EXTRACT_SIZE_PX)) .apply(configureRequest) .build() diff --git a/app/src/main/java/io/legado/app/ui/theme/LegadoTheme.kt b/app/src/main/java/io/legado/app/ui/theme/LegadoTheme.kt index d44361a86..9d6cba526 100644 --- a/app/src/main/java/io/legado/app/ui/theme/LegadoTheme.kt +++ b/app/src/main/java/io/legado/app/ui/theme/LegadoTheme.kt @@ -73,7 +73,8 @@ data class LegadoColorScheme( val onTertiaryFixedVariant: Color, val cardContainer: Color, - val onCardContainer: Color + val onCardContainer: Color, + val onSheetContent: Color ) data class LegadoTypography( diff --git a/app/src/main/java/io/legado/app/ui/theme/ThemeColorSchemeOverride.kt b/app/src/main/java/io/legado/app/ui/theme/ThemeColorSchemeOverride.kt index 9d67a8e12..eb569123f 100644 --- a/app/src/main/java/io/legado/app/ui/theme/ThemeColorSchemeOverride.kt +++ b/app/src/main/java/io/legado/app/ui/theme/ThemeColorSchemeOverride.kt @@ -68,7 +68,8 @@ fun ColorScheme.toLegadoColorScheme(): LegadoColorScheme { onTertiaryFixed = onTertiaryFixed, onTertiaryFixedVariant = onTertiaryFixedVariant, cardContainer = primaryContainer.copy(alpha = 0.5f), - onCardContainer = primary + onCardContainer = primary, + onSheetContent = surface ) } diff --git a/app/src/main/java/io/legado/app/ui/theme/ThemeOverride.kt b/app/src/main/java/io/legado/app/ui/theme/ThemeOverride.kt index dedfcab4e..24bf1c060 100644 --- a/app/src/main/java/io/legado/app/ui/theme/ThemeOverride.kt +++ b/app/src/main/java/io/legado/app/ui/theme/ThemeOverride.kt @@ -55,6 +55,7 @@ fun ProvideThemeOverride( content: @Composable () -> Unit, ) { var appliedTheme by remember { mutableStateOf(null) } + val baseTheme = LocalLegadoThemeColors.current LaunchedEffect(theme) { if (theme == null) { @@ -66,15 +67,16 @@ fun ProvideThemeOverride( } val currentTheme = appliedTheme - if (currentTheme == null) { - content() - } else { - ProvideColorSchemeOverride( - colorScheme = currentTheme.colorScheme, - seedColor = currentTheme.seedColor, - content = content + ?: ThemeOverrideState( + seedColor = baseTheme.seedColor, + colorScheme = baseTheme.colorScheme ) - } + + ProvideColorSchemeOverride( + colorScheme = currentTheme.colorScheme, + seedColor = currentTheme.seedColor, + content = content + ) } @Composable diff --git a/app/src/main/java/io/legado/app/ui/theme/Typography.kt b/app/src/main/java/io/legado/app/ui/theme/Typography.kt index 5896878d5..327d22bd3 100644 --- a/app/src/main/java/io/legado/app/ui/theme/Typography.kt +++ b/app/src/main/java/io/legado/app/ui/theme/Typography.kt @@ -49,3 +49,33 @@ fun miuixStylesToM3Typography(miuixStyles: TextStyles): Typography { labelSmallEmphasized = miuixStyles.footnote2.copy(fontWeight = FontWeight.Medium) ) } + +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +fun Typography.toLegadoTypography(): LegadoTypography { + return LegadoTypography( + headlineLarge = headlineLarge, + headlineLargeEmphasized = headlineLargeEmphasized, + headlineMedium = headlineMedium, + headlineMediumEmphasized = headlineMediumEmphasized, + headlineSmall = headlineSmall, + headlineSmallEmphasized = headlineSmallEmphasized, + titleLarge = titleLarge, + titleLargeEmphasized = titleLargeEmphasized, + titleMedium = titleMedium, + titleMediumEmphasized = titleMediumEmphasized, + titleSmall = titleSmall, + titleSmallEmphasized = titleSmallEmphasized, + bodyLarge = bodyLarge, + bodyLargeEmphasized = bodyLargeEmphasized, + bodyMedium = bodyMedium, + bodyMediumEmphasized = bodyMediumEmphasized, + bodySmall = bodySmall, + bodySmallEmphasized = bodySmallEmphasized, + labelLarge = labelLarge, + labelLargeEmphasized = labelLargeEmphasized, + labelMedium = labelMedium, + labelMediumEmphasized = labelMediumEmphasized, + labelSmall = labelSmall, + labelSmallEmphasized = labelSmallEmphasized + ) +} diff --git a/app/src/main/java/io/legado/app/ui/theme/hazeStyle/HazeLegado.kt b/app/src/main/java/io/legado/app/ui/theme/hazeStyle/HazeLegado.kt index 20b5acb54..3fbfd6a15 100644 --- a/app/src/main/java/io/legado/app/ui/theme/hazeStyle/HazeLegado.kt +++ b/app/src/main/java/io/legado/app/ui/theme/hazeStyle/HazeLegado.kt @@ -14,6 +14,16 @@ import top.yukonga.miuix.kmp.theme.MiuixTheme object HazeLegado { + @Composable + @ReadOnlyComposable + fun ultraThinPlus( + containerColor: Color = if (ThemeResolver.isMiuixEngine(composeEngine)) MiuixTheme.colorScheme.surface else MaterialTheme.colorScheme.surface, + ): HazeStyle = hazeLegado( + containerColor = containerColor, + lightAlpha = 0.0f, + darkAlpha = 0.0f, + ) + @Composable @ReadOnlyComposable fun ultraThin( diff --git a/app/src/main/java/io/legado/app/ui/widget/components/AppLinearProgressIndicator.kt b/app/src/main/java/io/legado/app/ui/widget/components/AppLinearProgressIndicator.kt new file mode 100644 index 000000000..c80f0412b --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/components/AppLinearProgressIndicator.kt @@ -0,0 +1,34 @@ +package io.legado.app.ui.widget.components + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import io.legado.app.ui.theme.LegadoTheme.composeEngine +import io.legado.app.ui.theme.ThemeResolver +// 这里建议使用 as 关键字为两个库的控件起别名,避免同名冲突 +import androidx.compose.material3.LinearProgressIndicator +// 请替换为实际的 MIUIX 控件包名 +import top.yukonga.miuix.kmp.basic.LinearProgressIndicator as MiuixLinearProgressIndicator + +@Composable +fun AppLinearProgressIndicator( + modifier: Modifier = Modifier, + progress: Float? = null, +) { + if (ThemeResolver.isMiuixEngine(composeEngine)) { + MiuixLinearProgressIndicator( + modifier = modifier, + progress = progress + ) + } else { + if (progress != null) { + LinearProgressIndicator( + progress = { progress }, + modifier = modifier + ) + } else { + LinearProgressIndicator( + modifier = modifier + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/components/AppSearchBar.kt b/app/src/main/java/io/legado/app/ui/widget/components/AppSearchBar.kt new file mode 100644 index 000000000..0fadd028a --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/components/AppSearchBar.kt @@ -0,0 +1,168 @@ +package io.legado.app.ui.widget.components + +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.input.rememberTextFieldState +import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd +import androidx.compose.material3.ExpandedFullScreenContainedSearchBar +import androidx.compose.material3.ExpandedFullScreenSearchBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.SearchBar +import androidx.compose.material3.SearchBarDefaults +import androidx.compose.material3.SearchBarValue +import androidx.compose.material3.SearchBarValue.Collapsed +import androidx.compose.material3.SearchBarValue.Expanded +import androidx.compose.material3.rememberSearchBarState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import io.legado.app.ui.theme.LegadoTheme +import io.legado.app.ui.theme.ThemeResolver +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.launch +import top.yukonga.miuix.kmp.basic.InputField as MiuixInputField +import top.yukonga.miuix.kmp.basic.SearchBar as MiuixSearchBar + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) +@Composable +fun AppSearchBar( + modifier: Modifier = Modifier, + query: String, + onQueryChange: (String) -> Unit, + onSearch: (String) -> Unit, + expanded: Boolean, + onExpandedChange: (Boolean) -> Unit, + label: String, + placeholder: @Composable (() -> Unit)? = null, + leadingIcon: @Composable (() -> Unit)? = null, + trailingIcon: @Composable (() -> Unit)? = null, + content: @Composable ColumnScope.() -> Unit, +) { + val isMiuix = ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine) + if (isMiuix) { + var internalExpanded by remember { mutableStateOf(expanded) } + var internalQuery by remember { mutableStateOf(query) } + + LaunchedEffect(expanded) { + internalExpanded = expanded + } + + LaunchedEffect(query) { + if (query != internalQuery) { + internalQuery = query + } + } + + MiuixSearchBar( + modifier = modifier, + inputField = { + MiuixInputField( + query = internalQuery, + onQueryChange = { newQuery -> + if (newQuery.isNotEmpty() || internalExpanded) { + internalQuery = newQuery + onQueryChange(newQuery) + } + }, + onSearch = { + onSearch(it) + onExpandedChange(false) + }, + expanded = internalExpanded, + onExpandedChange = { newExpanded -> + internalExpanded = newExpanded + onExpandedChange(newExpanded) + }, + label = label, + leadingIcon = leadingIcon, + trailingIcon = trailingIcon, + ) + }, + expanded = internalExpanded, + onExpandedChange = onExpandedChange, + content = content, + ) + return + } + + val initialSearchBarValue = remember { + if (expanded) SearchBarValue.Expanded else SearchBarValue.Collapsed + } + val searchBarState = rememberSearchBarState(initialValue = initialSearchBarValue) + val textFieldState = rememberTextFieldState(initialText = query) + val scope = rememberCoroutineScope() + val latestQuery by rememberUpdatedState(query) + val latestExpanded by rememberUpdatedState(expanded) + val latestOnQueryChange by rememberUpdatedState(onQueryChange) + val latestOnExpandedChange by rememberUpdatedState(onExpandedChange) + val latestOnSearch by rememberUpdatedState(onSearch) + + LaunchedEffect(query) { + if (query != textFieldState.text.toString()) { + textFieldState.setTextAndPlaceCursorAtEnd(query) + } + } + + LaunchedEffect(textFieldState) { + snapshotFlow { textFieldState.text.toString() } + .distinctUntilChanged() + .collect { newQuery -> + if (newQuery != latestQuery) { + latestOnQueryChange(newQuery) + } + } + } + + LaunchedEffect(expanded) { + if (expanded && searchBarState.targetValue != Expanded) { + searchBarState.animateToExpanded() + } else if (!expanded && searchBarState.targetValue != Collapsed) { + searchBarState.animateToCollapsed() + } + } + + LaunchedEffect(searchBarState) { + snapshotFlow { searchBarState.currentValue == Expanded } + .distinctUntilChanged() + .collect { isExpanded -> + if (isExpanded != latestExpanded) { + latestOnExpandedChange(isExpanded) + } + } + } + + val inputField: @Composable () -> Unit = { + SearchBarDefaults.InputField( + textFieldState = textFieldState, + searchBarState = searchBarState, + onSearch = { keyword -> + latestOnSearch(keyword) + scope.launch { searchBarState.animateToCollapsed() } + }, + placeholder = placeholder, + leadingIcon = leadingIcon, + trailingIcon = trailingIcon, + ) + } + + SearchBar( + modifier = modifier.padding(horizontal = 16.dp), + state = searchBarState, + inputField = inputField + ) + + ExpandedFullScreenSearchBar( + state = searchBarState, + inputField = inputField, + content = content + ) +} diff --git a/app/src/main/java/io/legado/app/ui/widget/components/AppTextField.kt b/app/src/main/java/io/legado/app/ui/widget/components/AppTextField.kt index 92e5104b4..c0e0483fc 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/AppTextField.kt +++ b/app/src/main/java/io/legado/app/ui/widget/components/AppTextField.kt @@ -28,6 +28,7 @@ import io.legado.app.ui.theme.LegadoTheme import io.legado.app.ui.theme.ThemeResolver import io.legado.app.ui.widget.components.text.AppText import top.yukonga.miuix.kmp.theme.MiuixTheme +import top.yukonga.miuix.kmp.basic.InputField as MiuixSearchBarInputField import top.yukonga.miuix.kmp.basic.TextField as MiuixTextField @OptIn(ExperimentalMaterial3Api::class) @@ -300,13 +301,48 @@ fun AppDenseTextField( scrollState: ScrollState = rememberScrollState(), shape: Shape = TextFieldDefaults.shape, interactionSource: MutableInteractionSource? = null, + miuixUseSearchBarInputField: Boolean = false, + miuixSearchBarLabel: String = label ?: "", + miuixOnSearch: (String) -> Unit = {}, ) { + val isMiuix = ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine) + if (isMiuix && miuixUseSearchBarInputField) { + MiuixSearchBarInputField( + query = state.text.toString(), + onQueryChange = { newQuery -> + val current = state.text.toString() + if (newQuery != current) { + state.edit { + replace(0, length, newQuery) + } + } + }, + onSearch = miuixOnSearch, + expanded = false, + onExpandedChange = {}, + modifier = modifier.heightIn(min = 45.dp), + label = miuixSearchBarLabel, + enabled = enabled, + leadingIcon = leadingIcon, + trailingIcon = trailingIcon, + interactionSource = interactionSource + ) + return + } + + val denseMinHeight = if (isMiuix) 45.dp else 48.dp + val denseBackgroundColor = if (isMiuix && backgroundColor == Color.Unspecified) { + MiuixTheme.colorScheme.surfaceContainerHigh + } else { + backgroundColor + } + AppTextField( state = state, - modifier = modifier.heightIn(min = 48.dp), + modifier = modifier.heightIn(min = denseMinHeight), enabled = enabled, readOnly = readOnly, - backgroundColor = backgroundColor, + backgroundColor = denseBackgroundColor, label = label, labelPosition = labelPosition, placeholder = placeholder, diff --git a/app/src/main/java/io/legado/app/ui/widget/components/EmptyMessageView.kt b/app/src/main/java/io/legado/app/ui/widget/components/EmptyMessage.kt similarity index 98% rename from app/src/main/java/io/legado/app/ui/widget/components/EmptyMessageView.kt rename to app/src/main/java/io/legado/app/ui/widget/components/EmptyMessage.kt index 2659e0ee2..a5046502e 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/EmptyMessageView.kt +++ b/app/src/main/java/io/legado/app/ui/widget/components/EmptyMessage.kt @@ -28,7 +28,7 @@ import io.legado.app.ui.widget.components.text.AnimatedTextLine @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable -fun EmptyMessageView( +fun EmptyMessage( message: String, modifier: Modifier = Modifier, isLoading: Boolean = false, @@ -81,7 +81,7 @@ fun EmptyMessageView( } @Composable -fun EmptyMessageView( +fun EmptyMessage( @StringRes messageResId: Int, modifier: Modifier = Modifier, isLoading: Boolean = false, @@ -93,7 +93,7 @@ fun EmptyMessageView( onFaceClick: (() -> Unit)? = null ) { val message = stringResource(id = messageResId) - EmptyMessageView( + EmptyMessage( message = message, modifier = modifier, isLoading = isLoading, diff --git a/app/src/main/java/io/legado/app/ui/widget/components/SearchBarSection.kt b/app/src/main/java/io/legado/app/ui/widget/components/SearchBarSection.kt index fa437c5bf..118e3c33d 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/SearchBarSection.kt +++ b/app/src/main/java/io/legado/app/ui/widget/components/SearchBarSection.kt @@ -9,14 +9,9 @@ import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface -import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -49,7 +44,6 @@ fun SearchBarSection( trailingIcon: @Composable (() -> Unit)? = null, dropdownMenu: (@Composable (onDismiss: () -> Unit) -> Unit)? = null ) { - var showMenu by remember { mutableStateOf(false) } val textFieldState = rememberTextFieldState(initialText = query) LaunchedEffect(query) { @@ -76,30 +70,20 @@ fun SearchBarSection( if (isMiuix) MiuixTheme.colorScheme.surfaceContainer else MaterialTheme.colorScheme.surfaceContainerLow } - val modifier = modifier - .fillMaxWidth() - .then( - if (isMiuix) modifier.padding(vertical = 4.dp) - else modifier - ) - - val searchTextField = @Composable { + if (isMiuix) { AppDenseTextField( state = textFieldState, - modifier = modifier, + modifier = modifier + .fillMaxWidth() + .padding(vertical = 4.dp), placeholder = { AppText(placeholder) }, leadingIcon = leadingIcon, + trailingIcon = trailingIcon, lineLimits = TextFieldLineLimits.SingleLine, - backgroundColor = if (!isMiuix) { - Color.Transparent - } else { - resolvedBackgroundColor - } + backgroundColor = resolvedBackgroundColor, + miuixUseSearchBarInputField = true, + miuixSearchBarLabel = placeholder ) - } - - if (isMiuix) { - searchTextField() } else { Surface( modifier = Modifier @@ -108,7 +92,15 @@ fun SearchBarSection( shape = RoundedCornerShape(32.dp), color = resolvedBackgroundColor ) { - searchTextField() + AppDenseTextField( + state = textFieldState, + modifier = modifier.fillMaxWidth(), + placeholder = { AppText(placeholder) }, + leadingIcon = leadingIcon, + trailingIcon = trailingIcon, + lineLimits = TextFieldLineLimits.SingleLine, + backgroundColor = Color.Transparent + ) } } } diff --git a/app/src/main/java/io/legado/app/ui/widget/components/alert/AppAlertDialog.kt b/app/src/main/java/io/legado/app/ui/widget/components/alert/AppAlertDialog.kt index d62311f37..7375204ac 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/alert/AppAlertDialog.kt +++ b/app/src/main/java/io/legado/app/ui/widget/components/alert/AppAlertDialog.kt @@ -154,6 +154,7 @@ fun AppAlertDialog( data: T?, onDismissRequest: () -> Unit, title: String? = null, + text: String? = null, confirmText: String = "确定", onConfirm: ((T) -> Unit)? = null, dismissText: String = "取消", @@ -173,6 +174,7 @@ fun AppAlertDialog( show = data != null, onDismissRequest = onDismissRequest, title = title, + text = text, modifier = modifier, confirmText = confirmText, onConfirm = onConfirm?.let { { it(currentData) } }, diff --git a/app/src/main/java/io/legado/app/ui/widget/components/button/SmallIconButton.kt b/app/src/main/java/io/legado/app/ui/widget/components/button/SmallIconButton.kt index 166a92cb0..79cc7118c 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/button/SmallIconButton.kt +++ b/app/src/main/java/io/legado/app/ui/widget/components/button/SmallIconButton.kt @@ -73,7 +73,8 @@ fun SmallIconButton( ) { MiuixIcon( imageVector = imageVector, - contentDescription = contentDescription + contentDescription = contentDescription, + modifier = Modifier.size(smallIconSize), ) } } else { diff --git a/app/src/main/java/io/legado/app/ui/widget/components/button/SmallTextButton.kt b/app/src/main/java/io/legado/app/ui/widget/components/button/SmallTextButton.kt index 918f8f34c..19d9399ef 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/button/SmallTextButton.kt +++ b/app/src/main/java/io/legado/app/ui/widget/components/button/SmallTextButton.kt @@ -1,13 +1,19 @@ package io.legado.app.ui.widget.components.button +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.unit.dp @@ -15,9 +21,11 @@ import io.legado.app.ui.theme.LegadoTheme import io.legado.app.ui.theme.LegadoTheme.composeEngine import io.legado.app.ui.theme.ThemeResolver import io.legado.app.ui.widget.components.text.AppText +import top.yukonga.miuix.kmp.basic.Card +import top.yukonga.miuix.kmp.basic.CardDefaults import top.yukonga.miuix.kmp.basic.Button as MiuixButton import top.yukonga.miuix.kmp.basic.ButtonDefaults as MiuixButtonDefaults -import top.yukonga.miuix.kmp.basic.Text as MiuixText +import top.yukonga.miuix.kmp.basic.Icon as MiuixIcon @Composable fun SmallTextButton( @@ -27,14 +35,31 @@ fun SmallTextButton( onClick: () -> Unit ) { if (ThemeResolver.isMiuixEngine(composeEngine)) { - MiuixButton( + Card( onClick = onClick, modifier = modifier, - colors = MiuixButtonDefaults.buttonColors() - ) { - MiuixText( - text = text + showIndication = true, + colors = CardDefaults.defaultColors( + color = LegadoTheme.colorScheme.surfaceVariant, + contentColor = LegadoTheme.colorScheme.onSurfaceVariant ) + ) { + Row( + modifier = Modifier + .padding(horizontal = 12.dp, vertical = 8.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally), + verticalAlignment = Alignment.CenterVertically + ) { + MiuixIcon( + imageVector = imageVector, + contentDescription = null, + modifier = Modifier.size(16.dp) + ) + AppText( + text = text, + style = LegadoTheme.typography.labelMedium + ) + } } } else { TextButton( diff --git a/app/src/main/java/io/legado/app/ui/widget/components/button/ToggleChip.kt b/app/src/main/java/io/legado/app/ui/widget/components/button/ToggleChip.kt index a4d2dee22..4cfc6443b 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/button/ToggleChip.kt +++ b/app/src/main/java/io/legado/app/ui/widget/components/button/ToggleChip.kt @@ -46,14 +46,15 @@ fun ToggleChip( Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center, - modifier = Modifier.padding(horizontal = 8.dp, vertical = 6.dp) + modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp) ) { AnimatedVisibility(visible = selected) { Row(verticalAlignment = Alignment.CenterVertically) { MiuixIcon( imageVector = Icons.Default.Check, contentDescription = checkedContentDescription, - modifier = Modifier.size(16.dp) + modifier = Modifier.size(18.dp), + tint = if (selected) MiuixTheme.colorScheme.onPrimary else MiuixTheme.colorScheme.onSurface ) Spacer(Modifier.width(6.dp)) } @@ -61,9 +62,10 @@ fun ToggleChip( MiuixText( text = label, - style = LegadoTheme.typography.labelSmall, + style = LegadoTheme.typography.labelMediumEmphasized, maxLines = 1, - softWrap = false + softWrap = false, + color = if (selected) MiuixTheme.colorScheme.onPrimary else MiuixTheme.colorScheme.onSurface ) } } diff --git a/app/src/main/java/io/legado/app/ui/widget/components/card/GlassCard.kt b/app/src/main/java/io/legado/app/ui/widget/components/card/GlassCard.kt index 73288bb7f..69680e3cc 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/card/GlassCard.kt +++ b/app/src/main/java/io/legado/app/ui/widget/components/card/GlassCard.kt @@ -2,6 +2,7 @@ package io.legado.app.ui.widget.components.card import androidx.compose.foundation.BorderStroke 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 @@ -39,15 +40,24 @@ private fun BaseCard( color = (containerColor ?: LegadoTheme.colorScheme.secondaryContainer.copy(alpha)), contentColor = contentColor ?: LegadoTheme.colorScheme.onSurface ) - MiuixCard( - modifier = modifier, - cornerRadius = cornerRadius, - pressFeedbackType = pressFeedbackType, - showIndication = true, - onClick = onClick, - content = content, - colors = colors - ) + if (onClick != null) { + MiuixCard( + modifier = modifier, + cornerRadius = cornerRadius, + pressFeedbackType = pressFeedbackType, + showIndication = true, + onClick = onClick, + content = content, + colors = colors + ) + } else { + MiuixCard( + modifier = modifier, + cornerRadius = cornerRadius, + content = content, + colors = colors + ) + } } else { val colors = CardDefaults.cardColors( containerColor = (containerColor ?: LegadoTheme.colorScheme.secondaryContainer).copy( @@ -60,7 +70,7 @@ private fun BaseCard( if (onClick != null) { Card( modifier = modifier, - shape = shape, + shape = RoundedCornerShape(cornerRadius), colors = colors, elevation = elevation, border = border, @@ -70,7 +80,7 @@ private fun BaseCard( } else { Card( modifier = modifier, - shape = shape, + shape = RoundedCornerShape(cornerRadius), colors = colors, elevation = elevation, border = border, diff --git a/app/src/main/java/io/legado/app/ui/widget/components/card/SelectionItemCard.kt b/app/src/main/java/io/legado/app/ui/widget/components/card/SelectionItemCard.kt index 05a39a824..a1c51184e 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/card/SelectionItemCard.kt +++ b/app/src/main/java/io/legado/app/ui/widget/components/card/SelectionItemCard.kt @@ -44,6 +44,8 @@ import io.legado.app.ui.theme.LegadoTheme.composeEngine import io.legado.app.ui.theme.ThemeResolver import io.legado.app.ui.widget.components.AdaptiveSwitch import io.legado.app.ui.widget.components.button.SmallIconButton +import io.legado.app.ui.widget.components.checkBox.AppCheckbox +import io.legado.app.ui.widget.components.icon.AppIcons import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu import io.legado.app.ui.widget.components.text.AppText import sh.calvin.reorderable.ReorderableItem @@ -70,8 +72,6 @@ fun SelectionItemCard( containerColor: Color? = null, selectedContainerColor: Color? = null ) { - var showMenu by remember { mutableStateOf(false) } - val composeEngine = ThemeResolver.isMiuixEngine(composeEngine) val animatedContainerColor by animateColorAsState( targetValue = if (isSelected) @@ -92,48 +92,104 @@ fun SelectionItemCard( containerColor = animatedContainerColor, elevation = CardDefaults.cardElevation(defaultElevation = elevation) ) { - Row( - modifier = Modifier - .fillMaxWidth() - .animateContentSize(), - verticalAlignment = Alignment.CenterVertically + SelectionItemCardContent( + title = title, + subtitle = subtitle, + supportingContent = supportingContent, + isEnabled = isEnabled, + isSelected = isSelected, + inSelectionMode = inSelectionMode, + leadingContent = leadingContent, + onEnabledChange = onEnabledChange, + onClickEdit = onClickEdit, + trailingAction = trailingAction, + dropdownContent = dropdownContent + ) + } +} + +@Composable +fun SelectionItemCardContent( + title: String, + subtitle: String? = null, + supportingContent: @Composable (() -> Unit)? = null, + isEnabled: Boolean = true, + isSelected: Boolean = false, + inSelectionMode: Boolean = false, + leadingContent: @Composable (() -> Unit)? = null, + onEnabledChange: ((Boolean) -> Unit)? = null, + onClickEdit: (() -> Unit)? = null, + trailingAction: @Composable (RowScope.() -> Unit)? = null, + dropdownContent: @Composable (ColumnScope.(onDismiss: () -> Unit) -> Unit)? = null +) { + var showMenu by remember { mutableStateOf(false) } + + Row( + modifier = Modifier + .fillMaxWidth() + .animateContentSize(), + verticalAlignment = Alignment.CenterVertically + ) { + AnimatedVisibility( + visible = inSelectionMode || leadingContent != null ) { - AnimatedVisibility( - visible = inSelectionMode || leadingContent != null + Box( + modifier = Modifier.padding(start = 12.dp), + contentAlignment = Alignment.Center ) { - Box( - modifier = Modifier.padding(start = 12.dp), - contentAlignment = Alignment.Center - ) { - AnimatedContent( - targetState = inSelectionMode, - label = "LeadingContent" - ) { selectionMode -> - if (selectionMode) { - Checkbox( - checked = isSelected, - onCheckedChange = null - ) - } else { - leadingContent?.invoke() - } + AnimatedContent( + targetState = inSelectionMode, + label = "LeadingContent" + ) { selectionMode -> + if (selectionMode) { + AppCheckbox( + checked = isSelected, + onCheckedChange = null + ) + } else { + leadingContent?.invoke() } } } + } - if (composeEngine) { - BasicComponent( - modifier = Modifier.weight(1f) - ) { + if (ThemeResolver.isMiuixEngine(composeEngine)) { + BasicComponent( + modifier = Modifier.weight(1f) + ) { + AppText( + text = title, + style = LegadoTheme.typography.titleSmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + when { + supportingContent != null -> supportingContent() + !subtitle.isNullOrBlank() -> { + AppText( + text = subtitle, + style = LegadoTheme.typography.bodySmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } + } + } else { + ListItem( + modifier = Modifier.weight(1f), + headlineContent = { AppText( text = title, style = LegadoTheme.typography.titleSmall, maxLines = 1, overflow = TextOverflow.Ellipsis ) - when { - supportingContent != null -> supportingContent() - !subtitle.isNullOrBlank() -> { + }, + supportingContent = when { + supportingContent != null -> supportingContent + !subtitle.isNullOrBlank() -> { + { AppText( text = subtitle, style = LegadoTheme.typography.bodySmall, @@ -142,75 +198,50 @@ fun SelectionItemCard( ) } } - } - } else { - ListItem( - modifier = Modifier.weight(1f), - headlineContent = { - AppText( - text = title, - style = LegadoTheme.typography.titleSmall, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - }, - supportingContent = when { - supportingContent != null -> supportingContent - !subtitle.isNullOrBlank() -> { - { - AppText( - text = subtitle, - style = LegadoTheme.typography.bodySmall, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - } - } - else -> null - }, - colors = ListItemDefaults.colors(containerColor = Color.Transparent) + else -> null + }, + colors = ListItemDefaults.colors(containerColor = Color.Transparent) + ) + } + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier + .padding(end = 8.dp) + ) { + onEnabledChange?.let { + AdaptiveSwitch( + modifier = Modifier.scale(0.8f), + checked = isEnabled, + onCheckedChange = it ) } - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier - .padding(end = 8.dp) - ) { - onEnabledChange?.let { - AdaptiveSwitch( - modifier = Modifier.scale(0.8f), - checked = isEnabled, - onCheckedChange = it - ) - } + if (onClickEdit != null) { + SmallIconButton( + onClick = onClickEdit, + imageVector = AppIcons.Edit, + contentDescription = "Edit" + ) + } - if (onClickEdit != null) { + if (trailingAction != null) { + trailingAction() + } + + if (dropdownContent != null) { + Box { SmallIconButton( - onClick = onClickEdit, - imageVector = Icons.Default.Edit, - contentDescription = "Edit" + onClick = { showMenu = true }, + imageVector = AppIcons.MoreVert, + contentDescription = "More" ) - } - - if (trailingAction != null) { - trailingAction() - } - - if (dropdownContent != null) { - Box { - SmallIconButton( - onClick = { showMenu = true }, - imageVector = Icons.Default.MoreVert, - contentDescription = "More" - ) - RoundDropdownMenu( - expanded = showMenu, - onDismissRequest = { showMenu = false } - ) { - dropdownContent { showMenu = false } - } + RoundDropdownMenu( + expanded = showMenu, + onDismissRequest = { showMenu = false } + ) { + dropdownContent { showMenu = false } } } } diff --git a/app/src/main/java/io/legado/app/ui/widget/components/card/TextCard.kt b/app/src/main/java/io/legado/app/ui/widget/components/card/TextCard.kt index 0ecb22b16..7b44b6c74 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/card/TextCard.kt +++ b/app/src/main/java/io/legado/app/ui/widget/components/card/TextCard.kt @@ -44,8 +44,7 @@ fun TextCard( NormalCard( modifier = modifier, - shape = RoundedCornerShape(cornerRadius), - cornerRadius = 4.dp, + cornerRadius = cornerRadius, containerColor = finalBackgroundColor, contentColor = finalContentColor, onClick = onClick diff --git a/app/src/main/java/io/legado/app/ui/widget/components/checkBox/CheckboxItem.kt b/app/src/main/java/io/legado/app/ui/widget/components/checkBox/CheckboxItem.kt index 642d944b8..5057785e0 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/checkBox/CheckboxItem.kt +++ b/app/src/main/java/io/legado/app/ui/widget/components/checkBox/CheckboxItem.kt @@ -14,44 +14,45 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import io.legado.app.ui.theme.LegadoTheme +import io.legado.app.ui.widget.components.card.GlassCard import io.legado.app.ui.widget.components.text.AppText @Composable fun CheckboxItem( title: String, - color: Color = Color.Transparent, + color: Color = LegadoTheme.colorScheme.onSheetContent, checked: Boolean, enabled: Boolean = true, onCheckedChange: (Boolean) -> Unit ) { val alpha = if (enabled) 1f else 0.5f - Row( - modifier = Modifier - .fillMaxWidth() - .clip(MaterialTheme.shapes.small) - .background( - color = if (checked && enabled) LegadoTheme.colorScheme.secondaryContainer - else color, - shape = MaterialTheme.shapes.small - ) - .clickable(enabled = enabled) { onCheckedChange(!checked) } - .padding(vertical = 12.dp, horizontal = 12.dp), - verticalAlignment = Alignment.CenterVertically + GlassCard( + cornerRadius = 12.dp, + containerColor = if (checked && enabled) LegadoTheme.colorScheme.secondaryContainer else color, ) { - AppCheckbox( - checked = checked, - onCheckedChange = null, - enabled = enabled, - modifier = Modifier.alpha(alpha) - ) - AppText( - text = title, - style = LegadoTheme.typography.bodyMediumEmphasized, - maxLines = 1, + Row( modifier = Modifier - .padding(start = 12.dp) - .alpha(alpha) - ) + .fillMaxWidth() + .clickable(enabled = enabled) { onCheckedChange(!checked) } + .padding(vertical = 12.dp, horizontal = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + AppCheckbox( + checked = checked, + onCheckedChange = null, + enabled = enabled, + modifier = Modifier.alpha(alpha) + ) + AppText( + text = title, + style = LegadoTheme.typography.bodyMediumEmphasized, + maxLines = 1, + modifier = Modifier + .padding(start = 12.dp) + .alpha(alpha) + ) + } } + } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/components/cover/BookCover.kt b/app/src/main/java/io/legado/app/ui/widget/components/cover/BookCover.kt index ccbf2e627..2baac8ff1 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/cover/BookCover.kt +++ b/app/src/main/java/io/legado/app/ui/widget/components/cover/BookCover.kt @@ -39,9 +39,10 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.dp import androidx.core.graphics.withSave import coil.compose.AsyncImage -import coil.request.ImageRequest import io.legado.app.ui.config.coverConfig.CoverConfig import io.legado.app.ui.theme.LegadoTheme +import io.legado.app.ui.widget.components.card.GlassCard +import io.legado.app.ui.widget.components.card.NormalCard import org.koin.compose.koinInject import io.legado.app.model.BookCover as BookCoverModel @@ -81,86 +82,93 @@ fun BookCover( isFinalPathLoaded = false } - BoxWithConstraints( - modifier = modifier - .aspectRatio(5f / 7f) - .then( - if (CoverConfig.coverShowShadow) { - Modifier.shadow(4.dp, RoundedCornerShape(4.dp)) - } else Modifier - ) - .clip(RoundedCornerShape(4.dp)) - .then( - if (!hasCustomDefault && !isOnlineCoverLoaded) { - Modifier.background(LegadoTheme.colorScheme.surfaceContainer) - } else Modifier - ) - ) { - val density = LocalDensity.current - val iconSizeDp = with(density) { - (minOf(maxWidth, maxHeight).toPx() / 3f).toDp() - } + NormalCard( + cornerRadius = 4.dp + ){ + BoxWithConstraints( + modifier = modifier + .aspectRatio(5f / 7f) + .then( + if (CoverConfig.coverShowShadow) { + Modifier.shadow(4.dp, RoundedCornerShape(4.dp)) + } else Modifier + ) + .then( + if (!hasCustomDefault && !isOnlineCoverLoaded) { + Modifier.background(LegadoTheme.colorScheme.surfaceContainer) + } else Modifier + ) + ) { + val density = LocalDensity.current + val iconSizeDp = with(density) { + (minOf(maxWidth, maxHeight).toPx() / 3f).toDp() + } - if (hasCustomDefault && !isFinalPathLoaded) { - AsyncImage( - model = ImageRequest.Builder(context) - .data(randomPath) - .crossfade(true) - .build(), - contentDescription = null, - imageLoader = koinInject(), - contentScale = ContentScale.Crop, - modifier = Modifier.fillMaxSize() - ) - } + if (hasCustomDefault && !isFinalPathLoaded) { + AsyncImage( + model = buildCoverImageRequest( + context = context, + data = randomPath, + sourceOrigin = null, + loadOnlyWifi = false, + crossfade = true, + ), + contentDescription = null, + imageLoader = koinInject(), + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize() + ) + } - if (finalPath != null) { - AsyncImage( - model = ImageRequest.Builder(context) - .data(finalPath) - .crossfade(true) - .setParameter("sourceOrigin", sourceOrigin) - .setParameter("loadOnlyWifi", CoverConfig.loadCoverOnlyWifi) - .build(), - contentDescription = null, - imageLoader = koinInject(), - contentScale = ContentScale.Crop, - modifier = Modifier.fillMaxSize(), - onSuccess = { - isOnlineCoverLoaded = true - isFinalPathLoaded = true - onLoadFinish?.invoke() - }, - onError = { - isOnlineCoverLoaded = false + if (finalPath != null) { + AsyncImage( + model = buildCoverImageRequest( + context = context, + data = finalPath, + sourceOrigin = sourceOrigin, + loadOnlyWifi = CoverConfig.loadCoverOnlyWifi, + crossfade = true, + ), + contentDescription = null, + imageLoader = koinInject(), + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize(), + onSuccess = { + isOnlineCoverLoaded = true + isFinalPathLoaded = true + onLoadFinish?.invoke() + }, + onError = { + isOnlineCoverLoaded = false + onLoadFinish?.invoke() + } + ) + } else { + LaunchedEffect(Unit) { onLoadFinish?.invoke() } - ) - } else { - LaunchedEffect(Unit) { - onLoadFinish?.invoke() } - } - if (!hasCustomDefault && !isOnlineCoverLoaded) { - Icon( - Icons.Default.Book, - contentDescription = null, - tint = LegadoTheme.colorScheme.secondary, - modifier = Modifier - .size(iconSizeDp) - .align(Alignment.Center) - ) - } + if (!hasCustomDefault && !isOnlineCoverLoaded) { + Icon( + Icons.Default.Book, + contentDescription = null, + tint = LegadoTheme.colorScheme.secondary, + modifier = Modifier + .size(iconSizeDp) + .align(Alignment.Center) + ) + } - if (!isOnlineCoverLoaded) { - CoverTextOverlay( - name = name, - author = author, - isNight = isNight - ) - } + if (!isOnlineCoverLoaded) { + CoverTextOverlay( + name = name, + author = author, + isNight = isNight + ) + } + } } } diff --git a/app/src/main/java/io/legado/app/ui/widget/components/cover/BookshelfCover.kt b/app/src/main/java/io/legado/app/ui/widget/components/cover/BookshelfCover.kt index 142e4d834..7506794a6 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/cover/BookshelfCover.kt +++ b/app/src/main/java/io/legado/app/ui/widget/components/cover/BookshelfCover.kt @@ -13,6 +13,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.unit.dp import io.legado.app.ui.theme.LegadoTheme +import io.legado.app.ui.widget.components.AppLinearProgressIndicator import io.legado.app.ui.widget.components.card.TextCard @Composable @@ -42,6 +43,7 @@ fun BookshelfCover( TextCard( text = badgeText, icon = if (showBadgeDot) Icons.Default.Update else null, + iconSize = 12.dp, backgroundColor = LegadoTheme.colorScheme.cardContainer, contentColor = LegadoTheme.colorScheme.onCardContainer, modifier = Modifier @@ -49,7 +51,7 @@ fun BookshelfCover( .padding(2.dp), cornerRadius = 4.dp, horizontalPadding = 4.dp, - verticalPadding = 0.dp + verticalPadding = 2.dp ) } @@ -63,20 +65,17 @@ fun BookshelfCover( .padding(2.dp), cornerRadius = 4.dp, horizontalPadding = 4.dp, - verticalPadding = 0.dp + verticalPadding = 2.dp ) } if (isUpdating) { - LinearProgressIndicator( + AppLinearProgressIndicator( modifier = Modifier .align(Alignment.BottomCenter) .fillMaxWidth() .padding(horizontal = 4.dp, vertical = 6.dp) - .height(3.dp), - color = LegadoTheme.colorScheme.primary, - trackColor = LegadoTheme.colorScheme.primary.copy(alpha = 0.2f), - strokeCap = StrokeCap.Round + .height(3.dp) ) } } diff --git a/app/src/main/java/io/legado/app/ui/widget/components/cover/Cover.kt b/app/src/main/java/io/legado/app/ui/widget/components/cover/Cover.kt index 91c683d6b..02bddd7b0 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/cover/Cover.kt +++ b/app/src/main/java/io/legado/app/ui/widget/components/cover/Cover.kt @@ -24,7 +24,6 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import coil.ImageLoader import coil.compose.AsyncImage -import coil.request.ImageRequest import io.legado.app.ui.theme.LegadoTheme import org.koin.compose.koinInject @@ -57,16 +56,18 @@ fun Cover( ) } else { AsyncImage( - model = ImageRequest.Builder(context) - .data(path) - .crossfade(true) - .setParameter("sourceOrigin", sourceOrigin) - .setParameter("loadOnlyWifi", loadOnlyWifi) - .listener( + model = buildCoverImageRequest( + context = context, + data = path, + sourceOrigin = sourceOrigin, + loadOnlyWifi = loadOnlyWifi, + crossfade = true, + ) { + listener( onSuccess = { _, _ -> onLoadFinish?.invoke() }, onError = { _, _ -> onLoadFinish?.invoke() } ) - .build(), + }, imageLoader = imageLoader, contentDescription = null, contentScale = ContentScale.Crop, @@ -93,4 +94,4 @@ fun Cover( } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/io/legado/app/ui/widget/components/cover/CoverImageRequest.kt b/app/src/main/java/io/legado/app/ui/widget/components/cover/CoverImageRequest.kt new file mode 100644 index 000000000..4bd201ab9 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/components/cover/CoverImageRequest.kt @@ -0,0 +1,21 @@ +package io.legado.app.ui.widget.components.cover + +import android.content.Context +import coil.request.ImageRequest + +fun buildCoverImageRequest( + context: Context, + data: Any?, + sourceOrigin: String?, + loadOnlyWifi: Boolean, + crossfade: Boolean = true, + configure: ImageRequest.Builder.() -> Unit = {}, +): ImageRequest { + return ImageRequest.Builder(context) + .data(data) + .crossfade(crossfade) + .setParameter("sourceOrigin", sourceOrigin) + .setParameter("loadOnlyWifi", loadOnlyWifi) + .apply(configure) + .build() +} diff --git a/app/src/main/java/io/legado/app/ui/widget/components/icon/AppIcons.kt b/app/src/main/java/io/legado/app/ui/widget/components/icon/AppIcons.kt index 699410b35..cc61c75f5 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/icon/AppIcons.kt +++ b/app/src/main/java/io/legado/app/ui/widget/components/icon/AppIcons.kt @@ -5,11 +5,19 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.LibraryBooks import androidx.compose.material.icons.automirrored.outlined.LibraryBooks import androidx.compose.material.icons.filled.Clear +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.Explore +import androidx.compose.material.icons.filled.FilterList +import androidx.compose.material.icons.filled.History +import androidx.compose.material.icons.filled.LocationSearching +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material.icons.filled.MyLocation import androidx.compose.material.icons.filled.Person import androidx.compose.material.icons.filled.Replay import androidx.compose.material.icons.filled.RssFeed import androidx.compose.material.icons.filled.Search +import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.outlined.Explore import androidx.compose.material.icons.outlined.Person import androidx.compose.material.icons.outlined.RssFeed @@ -18,15 +26,23 @@ import androidx.compose.ui.graphics.vector.ImageVector import io.legado.app.ui.main.MainDestination import io.legado.app.ui.theme.LegadoTheme import io.legado.app.ui.theme.ThemeResolver +import okhttp3.internal.http2.Settings import top.yukonga.miuix.kmp.icon.MiuixIcons import top.yukonga.miuix.kmp.icon.basic.Search import top.yukonga.miuix.kmp.icon.extended.Album import top.yukonga.miuix.kmp.icon.extended.Back import top.yukonga.miuix.kmp.icon.extended.Close import top.yukonga.miuix.kmp.icon.extended.ContactsBook +import top.yukonga.miuix.kmp.icon.extended.Delete +import top.yukonga.miuix.kmp.icon.extended.Edit import top.yukonga.miuix.kmp.icon.extended.Favorites +import top.yukonga.miuix.kmp.icon.extended.Filter +import top.yukonga.miuix.kmp.icon.extended.More +import top.yukonga.miuix.kmp.icon.extended.Pin import top.yukonga.miuix.kmp.icon.extended.Refresh import top.yukonga.miuix.kmp.icon.extended.Settings +import top.yukonga.miuix.kmp.icon.extended.Unpin +import top.yukonga.miuix.kmp.icon.extended.WorldClock object AppIcons { @@ -38,6 +54,18 @@ object AppIcons { @Composable get() = if (isMiuix) MiuixIcons.Basic.Search else Icons.Default.Search + val MoreVert: ImageVector + @Composable + get() = if (isMiuix) MiuixIcons.More else Icons.Default.MoreVert + + val Edit: ImageVector + @Composable + get() = if (isMiuix) MiuixIcons.Edit else Icons.Default.Edit + + val Delete: ImageVector + @Composable + get() = if (isMiuix) MiuixIcons.Delete else Icons.Default.Delete + val Close: ImageVector @Composable get() = if (isMiuix) MiuixIcons.Close else Icons.Default.Clear @@ -46,6 +74,26 @@ object AppIcons { @Composable get() = if (isMiuix) MiuixIcons.Back else Icons.AutoMirrored.Filled.ArrowBack + val Filter: ImageVector + @Composable + get() = if (isMiuix) MiuixIcons.Filter else Icons.Default.FilterList + + val Settings: ImageVector + @Composable + get() = if (isMiuix) MiuixIcons.Settings else Icons.Default.Settings + + val PrecisionSearch: ImageVector + @Composable + get() = if (isMiuix) MiuixIcons.Pin else Icons.Default.MyLocation + + val UnPrecisionSearch: ImageVector + @Composable + get() = if (isMiuix) MiuixIcons.Unpin else Icons.Default.LocationSearching + + val History: ImageVector + @Composable + get() = if (isMiuix) MiuixIcons.WorldClock else Icons.Default.History + val Replay: ImageVector @Composable get() = if (isMiuix) MiuixIcons.Refresh ?: Icons.Default.Replay else Icons.Default.Replay diff --git a/app/src/main/java/io/legado/app/ui/widget/components/menuItem/RoundDropdownMenu.kt b/app/src/main/java/io/legado/app/ui/widget/components/menuItem/RoundDropdownMenu.kt index e78a8c1fe..234234f05 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/menuItem/RoundDropdownMenu.kt +++ b/app/src/main/java/io/legado/app/ui/widget/components/menuItem/RoundDropdownMenu.kt @@ -45,7 +45,7 @@ fun RoundDropdownMenu( if (isMiuix) { val popupContentColor = LegadoTheme.colorScheme.onSurface - OverlayListPopup( + WindowListPopup( show = expanded, onDismissRequest = onDismissRequest, popupModifier = modifier diff --git a/app/src/main/java/io/legado/app/ui/widget/components/menuItem/RoundDropdownMenuItem.kt b/app/src/main/java/io/legado/app/ui/widget/components/menuItem/RoundDropdownMenuItem.kt index 60b0f1b3c..cbe88fa11 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/menuItem/RoundDropdownMenuItem.kt +++ b/app/src/main/java/io/legado/app/ui/widget/components/menuItem/RoundDropdownMenuItem.kt @@ -137,7 +137,7 @@ fun RoundDropdownMenuItem( else -> defaultContentColor.copy(alpha = 0.38f) } } - val containerColor = if (isSelected) colorScheme.primaryContainer else colorScheme.surface + val containerColor = colorScheme.surface Surface( onClick = onClick, diff --git a/app/src/main/java/io/legado/app/ui/widget/components/rules/RuleListScaffold.kt b/app/src/main/java/io/legado/app/ui/widget/components/rules/RuleListScaffold.kt index 75e65bed1..15d69cc23 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/rules/RuleListScaffold.kt +++ b/app/src/main/java/io/legado/app/ui/widget/components/rules/RuleListScaffold.kt @@ -76,7 +76,7 @@ fun RuleListScaffold( show = showDeleteConfirmDialog, onDismissRequest = { showDeleteConfirmDialog = false }, title = stringResource(R.string.delete), - text = stringResource(R.string.del_msg), + text = stringResource(R.string.sure_del), confirmText = stringResource(R.string.ok), onConfirm = { onDeleteSelected(state.selectedIds) diff --git a/app/src/main/java/io/legado/app/ui/widget/components/settingItem/SliderSettingItem.kt b/app/src/main/java/io/legado/app/ui/widget/components/settingItem/SliderSettingItem.kt index f2fb4fce9..66c238c07 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/settingItem/SliderSettingItem.kt +++ b/app/src/main/java/io/legado/app/ui/widget/components/settingItem/SliderSettingItem.kt @@ -2,6 +2,7 @@ import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row @@ -30,6 +31,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import io.legado.app.R +import io.legado.app.ui.theme.LegadoTheme import io.legado.app.ui.theme.LegadoTheme.composeEngine import io.legado.app.ui.theme.ThemeResolver import io.legado.app.ui.widget.components.button.ConfirmDismissButtonsRow @@ -41,7 +43,6 @@ import top.yukonga.miuix.kmp.basic.TextField as MiuixTextField @Composable fun SliderSettingItem( title: String, - color: Color? = null, value: Float, defaultValue: Float, valueRange: ClosedFloatingPointRange, @@ -56,7 +57,10 @@ fun SliderSettingItem( if (ThemeResolver.isMiuixEngine(composeEngine)) { - Column(modifier = Modifier.fillMaxWidth()) { + Column( + modifier = Modifier + .fillMaxWidth() + ) { BasicComponent( title = title, summary = description, @@ -143,7 +147,7 @@ fun SliderSettingItem( } else { SettingItem( title = title, - color = color ?: MaterialTheme.colorScheme.surfaceContainerLow, + color = MaterialTheme.colorScheme.surfaceContainerLow, option = description, expanded = expanded, onExpandChange = { expanded = it }, diff --git a/app/src/main/java/io/legado/app/ui/widget/components/topbar/DynamicTopAppBar.kt b/app/src/main/java/io/legado/app/ui/widget/components/topbar/DynamicTopAppBar.kt index acc86c446..95d482aff 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/topbar/DynamicTopAppBar.kt +++ b/app/src/main/java/io/legado/app/ui/widget/components/topbar/DynamicTopAppBar.kt @@ -75,7 +75,7 @@ fun DynamicTopAppBar( if (!isSelecting) { TopBarActionButton( onClick = { onSearchToggle(!state.isSearch) }, - imageVector = Icons.Default.Search, + imageVector = AppIcons.Search, contentDescription = "搜索" ) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 10cdac732..3d734816b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,7 +17,7 @@ kotlinxSerialization = "1.10.0" kotlinxCoroutinesAndroid = "1.10.2" kotlinxSerializationJson = "1.10.0" ksp = "2.3.6" -agp = "9.1.0" +agp = "9.1.1" appcompat = "1.7.1" colorpicker = "1.1.0" colorpicker-compose = "1.1.3"