diff --git a/app/src/main/java/io/legado/app/constant/PreferKey.kt b/app/src/main/java/io/legado/app/constant/PreferKey.kt index 864c82ac9..2f1ab8941 100644 --- a/app/src/main/java/io/legado/app/constant/PreferKey.kt +++ b/app/src/main/java/io/legado/app/constant/PreferKey.kt @@ -45,6 +45,7 @@ object PreferKey { const val prevKeys = "prevKeyCodes" const val nextKeys = "nextKeyCodes" const val showDiscovery = "showDiscovery" + const val showHome = "showHome" const val enableReview = "enableReview" const val showRss = "showRss" const val showStatusBar = "showStatusBar" diff --git a/app/src/main/java/io/legado/app/data/local/preferences/LocalPreferences.kt b/app/src/main/java/io/legado/app/data/local/preferences/LocalPreferences.kt new file mode 100644 index 000000000..669b335fa --- /dev/null +++ b/app/src/main/java/io/legado/app/data/local/preferences/LocalPreferences.kt @@ -0,0 +1,13 @@ +package io.legado.app.data.local.preferences + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.preferencesDataStore + +val Context.localDataStore: DataStore by preferencesDataStore(name = "local_ui_status") + +object LocalPreferencesKeys { + val SHOW_THEME_REFACTOR_TIP = booleanPreferencesKey("show_theme_refactor_tip") +} diff --git a/app/src/main/java/io/legado/app/data/local/preferences/LocalPreferencesRepository.kt b/app/src/main/java/io/legado/app/data/local/preferences/LocalPreferencesRepository.kt new file mode 100644 index 000000000..6c3641fd3 --- /dev/null +++ b/app/src/main/java/io/legado/app/data/local/preferences/LocalPreferencesRepository.kt @@ -0,0 +1,35 @@ +package io.legado.app.data.local.preferences + +import android.content.Context +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.emptyPreferences +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.map +import java.io.IOException + +class LocalPreferencesRepository(private val context: Context) { + + private val dataStore = context.localDataStore + + fun getPreference(key: Preferences.Key, defaultValue: T): Flow { + return dataStore.data + .catch { exception -> + if (exception is IOException) { + emit(emptyPreferences()) + } else { + throw exception + } + } + .map { preferences -> + preferences[key] ?: defaultValue + } + } + + suspend fun updatePreference(key: Preferences.Key, value: T) { + dataStore.edit { preferences -> + preferences[key] = value + } + } +} 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 8bb21516a..e487f516b 100644 --- a/app/src/main/java/io/legado/app/di/appModule.kt +++ b/app/src/main/java/io/legado/app/di/appModule.kt @@ -6,6 +6,7 @@ import coil.decode.GifDecoder import coil.decode.ImageDecoderDecoder import coil.decode.SvgDecoder import io.legado.app.data.AppDatabase +import io.legado.app.data.local.preferences.LocalPreferencesRepository import io.legado.app.data.repository.AppStartupRepository import io.legado.app.data.repository.BookCacheCleanupRepository import io.legado.app.data.repository.BookDomainRepositoryImpl @@ -137,6 +138,7 @@ val appModule = module { singleOf(::SearchContentRepository) singleOf(::RemoteBookRepository) singleOf(::SettingsRepository) + singleOf(::LocalPreferencesRepository) singleOf(::ExploreBooksUseCase) singleOf(::ExploreKindUiUseCase) singleOf(::SaveSearchBooksUseCase) diff --git a/app/src/main/java/io/legado/app/domain/usecase/ExploreBooksUseCase.kt b/app/src/main/java/io/legado/app/domain/usecase/ExploreBooksUseCase.kt index 427a8775a..d97b9bb3e 100644 --- a/app/src/main/java/io/legado/app/domain/usecase/ExploreBooksUseCase.kt +++ b/app/src/main/java/io/legado/app/domain/usecase/ExploreBooksUseCase.kt @@ -3,6 +3,8 @@ package io.legado.app.domain.usecase import io.legado.app.data.entities.SearchBook import io.legado.app.data.repository.BookSourceRepository import io.legado.app.model.webBook.WebBook +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext class ExploreBooksUseCase( private val bookSourceRepository: BookSourceRepository, @@ -20,7 +22,7 @@ class ExploreBooksUseCase( moduleUrl: String?, args: String?, page: Int = 1 - ): ExploreResult { + ): ExploreResult = withContext(Dispatchers.IO) { val base = bookSourceRepository.getBookSource(sourceUrl) ?: throw SourceNotFound(sourceUrl) val source = args?.let { base.copy().also { s -> s.setVariable(it) } } ?: base @@ -33,14 +35,14 @@ class ExploreBooksUseCase( throw InvalidUrl(resolvedUrl) } val books = WebBook.exploreBookSuspend(source, resolvedUrl, page) - return ExploreResult(resolvedUrl, books) + ExploreResult(resolvedUrl, books) } suspend fun executeForRanking( sourceUrl: String, moduleUrl: String?, args: String? - ): List { + ): List = withContext(Dispatchers.IO) { val result = execute(sourceUrl, moduleUrl, args) var books = result.books var page = 1 @@ -50,7 +52,7 @@ class ExploreBooksUseCase( WebBook.exploreBookSuspend( bookSourceRepository.getBookSource(sourceUrl) ?.let { s -> args?.let { s.copy().also { x -> x.setVariable(it) } } ?: s } - ?: return books.take(MAX_RANKING_BOOKS), + ?: return@withContext books.take(MAX_RANKING_BOOKS), result.resolvedUrl, page, ) @@ -60,7 +62,7 @@ class ExploreBooksUseCase( if (next.isEmpty()) break books = (books + next) } - return books.take(MAX_RANKING_BOOKS) + books.take(MAX_RANKING_BOOKS) } data class ExploreResult(val resolvedUrl: String, val books: List) diff --git a/app/src/main/java/io/legado/app/domain/usecase/ExploreKindUiUseCase.kt b/app/src/main/java/io/legado/app/domain/usecase/ExploreKindUiUseCase.kt index 3d90ca26b..d19da8344 100644 --- a/app/src/main/java/io/legado/app/domain/usecase/ExploreKindUiUseCase.kt +++ b/app/src/main/java/io/legado/app/domain/usecase/ExploreKindUiUseCase.kt @@ -99,9 +99,10 @@ class ExploreKindUiUseCase( } } - private suspend fun evalUiJs(jsStr: String, sourceUrl: String, infoMap: InfoMap): String? { - val source = getOrLoadBookSource(sourceUrl) ?: return null - return runScriptWithContext { + private suspend fun evalUiJs(jsStr: String, sourceUrl: String, infoMap: InfoMap): String? = + withContext(Dispatchers.IO) { + val source = getOrLoadBookSource(sourceUrl) ?: return@withContext null + runScriptWithContext { source.evalJS(jsStr) { put("infoMap", infoMap) }?.toString() diff --git a/app/src/main/java/io/legado/app/help/config/AppConfig.kt b/app/src/main/java/io/legado/app/help/config/AppConfig.kt index 8476d4395..95f1d542c 100644 --- a/app/src/main/java/io/legado/app/help/config/AppConfig.kt +++ b/app/src/main/java/io/legado/app/help/config/AppConfig.kt @@ -306,6 +306,9 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener { val showDiscovery: Boolean get() = appCtx.getPrefBoolean(PreferKey.showDiscovery, true) + val showHome: Boolean + get() = appCtx.getPrefBoolean(PreferKey.showHome, true) + val showRSS: Boolean get() = appCtx.getPrefBoolean(PreferKey.showRss, true) diff --git a/app/src/main/java/io/legado/app/help/config/ThemeImportExport.kt b/app/src/main/java/io/legado/app/help/config/ThemeImportExport.kt index 6473737d3..f0d19980f 100644 --- a/app/src/main/java/io/legado/app/help/config/ThemeImportExport.kt +++ b/app/src/main/java/io/legado/app/help/config/ThemeImportExport.kt @@ -3,12 +3,8 @@ package io.legado.app.help.config import android.content.Context import android.net.Uri import com.google.gson.GsonBuilder -import com.google.gson.JsonObject -import com.google.gson.JsonParser import io.legado.app.ui.config.themeConfig.ThemeConfig import io.legado.app.utils.GSON -import io.legado.app.utils.inputStream -import io.legado.app.utils.outputStream import splitties.init.appCtx import java.io.File @@ -175,6 +171,7 @@ object ThemeImportExport { customTagColorsJson = ThemeConfig.customTagColorsJson, // 主界面设置 + showHome = ThemeConfig.showHome, showDiscovery = ThemeConfig.showDiscovery, showRss = ThemeConfig.showRss, showStatusBar = ThemeConfig.showStatusBar, @@ -258,6 +255,7 @@ object ThemeImportExport { ThemeConfig.customTagColorsJson = data.customTagColorsJson // 主界面设置 + ThemeConfig.showHome = data.showHome ThemeConfig.showDiscovery = data.showDiscovery ThemeConfig.showRss = data.showRss ThemeConfig.showStatusBar = data.showStatusBar @@ -394,6 +392,7 @@ data class ThemeExportData( val customTagColorsJson: String? = null, // 主界面设置 + val showHome: Boolean = true, val showDiscovery: Boolean = true, val showRss: Boolean = true, val showStatusBar: Boolean = true, diff --git a/app/src/main/java/io/legado/app/ui/association/AddToBookshelfDialog.kt b/app/src/main/java/io/legado/app/ui/association/AddToBookshelfDialog.kt index 5496dcd38..10a4f87af 100644 --- a/app/src/main/java/io/legado/app/ui/association/AddToBookshelfDialog.kt +++ b/app/src/main/java/io/legado/app/ui/association/AddToBookshelfDialog.kt @@ -86,7 +86,9 @@ class AddToBookshelfDialog() : BaseDialogFragment(R.layout.dialog_add_to_bookshe context = requireContext(), name = it.name, author = it.author, - bookUrl = it.bookUrl + bookUrl = it.bookUrl, + origin = it.origin, + coverPath = it.coverUrl ) ) dismiss() 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 e0db349ce..79a7357de 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 @@ -1,8 +1,6 @@ package io.legado.app.ui.book.explore import android.annotation.SuppressLint -import androidx.appcompat.app.AppCompatActivity -import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibilityScope import androidx.compose.animation.Crossfade @@ -13,8 +11,6 @@ import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -27,9 +23,9 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridItemSpan import androidx.compose.foundation.lazy.grid.LazyVerticalGrid -import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.lazy.grid.itemsIndexed import androidx.compose.foundation.lazy.grid.rememberLazyGridState -import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.FormatListBulleted @@ -52,9 +48,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import dev.chrisbanes.haze.HazeState @@ -69,13 +63,11 @@ import io.legado.app.ui.theme.responsiveHazeEffect import io.legado.app.ui.theme.responsiveHazeSource import io.legado.app.ui.widget.components.AppPullToRefresh import io.legado.app.ui.widget.components.AppScaffold -import io.legado.app.ui.widget.components.SearchBar +import io.legado.app.ui.widget.components.LoadMoreFooter 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.button.AnimatedTextButton import io.legado.app.ui.widget.components.card.TextCard -import io.legado.app.ui.widget.components.explore.ExploreKindMultiTypeItem -import io.legado.app.ui.widget.components.explore.calculateExploreKindRows +import io.legado.app.ui.widget.components.explore.ExploreKindSelectSheet 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 @@ -98,7 +90,7 @@ fun ExploreShowScreen( sourceUrl: String?, exploreUrl: String?, onBack: () -> Unit, - onBookClick: (SearchBook) -> Unit, + onBookClick: (SearchBook, String?) -> Unit, viewModel: ExploreShowViewModel = koinViewModel(), sharedTransitionScope: SharedTransitionScope? = null, animatedVisibilityScope: AnimatedVisibilityScope? = null, @@ -111,7 +103,6 @@ fun ExploreShowScreen( val books by viewModel.uiBooks.collectAsState() val isBookEnd by viewModel.isEnd.collectAsState() val shouldTriggerAutoLoad by viewModel.shouldTriggerAutoLoad.collectAsState() - val kinds by viewModel.kinds.collectAsState() val isLoading by viewModel.isLoading.collectAsState() val errorMsg by viewModel.errorMsg.collectAsState() val filterState by viewModel.filterState.collectAsState() @@ -125,8 +116,6 @@ fun ExploreShowScreen( var showGridCountSheet by remember { mutableStateOf(false) } val gridColumnCount by viewModel.gridCount.collectAsState() val isMiuix = ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine) - val context = LocalContext.current - val activity = context as? AppCompatActivity val exploreKindUseCase: ExploreKindUiUseCase = koinInject() LaunchedEffect(sourceUrl) { @@ -233,73 +222,16 @@ fun ExploreShowScreen( } - AppModalBottomSheet( + ExploreKindSelectSheet( show = showKindSheet, - onDismissRequest = { showKindSheet = false } - ) { - - var kindQuery by remember { mutableStateOf("") } - - SearchBar( - query = kindQuery, - backgroundColor = LegadoTheme.colorScheme.surface.copy(alpha = 0.5f), - onQueryChange = { kindQuery = it }, - placeholder = "选择或搜索分类", - ) - - val filteredKinds = remember(kindQuery, kinds) { - if (kindQuery.isBlank()) kinds - else kinds.filter { kind -> - kind.title.contains(kindQuery, ignoreCase = true) || - (kind.url?.contains(kindQuery, ignoreCase = true) == true) + onDismissRequest = { showKindSheet = false }, + sourceUrl = sourceUrl, + onSelected = { selectedKinds -> + selectedKinds.firstOrNull()?.let { kind -> + viewModel.switchExploreUrl(kind) } } - val kindRows = remember(filteredKinds) { - calculateExploreKindRows(filteredKinds, 6) - } - - LazyColumn( - contentPadding = PaddingValues(vertical = 16.dp), - modifier = Modifier.weight(1f, fill = false) - ) { - items(kindRows) { rowItems -> - Row( - modifier = Modifier - .fillMaxWidth() - .animateItem() - .padding(vertical = 4.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - rowItems.forEach { (kind, span) -> - ExploreKindMultiTypeItem( - modifier = Modifier - .weight(span.toFloat()) - .animateItem(), - kind = kind, - sourceUrl = sourceUrl, - activity = activity, - onOpenUrl = { url -> - showKindSheet = false - viewModel.switchExploreUrl(kind.copy(url = url)) - }, - onRefreshKinds = viewModel::refreshKinds, - backgroundColor = LegadoTheme.colorScheme.surface.copy(alpha = 0.5f), - isMiuix = isMiuix, - useCase = exploreKindUseCase - ) - } - - val totalSpan = rowItems.sumOf { it.second } - if (totalSpan < 6) { - Spacer( - modifier = Modifier.weight((6 - totalSpan).toFloat()) - ) - } - } - } - } - } - + ) AppScaffold( modifier = Modifier @@ -433,17 +365,22 @@ fun ExploreShowScreen( horizontalArrangement = Arrangement.spacedBy(4.dp), verticalArrangement = Arrangement.spacedBy(8.dp) ) { - items( + itemsIndexed( items = books, - key = { it.book.bookUrl } - ) { item -> + key = { index, item -> "${item.book.bookUrl}:$index" } + ) { index, item -> + val sharedCoverKey = bookCoverSharedElementKey( + item.book.bookUrl, + "explore:grid:$index" + ) ExploreBookGridItem( book = item.book, shelfState = item.shelfState, - onClick = { onBookClick(item.book) }, + onClick = { onBookClick(item.book, sharedCoverKey) }, modifier = Modifier.animateItem(), sharedTransitionScope = sharedTransitionScope, animatedVisibilityScope = animatedVisibilityScope, + sharedCoverKey = sharedCoverKey, ) } @@ -467,17 +404,22 @@ fun ExploreShowScreen( bottom = paddingValues.calculateBottomPadding() + 16.dp ) ) { - items( + itemsIndexed( items = books, - key = { it.book.bookUrl } - ) { item -> + key = { index, item -> "${item.book.bookUrl}:$index" } + ) { index, item -> + val sharedCoverKey = bookCoverSharedElementKey( + item.book.bookUrl, + "explore:list:$index" + ) ExploreBookItem( book = item.book, shelfState = item.shelfState, - onClick = { onBookClick(item.book) }, + onClick = { onBookClick(item.book, sharedCoverKey) }, modifier = Modifier.animateItem(), sharedTransitionScope = sharedTransitionScope, animatedVisibilityScope = animatedVisibilityScope, + sharedCoverKey = sharedCoverKey, ) } @@ -506,6 +448,7 @@ fun ExploreBookItem( modifier: Modifier = Modifier, sharedTransitionScope: SharedTransitionScope? = null, animatedVisibilityScope: AnimatedVisibilityScope? = null, + sharedCoverKey: String? = null, ) { SearchBookListItem( book = book, @@ -514,7 +457,7 @@ fun ExploreBookItem( modifier = modifier, sharedTransitionScope = sharedTransitionScope, animatedVisibilityScope = animatedVisibilityScope, - sharedCoverKey = bookCoverSharedElementKey(book.bookUrl) + sharedCoverKey = sharedCoverKey ) } @@ -527,6 +470,7 @@ fun ExploreBookGridItem( modifier: Modifier = Modifier, sharedTransitionScope: SharedTransitionScope? = null, animatedVisibilityScope: AnimatedVisibilityScope? = null, + sharedCoverKey: String? = null, ) { SearchBookGridItem( book = book, @@ -535,60 +479,7 @@ fun ExploreBookGridItem( modifier = modifier, sharedTransitionScope = sharedTransitionScope, animatedVisibilityScope = animatedVisibilityScope, - sharedCoverKey = bookCoverSharedElementKey(book.bookUrl) + sharedCoverKey = sharedCoverKey ) } -@OptIn(ExperimentalMaterial3ExpressiveApi::class) -@Composable -fun LoadMoreFooter( - isLoading: Boolean, - errorMsg: String?, - isEnd: Boolean, - onRetry: () -> Unit -) { - - LaunchedEffect(isLoading, errorMsg, isEnd) { - if (!isLoading && errorMsg == null && !isEnd) { - onRetry() - } - } - - Box( - modifier = Modifier - .fillMaxWidth() - .padding(32.dp), - contentAlignment = Alignment.Center - ) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - - AnimatedContent( - targetState = when { - isLoading -> "加载中…" - errorMsg != null -> "加载失败: $errorMsg" - isEnd -> "已经到底了~" - else -> "我爱你" - }, - label = "FooterTextChange" - ) { text -> - AppText( - text = text, - color = when { - errorMsg != null -> Color.Red - else -> Color.Gray - }, - style = LegadoTheme.typography.bodySmall - ) - } - - Spacer(modifier = Modifier.height(8.dp)) - - AnimatedTextButton( - isLoading = isLoading, - onClick = onRetry, - text = if (errorMsg != null) "重试" else "再试一次", - modifier = Modifier.padding(top = 4.dp) - ) - } - } -} diff --git a/app/src/main/java/io/legado/app/ui/book/explore/ExploreShowViewModel.kt b/app/src/main/java/io/legado/app/ui/book/explore/ExploreShowViewModel.kt index 431aa69d7..87955d925 100644 --- a/app/src/main/java/io/legado/app/ui/book/explore/ExploreShowViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/explore/ExploreShowViewModel.kt @@ -13,6 +13,7 @@ import io.legado.app.domain.usecase.ResolveBookShelfStateUseCase import io.legado.app.domain.usecase.SaveSearchBooksUseCase import io.legado.app.help.config.AppConfig import io.legado.app.utils.exploreLayoutGrid +import io.legado.app.utils.stackTraceStr import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -222,7 +223,7 @@ class ExploreShowViewModel( } } .onFailure { - _errorMsg.value = it.localizedMessage + _errorMsg.value = it.stackTraceStr } _isLoading.value = false diff --git a/app/src/main/java/io/legado/app/ui/book/info/BookInfoRouteScreen.kt b/app/src/main/java/io/legado/app/ui/book/info/BookInfoRouteScreen.kt index 8cd29b95c..38cf6ec70 100644 --- a/app/src/main/java/io/legado/app/ui/book/info/BookInfoRouteScreen.kt +++ b/app/src/main/java/io/legado/app/ui/book/info/BookInfoRouteScreen.kt @@ -40,6 +40,10 @@ import kotlinx.coroutines.flow.collectLatest @Composable fun BookInfoRouteScreen( bookUrl: String, + name: String? = null, + author: String? = null, + origin: String? = null, + coverPath: String? = null, viewModel: BookInfoViewModel, onBack: () -> Unit, onFinish: (resultCode: Int?, afterTransition: Boolean) -> Unit, @@ -80,8 +84,14 @@ fun BookInfoRouteScreen( viewModel.onReaderResult(it.resultCode) } - LaunchedEffect(bookUrl, viewModel) { - viewModel.initData(bookUrl) + LaunchedEffect(bookUrl, name, author, origin, coverPath, viewModel) { + viewModel.initData( + bookUrl = bookUrl, + name = name, + author = author, + origin = origin, + coverPath = coverPath + ) } DisposableEffect(viewModel) { diff --git a/app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt b/app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt index b53a9091e..cb05a74bd 100644 --- a/app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt @@ -50,6 +50,7 @@ import io.legado.app.model.analyzeRule.AnalyzeUrl import io.legado.app.model.localBook.LocalBook import io.legado.app.model.webBook.WebBook import io.legado.app.ui.config.coverConfig.CoverConfig +import io.legado.app.ui.main.MainIntent import io.legado.app.ui.widget.components.image.cover.buildCoverImageRequest import io.legado.app.utils.ArchiveUtils import io.legado.app.utils.GSON @@ -113,12 +114,37 @@ class BookInfoViewModel( private var readRecordObserveJob: Job? = null fun initData(intent: Intent) { - initData(intent.getStringExtra("bookUrl") ?: "") + initData( + bookUrl = intent.getStringExtra(MainIntent.EXTRA_BOOK_URL) ?: "", + name = intent.getStringExtra(MainIntent.EXTRA_BOOK_NAME), + author = intent.getStringExtra(MainIntent.EXTRA_BOOK_AUTHOR), + origin = intent.getStringExtra(MainIntent.EXTRA_BOOK_ORIGIN), + coverPath = intent.getStringExtra(MainIntent.EXTRA_BOOK_COVER) + ) } - fun initData(bookUrl: String) { + fun initData( + bookUrl: String, + name: String? = null, + author: String? = null, + origin: String? = null, + coverPath: String? = null + ) { if (currentBook?.bookUrl == bookUrl) return - currentBook = null + _uiState.value = BookInfoUiState() // 立即重置 UI 状态 + currentBook = if (!name.isNullOrBlank() && !author.isNullOrBlank()) { + Book( + bookUrl = bookUrl, + name = name, + author = author, + origin = origin ?: BookType.localTag, + coverUrl = coverPath + ).apply { + addType(BookType.notShelf) + } + } else { + null + } currentChapterList = emptyList() currentWebFiles = emptyList() currentKindLabels = emptyList() @@ -128,24 +154,32 @@ class BookInfoViewModel( bookSource = null chapterChanged = false clearReadRecordObserve() - _uiState.value = BookInfoUiState() + syncUiState() execute { - val book = appDb.bookDao.getBook(bookUrl)?.let { - inBookshelf = !it.isNotShelf - it - } ?: appDb.searchBookDao.getSearchBook(bookUrl)?.toBook()?.let { - inBookshelf = false - it - } ?: throw NoStackTraceException("未找到书籍") - + val dbBook = appDb.bookDao.getBook(bookUrl) + if (dbBook != null) { + inBookshelf = !dbBook.isNotShelf + dbBook + } else { + val searchBook = appDb.searchBookDao.getSearchBook(bookUrl)?.toBook() + if (searchBook != null) { + inBookshelf = false + searchBook + } else { + currentBook ?: throw NoStackTraceException("未找到书籍") + } + } + }.onSuccess { book -> + // 如果从数据库/搜索中拿到的书没有封面,但我们有传入的封面,则保留传入的封面 + if (book.coverUrl.isNullOrBlank() && !coverPath.isNullOrBlank()) { + book.coverUrl = coverPath + } val source = if (book.isLocal) { null } else { appDb.bookSourceDao.getBookSource(book.origin) } - book to source - }.onSuccess { - upBook(it.first, it.second) + upBook(book, source) }.onError { context.toastOnUi(it.localizedMessage ?: "未找到书籍") emitEffect(BookInfoEffect.Finish(afterTransition = true)) 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 461a78df4..01d004271 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 @@ -40,13 +40,15 @@ class SearchActivity : BaseComposeActivity() { SearchScreen( viewModel = viewModel, onBack = { finish() }, - onOpenBookInfo = { name, author, bookUrl -> + onOpenBookInfo = { name, author, bookUrl, origin, coverPath, _ -> startActivity( MainActivity.createBookInfoIntent( context = this, name = name, author = author, - bookUrl = bookUrl + bookUrl = bookUrl, + origin = origin, + coverPath = coverPath ) ) }, diff --git a/app/src/main/java/io/legado/app/ui/book/search/SearchContract.kt b/app/src/main/java/io/legado/app/ui/book/search/SearchContract.kt index 36efba3eb..429bef7aa 100644 --- a/app/src/main/java/io/legado/app/ui/book/search/SearchContract.kt +++ b/app/src/main/java/io/legado/app/ui/book/search/SearchContract.kt @@ -58,7 +58,7 @@ sealed interface SearchIntent { data object PauseEngine : SearchIntent data object ResumeEngine : SearchIntent data class UseHistoryKeyword(val keyword: String) : SearchIntent - data class OpenSearchBook(val book: SearchBook) : SearchIntent + data class OpenSearchBook(val book: SearchBook, val sharedCoverKey: String?) : SearchIntent data class OpenBookshelfBook(val book: BookShelfItem) : SearchIntent data class DeleteHistory(val item: SearchKeyword) : SearchIntent data class SetClearHistoryDialogVisible(val visible: Boolean) : SearchIntent @@ -82,6 +82,9 @@ sealed interface SearchEffect { val name: String, val author: String, val bookUrl: String, + val origin: String? = null, + val coverPath: String? = null, + val sharedCoverKey: String?, ) : SearchEffect data object OpenSourceManage : SearchEffect diff --git a/app/src/main/java/io/legado/app/ui/book/search/SearchScreen.kt b/app/src/main/java/io/legado/app/ui/book/search/SearchScreen.kt index 59f54dd80..8da472196 100644 --- a/app/src/main/java/io/legado/app/ui/book/search/SearchScreen.kt +++ b/app/src/main/java/io/legado/app/ui/book/search/SearchScreen.kt @@ -90,7 +90,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged fun SearchScreen( viewModel: SearchViewModel, onBack: () -> Unit, - onOpenBookInfo: (name: String, author: String, bookUrl: String) -> Unit, + onOpenBookInfo: (name: String, author: String, bookUrl: String, origin: String?, coverPath: String?, sharedCoverKey: String?) -> Unit, onOpenSourceManage: () -> Unit, sharedTransitionScope: SharedTransitionScope? = null, animatedVisibilityScope: AnimatedVisibilityScope? = null, @@ -160,7 +160,14 @@ fun SearchScreen( viewModel.effects.collect { effect -> when (effect) { is SearchEffect.OpenBookInfo -> { - onOpenBookInfo(effect.name, effect.author, effect.bookUrl) + onOpenBookInfo( + effect.name, + effect.author, + effect.bookUrl, + effect.origin, + effect.coverPath, + effect.sharedCoverKey + ) } SearchEffect.OpenSourceManage -> onOpenSourceManage() @@ -394,16 +401,25 @@ fun SearchScreen( itemsIndexed( items = state.results, key = { index, item -> "${item.book.origin}:${item.book.bookUrl}:$index" } - ) { _, item -> + ) { index, item -> + val sharedCoverKey = bookCoverSharedElementKey( + item.book.bookUrl, + "search:${item.book.origin}:$index" + ) SearchBookListItem( book = item.book, shelfState = item.shelfState, onClick = { - viewModel.onIntent(SearchIntent.OpenSearchBook(item.book)) + viewModel.onIntent( + SearchIntent.OpenSearchBook( + item.book, + sharedCoverKey + ) + ) }, sharedTransitionScope = sharedTransitionScope, animatedVisibilityScope = animatedVisibilityScope, - sharedCoverKey = bookCoverSharedElementKey(item.book.bookUrl) + sharedCoverKey = sharedCoverKey ) } 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 8213e876f..d5e01a647 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 @@ -15,7 +15,6 @@ import io.legado.app.domain.usecase.SearchBooksUseCase import io.legado.app.domain.usecase.SearchRunEvent import io.legado.app.help.config.AppConfig import io.legado.app.ui.config.otherConfig.OtherConfig -import io.legado.app.ui.main.bookshelf.BookShelfItem import io.legado.app.utils.getPrefBoolean import io.legado.app.utils.putPrefBoolean import kotlinx.coroutines.CancellationException @@ -107,6 +106,9 @@ class SearchViewModel( name = intent.book.name, author = intent.book.author, bookUrl = intent.book.bookUrl, + origin = intent.book.origin, + coverPath = intent.book.coverUrl, + sharedCoverKey = intent.sharedCoverKey, ) ) } @@ -117,6 +119,9 @@ class SearchViewModel( name = intent.book.name, author = intent.book.author, bookUrl = intent.book.bookUrl, + origin = intent.book.origin, + coverPath = intent.book.getDisplayCover(), + sharedCoverKey = null, ) ) } diff --git a/app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfig.kt b/app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfig.kt index 654d4723a..b9a400816 100644 --- a/app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfig.kt +++ b/app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfig.kt @@ -141,6 +141,8 @@ object ThemeConfig { var showDiscovery by prefDelegate(PreferKey.showDiscovery, true) + var showHome by prefDelegate(PreferKey.showHome, true) + var showRss by prefDelegate(PreferKey.showRss, true) var showStatusBar by prefDelegate(PreferKey.showStatusBar, true) diff --git a/app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigScreen.kt b/app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigScreen.kt index 033ba9481..a7bfe181b 100644 --- a/app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigScreen.kt +++ b/app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigScreen.kt @@ -20,7 +20,6 @@ import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -47,7 +46,6 @@ import androidx.compose.material3.ButtonGroupDefaults import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -57,7 +55,6 @@ import androidx.compose.material3.ToggleButtonDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -73,31 +70,29 @@ import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView import androidx.constraintlayout.compose.ConstraintLayout -import com.google.android.material.color.DynamicColors -import com.google.android.material.color.DynamicColorsOptions +import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.legado.app.R import io.legado.app.base.AppContextWrapper -import io.legado.app.constant.PreferKey import io.legado.app.constant.EventBus +import io.legado.app.constant.PreferKey import io.legado.app.help.LauncherIconHelp -import io.legado.app.help.loadFontFiles import io.legado.app.help.config.AppConfig import io.legado.app.help.config.OldThemeConfig -import io.legado.app.lib.theme.ThemeStore -import io.legado.app.lib.theme.primaryColor +import io.legado.app.help.loadFontFiles import io.legado.app.ui.theme.LegadoTheme import io.legado.app.ui.theme.ThemeEngine import io.legado.app.ui.theme.ThemeResolver import io.legado.app.ui.theme.adaptiveContentPadding import io.legado.app.ui.widget.components.AppScaffold -import io.legado.app.ui.widget.components.AppTextField import io.legado.app.ui.widget.components.SplicedColumnGroup -import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet import io.legado.app.ui.widget.components.alert.AppAlertDialog -import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton import io.legado.app.ui.widget.components.button.SmallIconButton +import io.legado.app.ui.widget.components.card.GlassCard import io.legado.app.ui.widget.components.dialog.ColorPickerSheet +import io.legado.app.ui.widget.components.icon.AppIcons +import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet import io.legado.app.ui.widget.components.settingItem.ClickableSettingItem import io.legado.app.ui.widget.components.settingItem.DropdownListSettingItem import io.legado.app.ui.widget.components.settingItem.SliderSettingItem @@ -105,7 +100,7 @@ import io.legado.app.ui.widget.components.settingItem.SwitchSettingItem import io.legado.app.ui.widget.components.text.AppText import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults -import io.legado.app.utils.FileDoc +import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton import io.legado.app.utils.getPrefString import io.legado.app.utils.postEvent import io.legado.app.utils.putPrefString @@ -113,10 +108,6 @@ import io.legado.app.utils.restart import io.legado.app.utils.takePersistablePermissionSafely import io.legado.app.utils.toastOnUi import org.koin.androidx.compose.koinViewModel -import top.yukonga.miuix.kmp.theme.MiuixTheme -import top.yukonga.miuix.kmp.basic.Card as MiuixCard -import top.yukonga.miuix.kmp.basic.CardDefaults as MiuixCardDefaults -import top.yukonga.miuix.kmp.basic.Text as MiuixText @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @Composable @@ -138,16 +129,16 @@ fun ThemeConfigScreen( var showBorderColorPicker by remember { mutableStateOf(false) } var showNavIconSheet by remember { mutableStateOf(false) } var showFontSheet by remember { mutableStateOf(false) } - var fontItems by remember { mutableStateOf>(emptyList()) } - var fontFolderUri by remember { mutableStateOf(null) } + val showThemeRefactorTip by viewModel.showThemeRefactorTip.collectAsStateWithLifecycle() - fun loadFonts() { - fontItems = loadFontFiles(context, fontFolderUri) + var fontFolderUri by remember { + mutableStateOf( + context.getPrefString(PreferKey.fontFolder)?.let { Uri.parse(it) } + ) } - remember { - val saved = context.getPrefString(PreferKey.fontFolder) - if (!saved.isNullOrEmpty()) fontFolderUri = Uri.parse(saved) - loadFonts() + + val fontItems = remember(fontFolderUri) { + loadFontFiles(context, fontFolderUri) } val fontFolderLauncher = rememberLauncherForActivityResult( @@ -157,7 +148,6 @@ fun ThemeConfigScreen( fontFolderUri = uri uri.takePersistablePermissionSafely(context, Intent.FLAG_GRANT_READ_URI_PERMISSION) context.putPrefString(PreferKey.fontFolder, uri.toString()) - loadFonts() } } @@ -218,16 +208,28 @@ fun ThemeConfigScreen( themeItems.zip(themeValues).toList() } - if (isMiuixEngine) { - MiuixCard( + AnimatedVisibility(visible = showThemeRefactorTip) { + GlassCard( cornerRadius = 16.dp, - insideMargin = PaddingValues(16.dp), - colors = MiuixCardDefaults.defaultColors( - color = MiuixTheme.colorScheme.primaryVariant, - contentColor = MiuixTheme.colorScheme.onPrimary - ) + modifier = Modifier.padding(bottom = 16.dp) ) { - MiuixText(stringResource(R.string.theme_config_miuix_experimental_warning)) + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(16.dp) + ) { + AppText( + text = "仍有部分界面未用Compose重构,这些界面会与大部分界面有较大差异。", + style = LegadoTheme.typography.labelLargeEmphasized, + modifier = Modifier.weight(1f) + ) + SmallIconButton( + imageVector = AppIcons.Close, + contentDescription = "关闭", + onClick = { + viewModel.setShowThemeRefactorTip(false) + } + ) + } } } @@ -376,6 +378,11 @@ fun ThemeConfigScreen( } SplicedColumnGroup(title = stringResource(R.string.main_activity)) { + SwitchSettingItem( + title = stringResource(R.string.show_home), + checked = ThemeConfig.showHome, + onCheckedChange = { ThemeConfig.showHome = it } + ) SwitchSettingItem( title = stringResource(R.string.show_discovery), checked = ThemeConfig.showDiscovery, @@ -648,7 +655,11 @@ fun ThemeConfigScreen( .size(28.dp) .clip(CircleShape) .background(Color(ThemeConfig.itemDividerColor)) - .border(1.dp, MaterialTheme.colorScheme.outlineVariant, CircleShape) + .border( + 1.dp, + MaterialTheme.colorScheme.outlineVariant, + CircleShape + ) ) } } @@ -771,7 +782,9 @@ fun ThemeConfigScreen( content = { if (fontItems.isEmpty()) { Box( - modifier = Modifier.fillMaxWidth().height(120.dp), + modifier = Modifier + .fillMaxWidth() + .height(120.dp), contentAlignment = Alignment.Center ) { Text( @@ -789,7 +802,9 @@ fun ThemeConfigScreen( fontItems.forEach { fontDoc -> item { Card( - modifier = Modifier.fillMaxWidth().height(100.dp), + modifier = Modifier + .fillMaxWidth() + .height(100.dp), onClick = { ThemeConfig.appFontPath = fontDoc.uri.toString() showFontSheet = false diff --git a/app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigViewModel.kt b/app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigViewModel.kt index 3202bace3..af5d4c0d9 100644 --- a/app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigViewModel.kt @@ -2,19 +2,40 @@ package io.legado.app.ui.config.themeConfig import android.net.Uri import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import io.legado.app.constant.PreferKey +import io.legado.app.data.local.preferences.LocalPreferencesKeys +import io.legado.app.data.local.preferences.LocalPreferencesRepository import io.legado.app.utils.FileDoc import io.legado.app.utils.FileUtils import io.legado.app.utils.MD5Utils import io.legado.app.utils.externalFiles import io.legado.app.utils.inputStream import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import splitties.init.appCtx import java.io.File import java.io.FileOutputStream -class ThemeConfigViewModel : ViewModel() { +class ThemeConfigViewModel( + private val localPreferencesRepository: LocalPreferencesRepository +) : ViewModel() { + + val showThemeRefactorTip = localPreferencesRepository + .getPreference(LocalPreferencesKeys.SHOW_THEME_REFACTOR_TIP, true) + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), true) + + fun setShowThemeRefactorTip(show: Boolean) { + viewModelScope.launch { + localPreferencesRepository.updatePreference( + LocalPreferencesKeys.SHOW_THEME_REFACTOR_TIP, + show + ) + } + } /** * 设置背景图片 diff --git a/app/src/main/java/io/legado/app/ui/config/themeManage/EditThemeSheet.kt b/app/src/main/java/io/legado/app/ui/config/themeManage/EditThemeSheet.kt index a3b42b9a0..5496cba9c 100644 --- a/app/src/main/java/io/legado/app/ui/config/themeManage/EditThemeSheet.kt +++ b/app/src/main/java/io/legado/app/ui/config/themeManage/EditThemeSheet.kt @@ -31,7 +31,6 @@ import androidx.compose.ui.unit.dp import io.legado.app.R import io.legado.app.help.config.ThemeExportData import io.legado.app.ui.widget.components.AppTextField -import io.legado.app.ui.widget.components.SearchBar import io.legado.app.ui.widget.components.button.MediumIconButton import io.legado.app.ui.widget.components.dialog.ColorPickerSheet import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet @@ -164,6 +163,11 @@ fun EditThemeSheet( // Interface layout SectionTitle(stringResource(R.string.theme_manage_section_layout)) + CompactSwitchSettingItem( + title = "首页", + checked = data.showHome, + onCheckedChange = { data = data.copy(showHome = it) } + ) CompactSwitchSettingItem( title = stringResource(R.string.theme_manage_show_discovery), checked = data.showDiscovery, diff --git a/app/src/main/java/io/legado/app/ui/main/BookCoverSharedElement.kt b/app/src/main/java/io/legado/app/ui/main/BookCoverSharedElement.kt index bf3a36879..fa97c8688 100644 --- a/app/src/main/java/io/legado/app/ui/main/BookCoverSharedElement.kt +++ b/app/src/main/java/io/legado/app/ui/main/BookCoverSharedElement.kt @@ -1,3 +1,6 @@ package io.legado.app.ui.main -fun bookCoverSharedElementKey(bookUrl: String): String = "book-cover:$bookUrl" +fun bookCoverSharedElementKey(bookUrl: String, sourceId: String? = null): String { + val source = sourceId?.takeIf { it.isNotBlank() } ?: return "book-cover:$bookUrl" + return "book-cover:$source:$bookUrl" +} diff --git a/app/src/main/java/io/legado/app/ui/main/MainActivity.kt b/app/src/main/java/io/legado/app/ui/main/MainActivity.kt index 3c7bee3b6..b44cd923c 100644 --- a/app/src/main/java/io/legado/app/ui/main/MainActivity.kt +++ b/app/src/main/java/io/legado/app/ui/main/MainActivity.kt @@ -100,8 +100,11 @@ open class MainActivity : BaseComposeActivity(), VariableDialog.Callback { context: Context, name: String? = null, author: String? = null, - bookUrl: String - ): Intent = MainIntent.createBookInfoIntent(context, name, author, bookUrl) + bookUrl: String, + origin: String? = null, + coverPath: String? = null + ): Intent = + MainIntent.createBookInfoIntent(context, name, author, bookUrl, origin, coverPath) fun createExploreShowIntent( context: Context, diff --git a/app/src/main/java/io/legado/app/ui/main/MainIntent.kt b/app/src/main/java/io/legado/app/ui/main/MainIntent.kt index 24e769d93..0f70a3c8b 100644 --- a/app/src/main/java/io/legado/app/ui/main/MainIntent.kt +++ b/app/src/main/java/io/legado/app/ui/main/MainIntent.kt @@ -12,6 +12,8 @@ object MainIntent { const val EXTRA_BOOK_NAME = "name" const val EXTRA_BOOK_AUTHOR = "author" const val EXTRA_BOOK_URL = "bookUrl" + const val EXTRA_BOOK_ORIGIN = "origin" + const val EXTRA_BOOK_COVER = "coverPath" const val EXTRA_EXPLORE_NAME = "exploreName" const val EXTRA_SOURCE_URL = "sourceUrl" const val EXTRA_EXPLORE_URL = "exploreUrl" @@ -114,13 +116,17 @@ object MainIntent { context: Context, name: String? = null, author: String? = null, - bookUrl: String + bookUrl: String, + origin: String? = null, + coverPath: String? = null ): Intent { return createLauncherIntent(context).apply { putExtra(EXTRA_START_ROUTE, MainRouteConst.ROUTE_BOOK_INFO) putExtra(EXTRA_BOOK_NAME, name) putExtra(EXTRA_BOOK_AUTHOR, author) putExtra(EXTRA_BOOK_URL, bookUrl) + putExtra(EXTRA_BOOK_ORIGIN, origin) + putExtra(EXTRA_BOOK_COVER, coverPath) } } diff --git a/app/src/main/java/io/legado/app/ui/main/MainNavGraph.kt b/app/src/main/java/io/legado/app/ui/main/MainNavGraph.kt index 9af1db32a..0aa0db1ef 100644 --- a/app/src/main/java/io/legado/app/ui/main/MainNavGraph.kt +++ b/app/src/main/java/io/legado/app/ui/main/MainNavGraph.kt @@ -90,12 +90,15 @@ fun MainActivity.mainEntryProvider( onNavigateToBookCacheManage = { onNavigateToRoute(MainRouteBookCacheManage) }, - onNavigateToBookInfo = { name, author, bookUrl -> + onNavigateToBookInfo = { name, author, bookUrl, origin, coverPath, sharedCoverKey -> onNavigateToRoute( MainRouteBookInfo( name = name, author = author, - bookUrl = bookUrl + bookUrl = bookUrl, + origin = origin, + coverPath = coverPath, + sharedCoverKey = sharedCoverKey ) ) }, @@ -246,12 +249,15 @@ fun MainActivity.mainEntryProvider( searchViewModel.onIntent(SearchIntent.ClearSearchResults) onNavigateBack() }, - onOpenBookInfo = { name, author, bookUrl -> + onOpenBookInfo = { name, author, bookUrl, origin, coverPath, sharedCoverKey -> onNavigateToRoute( MainRouteBookInfo( name = name, author = author, - bookUrl = bookUrl + bookUrl = bookUrl, + origin = origin, + coverPath = coverPath, + sharedCoverKey = sharedCoverKey ) ) }, @@ -395,9 +401,13 @@ fun MainActivity.mainEntryProvider( } else null } ) { route -> - val bookInfoViewModel = koinViewModel() + val bookInfoViewModel = koinViewModel(key = route.bookUrl) BookInfoRouteScreen( bookUrl = route.bookUrl, + name = route.name, + author = route.author, + origin = route.origin, + coverPath = route.coverPath, viewModel = bookInfoViewModel, onBack = { onNavigateBack() }, onFinish = { _, _ -> onNavigateBack() }, @@ -406,7 +416,7 @@ fun MainActivity.mainEntryProvider( }, sharedTransitionScope = sharedTransitionScope, animatedVisibilityScope = LocalNavAnimatedContentScope.current, - sharedCoverKey = bookCoverSharedElementKey(route.bookUrl), + sharedCoverKey = route.sharedCoverKey ?: bookCoverSharedElementKey(route.bookUrl), onRegisterVariableSetter = { setter -> onRegisterVariableSetter(setter) } @@ -419,12 +429,15 @@ fun MainActivity.mainEntryProvider( sourceUrl = route.sourceUrl, exploreUrl = route.exploreUrl, onBack = { onNavigateBack() }, - onBookClick = { book -> + onBookClick = { book, sharedCoverKey -> onNavigateToRoute( MainRouteBookInfo( name = book.name, author = book.author, - bookUrl = book.bookUrl + bookUrl = book.bookUrl, + origin = book.origin, + coverPath = book.coverUrl, + sharedCoverKey = sharedCoverKey ) ) }, diff --git a/app/src/main/java/io/legado/app/ui/main/MainNavKey.kt b/app/src/main/java/io/legado/app/ui/main/MainNavKey.kt index ee8a3c2db..c08763921 100644 --- a/app/src/main/java/io/legado/app/ui/main/MainNavKey.kt +++ b/app/src/main/java/io/legado/app/ui/main/MainNavKey.kt @@ -65,6 +65,9 @@ data class MainRouteBookInfo( val name: String?, val author: String?, val bookUrl: String, + val origin: String? = null, + val coverPath: String? = null, + val sharedCoverKey: String? = null, ) : MainRoute @Serializable diff --git a/app/src/main/java/io/legado/app/ui/main/MainNavigator.kt b/app/src/main/java/io/legado/app/ui/main/MainNavigator.kt index ca7786091..fc349eee0 100644 --- a/app/src/main/java/io/legado/app/ui/main/MainNavigator.kt +++ b/app/src/main/java/io/legado/app/ui/main/MainNavigator.kt @@ -254,7 +254,9 @@ object MainNavigator { MainRouteBookInfo( name = intent.getStringExtra(MainIntent.EXTRA_BOOK_NAME), author = intent.getStringExtra(MainIntent.EXTRA_BOOK_AUTHOR), - bookUrl = bookUrl + bookUrl = bookUrl, + origin = intent.getStringExtra(MainIntent.EXTRA_BOOK_ORIGIN), + coverPath = intent.getStringExtra(MainIntent.EXTRA_BOOK_COVER) ) } ?: MainRouteHome diff --git a/app/src/main/java/io/legado/app/ui/main/MainScreen.kt b/app/src/main/java/io/legado/app/ui/main/MainScreen.kt index 0aa926247..5e0bf4a9e 100644 --- a/app/src/main/java/io/legado/app/ui/main/MainScreen.kt +++ b/app/src/main/java/io/legado/app/ui/main/MainScreen.kt @@ -110,7 +110,7 @@ fun MainScreen( onNavigateToLocalImport: () -> Unit, onNavigateToCache: (Long) -> Unit, onNavigateToBookCacheManage: () -> Unit, - onNavigateToBookInfo: (name: String, author: String, bookUrl: String) -> Unit, + onNavigateToBookInfo: (name: String, author: String, bookUrl: String, origin: String?, coverPath: String?, sharedCoverKey: String?) -> Unit, onNavigateToExploreShow: (title: String?, sourceUrl: String, exploreUrl: String?) -> Unit, onNavigateToRssSort: (sourceUrl: String, sortUrl: String?, key: String?) -> Unit, onNavigateToRssRead: (title: String?, origin: String, link: String?, openUrl: String?) -> Unit, @@ -368,8 +368,15 @@ fun MainScreen( val destination = destinations.getOrNull(page) ?: return@HorizontalPager when (destination) { MainDestination.Home -> HomepageScreen( - onBookClick = { name, author, bookUrl -> - onNavigateToBookInfo(name ?: "", author ?: "", bookUrl) + onBookClick = { name, author, bookUrl, origin, coverPath, sharedCoverKey -> + onNavigateToBookInfo( + name ?: "", + author ?: "", + bookUrl, + origin, + coverPath, + sharedCoverKey + ) }, onModuleHeaderClick = { title, sourceUrl, exploreUrl -> onNavigateToExploreShow(title, sourceUrl, exploreUrl) @@ -382,8 +389,15 @@ fun MainScreen( onBookClick = { book -> context.startActivityForBook(book) }, - onBookLongClick = { book -> - onNavigateToBookInfo(book.name, book.author, book.bookUrl) + onBookLongClick = { book, sharedCoverKey -> + onNavigateToBookInfo( + book.name, + book.author, + book.bookUrl, + book.origin, + book.getDisplayCover(), + sharedCoverKey + ) }, onNavigateToSearch = { query -> onNavigateToSearch(query) }, onNavigateToRemoteImport = onNavigateToRemoteImport, diff --git a/app/src/main/java/io/legado/app/ui/main/MainViewModel.kt b/app/src/main/java/io/legado/app/ui/main/MainViewModel.kt index ac5e3297e..1855a0a5d 100644 --- a/app/src/main/java/io/legado/app/ui/main/MainViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/main/MainViewModel.kt @@ -31,6 +31,7 @@ class MainViewModel( private val prefs = context.defaultSharedPreferences private val mainPreferenceKeys = setOf( PreferKey.showDiscovery, + PreferKey.showHome, PreferKey.showRss, PreferKey.showBottomView, PreferKey.useFloatingBottomBar, @@ -152,10 +153,12 @@ private const val NAV_EXTENDED_KEY = "navExtended" private fun MainViewModel.readMainUiState(): MainUiState { val showDiscovery = context.getPrefBoolean(PreferKey.showDiscovery, true) + val showHome = context.getPrefBoolean(PreferKey.showHome, true) val showRss = context.getPrefBoolean(PreferKey.showRss, true) val destinations = MainDestination.mainDestinations.filter { when (it) { MainDestination.Explore -> showDiscovery + MainDestination.Home -> showHome MainDestination.Rss -> showRss else -> true } 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 156243c59..4b50443c1 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 @@ -153,7 +153,7 @@ import sh.calvin.reorderable.rememberReorderableLazyGridState fun BookshelfScreen( viewModel: BookshelfViewModel = koinViewModel(), onBookClick: (BookShelfItem) -> Unit, - onBookLongClick: (BookShelfItem) -> Unit, + onBookLongClick: (book: BookShelfItem, sharedCoverKey: String?) -> Unit, onNavigateToSearch: (String) -> Unit, onNavigateToRemoteImport: () -> Unit, onNavigateToLocalImport: () -> Unit, @@ -1211,7 +1211,7 @@ fun BookshelfPage( onDragFinished: () -> Unit, onGlobalSearch: () -> Unit, onBookClick: (BookShelfItem) -> Unit, - onBookLongClick: (BookShelfItem) -> Unit, + onBookLongClick: (BookShelfItem, String?) -> Unit, isCurrentPage: Boolean = true, sharedTransitionScope: SharedTransitionScope? = null, animatedVisibilityScope: AnimatedVisibilityScope? = null, @@ -1327,6 +1327,14 @@ fun BookshelfPage( ) { items(displayBooks, key = { it.book.bookUrl }) { bookUi -> val isSelected = selectedBookUrls.contains(bookUi.book.bookUrl) + val sharedCoverKey = if (isCurrentPage) { + bookCoverSharedElementKey( + bookUi.book.bookUrl, + "bookshelf:${uiState.selectedGroupId}" + ) + } else { + null + } ReorderableItem( state = reorderableState, key = bookUi.book.bookUrl, @@ -1366,7 +1374,7 @@ fun BookshelfPage( searchKey = uiState.searchKey, sharedTransitionScope = sharedTransitionScope, animatedVisibilityScope = animatedVisibilityScope, - sharedCoverKey = if (isCurrentPage) bookCoverSharedElementKey(bookUi.book.bookUrl) else null, + sharedCoverKey = sharedCoverKey, onClick = { if (uiState.isEditMode) { onToggleBookSelection(bookUi) @@ -1381,7 +1389,7 @@ fun BookshelfPage( if (uiState.isEditMode) { onToggleBookSelection(bookUi) } else { - onBookLongClick(bookUi.book) + onBookLongClick(bookUi.book, sharedCoverKey) } } } diff --git a/app/src/main/java/io/legado/app/ui/main/homepage/HomepageEffect.kt b/app/src/main/java/io/legado/app/ui/main/homepage/HomepageEffect.kt index 5df9ae961..48452096b 100644 --- a/app/src/main/java/io/legado/app/ui/main/homepage/HomepageEffect.kt +++ b/app/src/main/java/io/legado/app/ui/main/homepage/HomepageEffect.kt @@ -5,6 +5,9 @@ sealed interface HomepageEffect { val name: String?, val author: String?, val bookUrl: String, + val origin: String? = null, + val coverPath: String? = null, + val sharedCoverKey: String?, ) : HomepageEffect data class NavigateToExploreShow( diff --git a/app/src/main/java/io/legado/app/ui/main/homepage/HomepageModuleManageSheet.kt b/app/src/main/java/io/legado/app/ui/main/homepage/HomepageModuleManageSheet.kt index ac998e93a..4f90b9e9d 100644 --- a/app/src/main/java/io/legado/app/ui/main/homepage/HomepageModuleManageSheet.kt +++ b/app/src/main/java/io/legado/app/ui/main/homepage/HomepageModuleManageSheet.kt @@ -43,9 +43,12 @@ import io.legado.app.ui.widget.components.JsonRawEditor import io.legado.app.ui.widget.components.alert.AppAlertDialog import io.legado.app.ui.widget.components.button.SecondaryButton import io.legado.app.ui.widget.components.button.SmallIconButton +import io.legado.app.ui.widget.components.card.GlassCard import io.legado.app.ui.widget.components.card.ReorderableSelectionItem import io.legado.app.ui.widget.components.card.SelectionItemCard import io.legado.app.ui.widget.components.divider.PillDivider +import io.legado.app.ui.widget.components.divider.PillHeaderDivider +import io.legado.app.ui.widget.components.explore.ExploreKindSelectSheet 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 @@ -102,6 +105,7 @@ fun HomepageModuleManageSheet( var browseModuleType by remember(data != null) { mutableStateOf("card") } var selectedKindTitles by remember(data != null) { mutableStateOf>(emptySet()) } var showCustomSetAddModules by remember(data != null) { mutableStateOf(false) } + var showKindSelect by remember(data != null) { mutableStateOf(false) } var showAddButtonGroupDialog by remember(data != null) { mutableStateOf(false) } val defaultQuickActionsTitle = stringResource(R.string.homepage_quick_actions) var tempButtonGroupTitle by remember(data != null) { mutableStateOf(defaultQuickActionsTitle) } @@ -310,8 +314,7 @@ fun HomepageModuleManageSheet( onClick = { deleteConfirmId = module.id }, imageVector = Icons.Default.Delete ) - }, - modifier = Modifier.padding(horizontal = 4.dp) + } ) } @@ -353,8 +356,7 @@ fun HomepageModuleManageSheet( onClick = { deleteConfirmId = module.id }, imageVector = Icons.Default.Delete ) - }, - modifier = Modifier.padding(horizontal = 4.dp) + } ) } } @@ -398,8 +400,7 @@ fun HomepageModuleManageSheet( sourceUrl = browseUrl, ) ) - }, - modifier = Modifier.padding(horizontal = 4.dp) + } ) } } @@ -408,90 +409,76 @@ fun HomepageModuleManageSheet( 2 -> { val isButtonGroup = browseModuleType == "buttonGroup" - val selectableKinds = exploreKinds Column { val typeList = remember { HomepageModuleType.entries.filter { it != HomepageModuleType.Unknown } } - CompactDropdownSettingItem( - title = stringResource(R.string.homepage_module_type), - selectedValue = browseModuleType, - displayEntries = typeList.map { it.title }.toTypedArray(), - entryValues = typeList.map { it.key }.toTypedArray(), - onValueChange = { - browseModuleType = it; selectedKindTitles = emptySet() + + GlassCard( + containerColor = LegadoTheme.colorScheme.onSheetContent, + cornerRadius = 12.dp + ) { + CompactDropdownSettingItem( + title = stringResource(R.string.homepage_module_type), + selectedValue = browseModuleType, + displayEntries = typeList.map { it.title }.toTypedArray(), + entryValues = typeList.map { it.key }.toTypedArray(), + onValueChange = { + browseModuleType = it; selectedKindTitles = emptySet() + } + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + SelectionItemCard( + title = stringResource(R.string.homepage_select_from_kinds), + subtitle = if (isButtonGroup) { + if (selectedKindTitles.isEmpty()) stringResource(R.string.homepage_select_multiple_kinds) + else stringResource( + R.string.homepage_n_selected, + selectedKindTitles.size + ) + } else { + stringResource(R.string.homepage_select_one_kind) + }, + containerColor = LegadoTheme.colorScheme.onSheetContent, + onToggleSelection = { showKindSelect = true }, + trailingAction = { + if (isButtonGroup && selectedKindTitles.isNotEmpty()) { + SmallIconButton( + onClick = { showAddButtonGroupDialog = true }, + imageVector = Icons.Default.Check + ) + } } ) - if (selectableKinds.isEmpty()) { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(24.dp), - contentAlignment = Alignment.Center - ) { - AppText( - stringResource(R.string.homepage_source_no_discover), - color = LegadoTheme.colorScheme.onSurfaceVariant - ) - } - } else { - AppText( - stringResource(R.string.homepage_select_items), - style = LegadoTheme.typography.labelMedium, - modifier = Modifier.padding( - horizontal = 16.dp, - vertical = 4.dp - ) - ) - LazyColumn( - modifier = Modifier - .fillMaxWidth() - .weight(1f), - verticalArrangement = Arrangement.spacedBy(4.dp) - ) { - items( - selectableKinds.distinctBy { it.first + it.second }, - key = { it.first + it.second }) { (kindTitle, kindUrl) -> - if (isButtonGroup) { - val isSelected = kindTitle in selectedKindTitles - SelectionItemCard( - title = kindTitle, - subtitle = kindUrl.take(60), - containerColor = LegadoTheme.colorScheme.onSheetContent, - isSelected = isSelected, - inSelectionMode = true, - onToggleSelection = { - selectedKindTitles = - if (isSelected) selectedKindTitles - kindTitle - else selectedKindTitles + kindTitle - }, - modifier = Modifier.padding(horizontal = 4.dp) - ) - } else { - val isJoined = joinedKeys.contains(kindTitle) - SelectionItemCard( - title = kindTitle, - subtitle = kindUrl.take(60) + if (isJoined) stringResource( - R.string.homepage_status_joined - ) else "", - containerColor = LegadoTheme.colorScheme.onSheetContent, - isSelected = isJoined, - inSelectionMode = true, - onToggleSelection = { - if (!isJoined) addDialogPrefill = - AddDialogPrefill( - kindTitle, - kindUrl, - browseModuleType - ) - }, - modifier = Modifier.padding(horizontal = 4.dp) + + ExploreKindSelectSheet( + show = showKindSelect, + onDismissRequest = { showKindSelect = false }, + sourceUrl = browseUrl, + multiple = isButtonGroup, + initialSelectedTitles = selectedKindTitles.toList(), + onSelected = { kinds -> + if (isButtonGroup) { + selectedKindTitles = kinds.map { it.title }.toSet() + } else { + kinds.firstOrNull()?.let { kind -> + addDialogPrefill = AddDialogPrefill( + title = kind.title, + url = kind.url ?: "", + type = browseModuleType ) } } } - } - Spacer(modifier = Modifier.height(12.dp)) + ) + + PillDivider( + modifier = Modifier.padding(vertical = 12.dp) + ) + SecondaryButton( text = stringResource(R.string.homepage_manual_add), onClick = { @@ -544,8 +531,7 @@ fun HomepageModuleManageSheet( joinedInCurrent = joinedInCurrent + (module.moduleKey to "temp_${module.id}") } - }, - modifier = Modifier.padding(horizontal = 4.dp) + } ) } } @@ -567,8 +553,7 @@ fun HomepageModuleManageSheet( onToggleSelection = { browsingSourceUrl = source.sourceUrl browsingDetail = true - }, - modifier = Modifier.padding(horizontal = 4.dp) + } ) } } @@ -662,8 +647,7 @@ fun HomepageModuleManageSheet( onClick = { deleteConfirmId = module.id }, imageVector = Icons.Default.Delete ) - }, - modifier = Modifier.padding(horizontal = 4.dp) + } ) } } @@ -694,8 +678,7 @@ fun HomepageModuleManageSheet( onClick = { deleteConfirmId = module.id }, imageVector = Icons.Default.Delete ) - }, - modifier = Modifier.padding(horizontal = 4.dp) + } ) } } @@ -770,8 +753,12 @@ fun HomepageModuleManageSheet( onClick = { deleteSetConfirmId = set.sourceUrl }, imageVector = Icons.Default.Delete ) - }, - modifier = Modifier.padding(horizontal = 4.dp) + } + ) + } + item { + PillDivider( + modifier = Modifier.padding(vertical = 12.dp) ) } item(key = "create_set") { @@ -1001,41 +988,51 @@ fun AddCustomModuleDialog( .fillMaxWidth() .height(400.dp) .verticalScroll(rememberScrollState()), - verticalArrangement = Arrangement.spacedBy(4.dp) + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalAlignment = Alignment.CenterHorizontally ) { AppTextField( value = title, onValueChange = { title = it }, + backgroundColor = LegadoTheme.colorScheme.onSheetContent, label = stringResource(R.string.homepage_title_label), modifier = Modifier.fillMaxWidth() ) AppTextField( value = url, onValueChange = { url = it }, + backgroundColor = LegadoTheme.colorScheme.onSheetContent, label = "URL", modifier = Modifier.fillMaxWidth() ) val typeList = remember { HomepageModuleType.entries.filter { it != HomepageModuleType.Unknown } } - DropdownListSettingItem( - title = stringResource(R.string.homepage_type_label), - selectedValue = type, - displayEntries = typeList.map { it.title }.toTypedArray(), - entryValues = typeList.map { it.key }.toTypedArray(), - onValueChange = { type = it } - ) + + GlassCard( + containerColor = LegadoTheme.colorScheme.onSheetContent + ) { + DropdownListSettingItem( + title = stringResource(R.string.homepage_type_label), + selectedValue = type, + displayEntries = typeList.map { it.title }.toTypedArray(), + entryValues = typeList.map { it.key }.toTypedArray(), + onValueChange = { type = it } + ) + } + AppTextField( value = args, onValueChange = { args = it }, + backgroundColor = LegadoTheme.colorScheme.onSheetContent, label = "Args (JSON)", modifier = Modifier.fillMaxWidth() ) - AppText( - text = stringResource(R.string.homepage_layout_config_label), - style = LegadoTheme.typography.labelMedium, - modifier = Modifier.padding(top = 16.dp, bottom = 4.dp) + + PillHeaderDivider( + title = stringResource(R.string.homepage_layout_config_label) ) + if (hasVisualizableKeys) { JsonConfigEditor( jsonString = layoutConfig, diff --git a/app/src/main/java/io/legado/app/ui/main/homepage/HomepageScreen.kt b/app/src/main/java/io/legado/app/ui/main/homepage/HomepageScreen.kt index 987ba600a..52bfaaa27 100644 --- a/app/src/main/java/io/legado/app/ui/main/homepage/HomepageScreen.kt +++ b/app/src/main/java/io/legado/app/ui/main/homepage/HomepageScreen.kt @@ -10,27 +10,28 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets 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.lazy.staggeredgrid.LazyStaggeredGridState import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridItemSpan -import androidx.compose.foundation.lazy.staggeredgrid.items +import androidx.compose.foundation.lazy.staggeredgrid.itemsIndexed import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowForward import androidx.compose.material.icons.filled.GridView +import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.Settings +import androidx.compose.material.icons.outlined.Info import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -46,7 +47,6 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -60,12 +60,15 @@ import io.legado.app.ui.main.homepage.modules.GridModule import io.legado.app.ui.main.homepage.modules.GridRankingModule import io.legado.app.ui.main.homepage.modules.RankingModule import io.legado.app.ui.main.homepage.modules.WaterfallItem +import io.legado.app.ui.theme.LegadoTheme import io.legado.app.ui.widget.components.AppPullToRefresh import io.legado.app.ui.widget.components.AppScaffold +import io.legado.app.ui.widget.components.LoadMoreFooter import io.legado.app.ui.widget.components.alert.AppAlertDialog import io.legado.app.ui.widget.components.book.SearchBookGridItem -import io.legado.app.ui.widget.components.button.SecondaryButton import io.legado.app.ui.widget.components.button.SmallTonalIconButton +import io.legado.app.ui.widget.components.card.GlassCard +import io.legado.app.ui.widget.components.icon.AppIcon import io.legado.app.ui.widget.components.progressIndicator.AppCircularProgressIndicator import io.legado.app.ui.widget.components.tabRow.AppTabRow import io.legado.app.ui.widget.components.text.AppText @@ -84,7 +87,7 @@ import org.koin.androidx.compose.koinViewModel @Composable fun HomepageScreen( viewModel: HomepageViewModel = koinViewModel(), - onBookClick: (name: String?, author: String?, bookUrl: String) -> Unit, + onBookClick: (name: String?, author: String?, bookUrl: String, origin: String?, coverPath: String?, sharedCoverKey: String?) -> Unit, onModuleHeaderClick: (title: String?, sourceUrl: String, exploreUrl: String?) -> Unit, sharedTransitionScope: SharedTransitionScope? = null, animatedVisibilityScope: AnimatedVisibilityScope? = null, @@ -137,7 +140,14 @@ fun HomepageScreen( viewModel.effects.collect { effect -> when (effect) { is HomepageEffect.NavigateToBookInfo -> - onBookClick(effect.name, effect.author, effect.bookUrl) + onBookClick( + effect.name, + effect.author, + effect.bookUrl, + effect.origin, + effect.coverPath, + effect.sharedCoverKey + ) is HomepageEffect.NavigateToExploreShow -> onModuleHeaderClick(effect.title, effect.sourceUrl, effect.exploreUrl) @@ -238,6 +248,7 @@ fun HomepageScreen( data = errorMsg, onDismissRequest = { errorMsg = null }, title = stringResource(R.string.homepage_module_error), + text = errorMsg, confirmText = stringResource(R.string.copy_text), onConfirm = { context.sendToClip(it) @@ -355,12 +366,14 @@ private fun ModuleList( item(key = "header_${moduleUi.globalId}", span = StaggeredGridItemSpan.FullLine) { ModuleHeader( title = moduleUi.title, - onNavigate = { - viewModel.onModuleHeaderClick( - moduleUi.sourceUrl, - moduleUi.exploreUrl, - moduleUi.title, - ) + onNavigate = if (moduleUi.type == HomepageModuleType.ButtonGroup) null else { + { + viewModel.onModuleHeaderClick( + moduleUi.sourceUrl, + moduleUi.exploreUrl, + moduleUi.title, + ) + } }, ) } @@ -390,27 +403,78 @@ private fun ModuleList( ) { Column( modifier = Modifier - .fillMaxWidth() - .height(80.dp) - .clickable { onErrorClick(state.message) }, - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally ) { - AppText( - text = state.message, - color = MaterialTheme.colorScheme.error, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - textAlign = TextAlign.Center, - modifier = Modifier.padding(horizontal = 16.dp) - ) - Spacer(modifier = Modifier.height(4.dp)) - SecondaryButton( - text = stringResource(R.string.retry), - onClick = { - viewModel.retryModule(moduleUi.globalId) + GlassCard( + onClick = { onErrorClick(state.message) }, + containerColor = LegadoTheme.colorScheme.errorContainer.copy( + alpha = 0.6f + ), + ) { + Column( + modifier = Modifier.fillMaxWidth() + ) { + + Row( + modifier = Modifier + .fillMaxWidth() + .padding( + horizontal = 16.dp, + vertical = 16.dp + ), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + + AppIcon( + imageVector = Icons.Outlined.Info, + contentDescription = null, + tint = LegadoTheme.colorScheme.error + ) + + AppText( + text = state.message, + color = LegadoTheme.colorScheme.error, + style = LegadoTheme.typography.bodySmall, + modifier = Modifier.weight(1f), + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } + + HorizontalDivider( + color = LegadoTheme.colorScheme.error.copy(alpha = 0.3f) + ) + + Box( + modifier = Modifier + .fillMaxWidth() + .clickable { + viewModel.retryModule(moduleUi.globalId) + } + .padding(vertical = 10.dp), + contentAlignment = Alignment.Center + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + AppIcon( + imageVector = Icons.Default.Refresh, + contentDescription = null, + tint = LegadoTheme.colorScheme.error + ) + + AppText( + text = "重试", + color = LegadoTheme.colorScheme.error, + style = LegadoTheme.typography.labelMedium + ) + } + } } - ) + } } } } @@ -435,68 +499,63 @@ private fun ModuleList( val config = moduleUi.config when (moduleUi.type) { HomepageModuleType.Waterfall -> { - items( + itemsIndexed( state.books, - key = { "wf_${moduleUi.globalId}_${it.bookUrl}" }) { book -> + key = { index, book -> "wf_${moduleUi.globalId}_${book.bookUrl}_$index" }) { index, book -> + val sharedCoverKey = bookCoverSharedElementKey( + book.bookUrl, + "home:${moduleUi.globalId}:waterfall:$index" + ) WaterfallItem( book = book, - onClick = { viewModel.onBookClick(book) }, + onClick = { viewModel.onBookClick(book, sharedCoverKey) }, sharedTransitionScope = sharedTransitionScope, animatedVisibilityScope = animatedVisibilityScope, + sharedCoverKey = sharedCoverKey, ) } - if (state.hasMore) { - item( - key = "wf_more_${moduleUi.globalId}", - span = StaggeredGridItemSpan.FullLine - ) { - LaunchedEffect(state.books.size) { - viewModel.loadMoreModule(moduleUi.globalId) - } - Box( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - contentAlignment = Alignment.Center - ) { - AppCircularProgressIndicator(modifier = Modifier.size(24.dp)) - } - } + item( + key = "wf_more_${moduleUi.globalId}", + span = StaggeredGridItemSpan.FullLine + ) { + LoadMoreFooter( + isLoading = state.isLoadingMore, + errorMsg = null, + isEnd = !state.hasMore, + onRetry = { viewModel.loadMoreModule(moduleUi.globalId) } + ) } } HomepageModuleType.InfiniteGrid -> { - items( + itemsIndexed( state.books, - key = { "inf_grid_${moduleUi.globalId}_${it.bookUrl}" }) { book -> + key = { index, book -> "inf_grid_${moduleUi.globalId}_${book.bookUrl}_$index" }) { index, book -> + val sharedCoverKey = bookCoverSharedElementKey( + book.bookUrl, + "home:${moduleUi.globalId}:infinite:$index" + ) SearchBookGridItem( book = book, shelfState = io.legado.app.domain.model.BookShelfState.NOT_IN_SHELF, - onClick = { viewModel.onBookClick(book) }, + onClick = { viewModel.onBookClick(book, sharedCoverKey) }, sharedTransitionScope = sharedTransitionScope, animatedVisibilityScope = animatedVisibilityScope, - sharedCoverKey = bookCoverSharedElementKey(book.bookUrl) + sharedCoverKey = sharedCoverKey ) } - if (state.hasMore) { - item( - key = "inf_grid_more_${moduleUi.globalId}", - span = StaggeredGridItemSpan.FullLine - ) { - LaunchedEffect(state.books.size) { - viewModel.loadMoreModule(moduleUi.globalId) - } - Box( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - contentAlignment = Alignment.Center - ) { - AppCircularProgressIndicator(modifier = Modifier.size(24.dp)) - } - } + item( + key = "inf_grid_more_${moduleUi.globalId}", + span = StaggeredGridItemSpan.FullLine + ) { + LoadMoreFooter( + isLoading = state.isLoadingMore, + errorMsg = null, + isEnd = !state.hasMore, + onRetry = { viewModel.loadMoreModule(moduleUi.globalId) } + ) } } @@ -509,12 +568,15 @@ private fun ModuleList( ) { GridModule( books = state.books, - onClick = { viewModel.onBookClick(it) }, + onClick = { book, sharedCoverKey -> + viewModel.onBookClick(book, sharedCoverKey) + }, modifier = Modifier.fillMaxWidth(), columns = columns, maxRows = rows, sharedTransitionScope = sharedTransitionScope, animatedVisibilityScope = animatedVisibilityScope, + sharedCoverKeySourceId = "home:${moduleUi.globalId}:grid", ) } } @@ -528,10 +590,13 @@ private fun ModuleList( ) { BannerModule( books = state.books, - onClick = { viewModel.onBookClick(it) }, + onClick = { book, sharedCoverKey -> + viewModel.onBookClick(book, sharedCoverKey) + }, modifier = Modifier.fillMaxWidth(), sharedTransitionScope = sharedTransitionScope, animatedVisibilityScope = animatedVisibilityScope, + sharedCoverKeySourceId = "home:${moduleUi.globalId}:banner", ) } } @@ -543,10 +608,13 @@ private fun ModuleList( ) { RankingModule( books = state.books, - onClick = { viewModel.onBookClick(it) }, + onClick = { book, sharedCoverKey -> + viewModel.onBookClick(book, sharedCoverKey) + }, modifier = Modifier.fillMaxWidth(), sharedTransitionScope = sharedTransitionScope, animatedVisibilityScope = animatedVisibilityScope, + sharedCoverKeySourceId = "home:${moduleUi.globalId}:ranking", ) } } @@ -558,11 +626,14 @@ private fun ModuleList( ) { GridRankingModule( books = state.books, - onClick = { viewModel.onBookClick(it) }, + onClick = { book, sharedCoverKey -> + viewModel.onBookClick(book, sharedCoverKey) + }, modifier = Modifier.fillMaxWidth(), rows = config["layout_rows"]?.toIntOrNull() ?: 4, sharedTransitionScope = sharedTransitionScope, animatedVisibilityScope = animatedVisibilityScope, + sharedCoverKeySourceId = "home:${moduleUi.globalId}:grid-ranking", ) } } @@ -574,10 +645,13 @@ private fun ModuleList( ) { CardModule( books = state.books, - onClick = { viewModel.onBookClick(it) }, + onClick = { book, sharedCoverKey -> + viewModel.onBookClick(book, sharedCoverKey) + }, modifier = Modifier.fillMaxWidth(), sharedTransitionScope = sharedTransitionScope, animatedVisibilityScope = animatedVisibilityScope, + sharedCoverKeySourceId = "home:${moduleUi.globalId}:card", ) } } @@ -596,7 +670,7 @@ private fun ModuleList( @Composable private fun ModuleHeader( title: String, - onNavigate: () -> Unit, + onNavigate: (() -> Unit)? = null, ) { Row( modifier = Modifier @@ -612,9 +686,11 @@ private fun ModuleHeader( overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f), ) - SmallTonalIconButton( - onClick = onNavigate, - imageVector = Icons.AutoMirrored.Filled.ArrowForward - ) + if (onNavigate != null) { + SmallTonalIconButton( + onClick = onNavigate, + imageVector = Icons.AutoMirrored.Filled.ArrowForward + ) + } } -} \ No newline at end of file +} diff --git a/app/src/main/java/io/legado/app/ui/main/homepage/HomepageViewModel.kt b/app/src/main/java/io/legado/app/ui/main/homepage/HomepageViewModel.kt index 8bdf63423..f3f618cec 100644 --- a/app/src/main/java/io/legado/app/ui/main/homepage/HomepageViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/main/homepage/HomepageViewModel.kt @@ -17,6 +17,7 @@ import io.legado.app.domain.usecase.SaveSearchBooksUseCase import io.legado.app.help.source.exploreKinds import io.legado.app.utils.GSON import io.legado.app.utils.fromJsonArray +import io.legado.app.utils.stackTraceStr import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Dispatchers @@ -215,28 +216,10 @@ class HomepageViewModel( } } - // sync: 只处理有 homepageModules 的书源 - viewModelScope.launch { - initModulesSyncFlow.collect { sources -> - sources.forEach { source -> syncModulesFromSource(source) } - } - } - - // cache: 所有启用发现的书源(包括无 homepageModules 的) + // 清理 _pendingUserModules 中已入库的条目 viewModelScope.launch { exploreSourcesFlow.collect { sources -> _bookSourcesCache.value = sources.associateBy { it.bookSourceUrl } - val kindsCache = mutableMapOf>>() - for (source in sources) { - kindsCache[source.bookSourceUrl] = try { - withContext(Dispatchers.IO) { - source.exploreKinds().map { it.title to (it.url ?: "") } - } - } catch (_: Exception) { - emptyList() - } - } - _exploreKindsCache.value = kindsCache } } @@ -367,7 +350,7 @@ class HomepageViewModel( }.onFailure { e -> _moduleContentStates.update { it + (module.id to ModuleLoadState.Error( - e.message ?: "Unknown error" + e.stackTraceStr )) } } @@ -404,7 +387,7 @@ class HomepageViewModel( }.onFailure { e -> _moduleContentStates.update { it + (module.id to ModuleLoadState.Error( - e.message ?: "Unknown error" + e.stackTraceStr )) } } @@ -454,7 +437,7 @@ class HomepageViewModel( HomepageEffect.ShowSnackbar( getApplication().getString( R.string.homepage_load_more_failed, - e.message ?: "" + e.stackTraceStr ) ) ) @@ -477,6 +460,13 @@ class HomepageViewModel( _isRefreshing.value = true loadJobs.values.forEach { it.cancel() } loadJobs.clear() + + // 刷新时同步当前已启用模块所属书源的定义 + val activeSourceUrls = uiState.value.modules.map { it.sourceUrl }.distinct() + activeSourceUrls.forEach { url -> + resolveBookSource(url)?.let { syncModulesFromSource(it) } + } + _moduleContentStates.value = emptyMap() uiState.map { it.modules }.first { modules -> modules.all { it.state !is ModuleLoadState.Loading } @@ -704,6 +694,12 @@ class HomepageViewModel( targetSetId: String? = null ): List { val source = resolveBookSource(sourceUrl) ?: return emptyList() + + // 按需同步:只有进入该源的管理页才同步其 JSON 定义 + viewModelScope.launch { + syncModulesFromSource(source) + } + val json = source.homepageModules ?: return emptyList() val jsonDefs = parseBookSourceModules(source, json) @@ -850,10 +846,19 @@ class HomepageViewModel( } } - fun onBookClick(book: SearchBook) { + fun onBookClick(book: SearchBook, sharedCoverKey: String?) { viewModelScope.launch { saveSearchBooksUseCase.save(book) - _effects.emit(HomepageEffect.NavigateToBookInfo(book.name, book.author, book.bookUrl)) + _effects.emit( + HomepageEffect.NavigateToBookInfo( + name = book.name, + author = book.author, + bookUrl = book.bookUrl, + origin = book.origin, + coverPath = book.coverUrl, + sharedCoverKey = sharedCoverKey + ) + ) } } @@ -909,4 +914,4 @@ private data class HomepageUiFlags( val isRefreshing: Boolean, val isManageMode: Boolean, val isConfigMode: Boolean -) \ No newline at end of file +) diff --git a/app/src/main/java/io/legado/app/ui/main/homepage/modules/BannerModule.kt b/app/src/main/java/io/legado/app/ui/main/homepage/modules/BannerModule.kt index b4cfa2dcd..aa6f1dde3 100644 --- a/app/src/main/java/io/legado/app/ui/main/homepage/modules/BannerModule.kt +++ b/app/src/main/java/io/legado/app/ui/main/homepage/modules/BannerModule.kt @@ -8,7 +8,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -23,10 +23,11 @@ import kotlinx.collections.immutable.ImmutableList @Composable fun BannerModule( books: ImmutableList, - onClick: (SearchBook) -> Unit, + onClick: (SearchBook, String?) -> Unit, modifier: Modifier = Modifier, sharedTransitionScope: SharedTransitionScope? = null, animatedVisibilityScope: AnimatedVisibilityScope? = null, + sharedCoverKeySourceId: String? = null, ) { if (books.isEmpty()) return @@ -38,7 +39,11 @@ fun BannerModule( .fadingEdge(lazyListState, gradientWidth = 16.dp), horizontalArrangement = Arrangement.spacedBy(12.dp), ) { - items(books) { book -> + itemsIndexed(books, key = { index, book -> "${book.bookUrl}:$index" }) { index, book -> + val sharedCoverKey = bookCoverSharedElementKey( + book.bookUrl, + sharedCoverKeySourceId?.let { "$it:$index" } + ) CoilBookCover( name = book.name, author = book.author, @@ -47,10 +52,10 @@ fun BannerModule( sourceOrigin = book.origin, modifier = Modifier .width(96.dp) - .clickable { onClick(book) }, + .clickable { onClick(book, sharedCoverKey) }, sharedTransitionScope = sharedTransitionScope, animatedVisibilityScope = animatedVisibilityScope, - sharedCoverKey = bookCoverSharedElementKey(book.bookUrl) + sharedCoverKey = sharedCoverKey ) } } diff --git a/app/src/main/java/io/legado/app/ui/main/homepage/modules/ButtonGroupModule.kt b/app/src/main/java/io/legado/app/ui/main/homepage/modules/ButtonGroupModule.kt index 940a3faaf..392fe4e9d 100644 --- a/app/src/main/java/io/legado/app/ui/main/homepage/modules/ButtonGroupModule.kt +++ b/app/src/main/java/io/legado/app/ui/main/homepage/modules/ButtonGroupModule.kt @@ -3,6 +3,7 @@ package io.legado.app.ui.main.homepage.modules import androidx.appcompat.app.AppCompatActivity import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -12,12 +13,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size 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.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -26,14 +22,14 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import io.legado.app.data.entities.rule.ExploreKind import io.legado.app.domain.usecase.ExploreKindUiUseCase -import io.legado.app.help.source.getExploreInfoMap import io.legado.app.ui.main.homepage.HomepageViewModel import io.legado.app.ui.theme.LegadoTheme +import io.legado.app.ui.theme.ThemeResolver import io.legado.app.ui.widget.components.card.GlassCard +import io.legado.app.ui.widget.components.explore.ExploreKindMultiTypeItem import io.legado.app.ui.widget.components.image.sourceIcon.SourceIcon import io.legado.app.ui.widget.components.text.AppText import io.legado.app.utils.GSON -import kotlinx.coroutines.launch import org.koin.compose.koinInject @Composable @@ -51,10 +47,7 @@ fun ButtonGroupModule( val context = LocalContext.current val activity = context as? AppCompatActivity val useCase: ExploreKindUiUseCase = koinInject() - val scope = rememberCoroutineScope() - val infoMap = remember(sourceUrl) { - sourceUrl.takeIf { it.isNotBlank() }?.let { getExploreInfoMap(it) } - } + val isMiuix = ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine) // 解析图标映射表和默认图标 val (iconMap, defaultIcon) = remember(layoutConfig) { @@ -90,74 +83,76 @@ fun ButtonGroupModule( horizontalArrangement = Arrangement.spacedBy(8.dp), ) { rowKinds.forEach { kind -> - var displayName by remember(kind.title) { mutableStateOf(kind.title) } - - LaunchedEffect(kind, sourceUrl, infoMap) { - displayName = useCase.resolveDisplayName(kind, sourceUrl, infoMap) - } - val buttonIcon = iconMap[kind.title] ?: defaultIcon val hasIcon = !buttonIcon.isNullOrBlank() - GlassCard( - onClick = { - when (kind.type) { - ExploreKind.Type.url -> { - kind.url?.takeIf { it.isNotBlank() }?.let { - viewModel.onKindUrlClick(sourceUrl, it, kind.title) - } - } + ExploreKindMultiTypeItem( + kind = kind, + sourceUrl = sourceUrl, + activity = activity, + onOpenUrl = { url -> + viewModel.onKindUrlClick(sourceUrl, url, kind.title) + }, + onRefreshKinds = { + viewModel.refreshButtonGroup(globalId) + }, + useCase = useCase, + isMiuix = isMiuix, + modifier = Modifier.weight(1f), + content = { displayName, isSelected, onClick, trailingIcon -> + GlassCard( + onClick = onClick, + cornerRadius = 8.dp, + containerColor = if (isSelected) LegadoTheme.colorScheme.primaryContainer else LegadoTheme.colorScheme.surfaceContainerLow, + modifier = Modifier.fillMaxWidth() + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = Modifier + .fillMaxSize() + .padding(vertical = 8.dp, horizontal = 4.dp) + ) { + if (hasIcon) { + SourceIcon( + path = buttonIcon!!, + modifier = Modifier.size(20.dp), + placeholderIcon = { - ExploreKind.Type.button -> { - scope.launch { - useCase.executeAction( - action = kind.action, - title = kind.title, - sourceUrl = sourceUrl, - infoMap = infoMap, - activity = activity, - onRefreshKinds = { - viewModel.refreshButtonGroup(globalId) } ) + Spacer(modifier = Modifier.height(6.dp)) + } + + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + AppText( + text = displayName, + style = LegadoTheme.typography.labelSmallEmphasized, + textAlign = TextAlign.Center, + maxLines = 1, + overflow = TextOverflow.Clip, + modifier = Modifier + .padding(horizontal = 4.dp) + .basicMarquee() + ) + + if (trailingIcon != null) { + Box( + modifier = Modifier + .align(Alignment.TopEnd) + .padding(end = 2.dp) + ) { + trailingIcon() + } + } } } } - }, - cornerRadius = 8.dp, - containerColor = LegadoTheme.colorScheme.surfaceContainerLow, - modifier = Modifier.weight(1f) - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - modifier = Modifier - .fillMaxSize() - .padding(vertical = 8.dp, horizontal = 4.dp) - ) { - if (hasIcon) { - SourceIcon( - path = buttonIcon, - modifier = Modifier.size(20.dp), - placeholderIcon = { - - } - ) - Spacer(modifier = Modifier.height(6.dp)) - } - - AppText( - text = displayName, - style = LegadoTheme.typography.labelSmallEmphasized, - textAlign = TextAlign.Center, - maxLines = 1, - overflow = TextOverflow.Clip, - modifier = Modifier - .padding(horizontal = 4.dp) - .basicMarquee() - ) } - } + ) } if (rowKinds.size < actualColumns) { diff --git a/app/src/main/java/io/legado/app/ui/main/homepage/modules/CardModule.kt b/app/src/main/java/io/legado/app/ui/main/homepage/modules/CardModule.kt index 861e4c188..a4c4365da 100644 --- a/app/src/main/java/io/legado/app/ui/main/homepage/modules/CardModule.kt +++ b/app/src/main/java/io/legado/app/ui/main/homepage/modules/CardModule.kt @@ -12,7 +12,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable @@ -35,10 +35,11 @@ import kotlinx.collections.immutable.ImmutableList @Composable fun CardModule( books: ImmutableList, - onClick: (SearchBook) -> Unit, + onClick: (SearchBook, String?) -> Unit, modifier: Modifier = Modifier, sharedTransitionScope: SharedTransitionScope? = null, animatedVisibilityScope: AnimatedVisibilityScope? = null, + sharedCoverKeySourceId: String? = null, ) { if (books.isEmpty()) return val lazyListState = rememberLazyListState() @@ -49,13 +50,17 @@ fun CardModule( .fadingEdge(lazyListState, gradientWidth = 8.dp), horizontalArrangement = Arrangement.spacedBy(12.dp), ) { - items(books, key = { it.bookUrl }) { book -> + itemsIndexed(books, key = { index, book -> "${book.bookUrl}:$index" }) { index, book -> + val sharedCoverKey = bookCoverSharedElementKey( + book.bookUrl, + sharedCoverKeySourceId?.let { "$it:$index" } + ) Column( modifier = Modifier .width(120.dp) .clip(RoundedCornerShape(16.dp)) .background(LegadoTheme.colorScheme.surfaceContainerLow) - .clickable { onClick(book) } + .clickable { onClick(book, sharedCoverKey) } ) { CoilBookCover( name = book.name, @@ -67,19 +72,20 @@ fun CardModule( .wrapContentWidth(), sharedTransitionScope = sharedTransitionScope, animatedVisibilityScope = animatedVisibilityScope, - sharedCoverKey = bookCoverSharedElementKey(book.bookUrl) + sharedCoverKey = sharedCoverKey ) AppText( text = book.name, style = LegadoTheme.typography.labelLargeEmphasized, maxLines = 2, + minLines = 2, overflow = TextOverflow.Ellipsis, modifier = Modifier.padding( start = 8.dp, end = 8.dp, top = 8.dp, - bottom = 2.dp + bottom = 8.dp ), ) @@ -88,7 +94,7 @@ fun CardModule( if (intro != null) { AppText( text = intro, - style = LegadoTheme.typography.bodySmall, + style = LegadoTheme.typography.labelSmallEmphasized, color = LegadoTheme.colorScheme.onSurfaceVariant, maxLines = 2, overflow = TextOverflow.Ellipsis, diff --git a/app/src/main/java/io/legado/app/ui/main/homepage/modules/GridModule.kt b/app/src/main/java/io/legado/app/ui/main/homepage/modules/GridModule.kt index 55004f808..7423b3f87 100644 --- a/app/src/main/java/io/legado/app/ui/main/homepage/modules/GridModule.kt +++ b/app/src/main/java/io/legado/app/ui/main/homepage/modules/GridModule.kt @@ -21,12 +21,13 @@ import kotlinx.collections.immutable.ImmutableList @Composable fun GridModule( books: ImmutableList, - onClick: (SearchBook) -> Unit, + onClick: (SearchBook, String?) -> Unit, modifier: Modifier = Modifier, columns: Int = 3, maxRows: Int? = null, sharedTransitionScope: SharedTransitionScope? = null, animatedVisibilityScope: AnimatedVisibilityScope? = null, + sharedCoverKeySourceId: String? = null, ) { if (books.isEmpty()) return var rows = books.toList().chunked(columns) @@ -37,20 +38,25 @@ fun GridModule( modifier = modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(4.dp), ) { - for (row in rows) { + for ((rowIndex, row) in rows.withIndex()) { Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(12.dp), ) { - for (book in row) { + for ((columnIndex, book) in row.withIndex()) { + val itemIndex = rowIndex * columns + columnIndex + val sharedCoverKey = bookCoverSharedElementKey( + book.bookUrl, + sharedCoverKeySourceId?.let { "$it:$itemIndex" } + ) SearchBookGridItem( book = book, shelfState = BookShelfState.NOT_IN_SHELF, - onClick = { onClick(book) }, + onClick = { onClick(book, sharedCoverKey) }, modifier = Modifier.weight(1f), sharedTransitionScope = sharedTransitionScope, animatedVisibilityScope = animatedVisibilityScope, - sharedCoverKey = bookCoverSharedElementKey(book.bookUrl) + sharedCoverKey = sharedCoverKey ) } repeat(columns - row.size) { Spacer(Modifier.weight(1f)) } diff --git a/app/src/main/java/io/legado/app/ui/main/homepage/modules/GridRankingModule.kt b/app/src/main/java/io/legado/app/ui/main/homepage/modules/GridRankingModule.kt index af567717b..098423c1f 100644 --- a/app/src/main/java/io/legado/app/ui/main/homepage/modules/GridRankingModule.kt +++ b/app/src/main/java/io/legado/app/ui/main/homepage/modules/GridRankingModule.kt @@ -37,11 +37,12 @@ import kotlinx.collections.immutable.ImmutableList @Composable fun GridRankingModule( books: ImmutableList, - onClick: (SearchBook) -> Unit, + onClick: (SearchBook, String?) -> Unit, modifier: Modifier = Modifier, rows: Int = 4, sharedTransitionScope: SharedTransitionScope? = null, animatedVisibilityScope: AnimatedVisibilityScope? = null, + sharedCoverKeySourceId: String? = null, ) { if (books.isEmpty()) return // 限制最多显示 20 项 @@ -70,13 +71,19 @@ fun GridRankingModule( .fillMaxWidth() .padding(vertical = 12.dp, horizontal = 12.dp) ) { - for (book in page) { + for ((rowIndex, book) in page.withIndex()) { + val itemIndex = pageIndex * rows + rowIndex + val sharedCoverKey = bookCoverSharedElementKey( + book.bookUrl, + sharedCoverKeySourceId?.let { "$it:$itemIndex" } + ) GridRankingItem( rank = pages.flatten().indexOf(book) + 1, book = book, - onClick = { onClick(book) }, + onClick = { onClick(book, sharedCoverKey) }, sharedTransitionScope = sharedTransitionScope, animatedVisibilityScope = animatedVisibilityScope, + sharedCoverKey = sharedCoverKey, ) } // 占位逻辑 @@ -96,6 +103,7 @@ private fun GridRankingItem( onClick: () -> Unit, sharedTransitionScope: SharedTransitionScope? = null, animatedVisibilityScope: AnimatedVisibilityScope? = null, + sharedCoverKey: String? = null, ) { Row( modifier = Modifier @@ -114,7 +122,7 @@ private fun GridRankingItem( modifier = Modifier.width(48.dp), sharedTransitionScope = sharedTransitionScope, animatedVisibilityScope = animatedVisibilityScope, - sharedCoverKey = bookCoverSharedElementKey(book.bookUrl) + sharedCoverKey = sharedCoverKey ) // 2. 排名 diff --git a/app/src/main/java/io/legado/app/ui/main/homepage/modules/RankingModule.kt b/app/src/main/java/io/legado/app/ui/main/homepage/modules/RankingModule.kt index 33d47075d..9c99cb0e2 100644 --- a/app/src/main/java/io/legado/app/ui/main/homepage/modules/RankingModule.kt +++ b/app/src/main/java/io/legado/app/ui/main/homepage/modules/RankingModule.kt @@ -45,10 +45,11 @@ private const val MAX_COUNT = 20 @Composable fun RankingModule( books: ImmutableList, - onClick: (SearchBook) -> Unit, + onClick: (SearchBook, String?) -> Unit, modifier: Modifier = Modifier, sharedTransitionScope: SharedTransitionScope? = null, animatedVisibilityScope: AnimatedVisibilityScope? = null, + sharedCoverKeySourceId: String? = null, ) { var visibleCount by rememberSaveable { mutableIntStateOf(INITIAL_COUNT) } val displayBooks = books.take(visibleCount) @@ -72,6 +73,10 @@ fun RankingModule( onClick = onClick, sharedTransitionScope = sharedTransitionScope, animatedVisibilityScope = animatedVisibilityScope, + sharedCoverKey = bookCoverSharedElementKey( + book.bookUrl, + sharedCoverKeySourceId?.let { "$it:$index" } + ) ) } @@ -114,14 +119,15 @@ fun RankingModule( private fun RankingItem( rank: Int, book: SearchBook, - onClick: (SearchBook) -> Unit, + onClick: (SearchBook, String?) -> Unit, sharedTransitionScope: SharedTransitionScope? = null, animatedVisibilityScope: AnimatedVisibilityScope? = null, + sharedCoverKey: String? = null, ) { Row( modifier = Modifier .fillMaxWidth() - .clickable { onClick(book) } + .clickable { onClick(book, sharedCoverKey) } .padding(vertical = 4.dp, horizontal = 4.dp), verticalAlignment = Alignment.CenterVertically, ) { @@ -144,7 +150,7 @@ private fun RankingItem( modifier = Modifier.weight(1f), sharedTransitionScope = sharedTransitionScope, animatedVisibilityScope = animatedVisibilityScope, - sharedCoverKey = bookCoverSharedElementKey(book.bookUrl) + sharedCoverKey = sharedCoverKey ) } } diff --git a/app/src/main/java/io/legado/app/ui/main/homepage/modules/WaterfallModule.kt b/app/src/main/java/io/legado/app/ui/main/homepage/modules/WaterfallModule.kt index 5601ee729..5b27a1061 100644 --- a/app/src/main/java/io/legado/app/ui/main/homepage/modules/WaterfallModule.kt +++ b/app/src/main/java/io/legado/app/ui/main/homepage/modules/WaterfallModule.kt @@ -17,7 +17,6 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import io.legado.app.data.entities.SearchBook -import io.legado.app.ui.main.bookCoverSharedElementKey import io.legado.app.ui.theme.LegadoTheme import io.legado.app.ui.widget.components.book.SearchBookTagChip import io.legado.app.ui.widget.components.card.GlassCard @@ -36,6 +35,7 @@ fun WaterfallItem( modifier: Modifier = Modifier, sharedTransitionScope: SharedTransitionScope? = null, animatedVisibilityScope: AnimatedVisibilityScope? = null, + sharedCoverKey: String? = null, ) { GlassCard( containerColor = LegadoTheme.colorScheme.surfaceContainerLow @@ -55,7 +55,7 @@ fun WaterfallItem( .fillMaxWidth(), sharedTransitionScope = sharedTransitionScope, animatedVisibilityScope = animatedVisibilityScope, - sharedCoverKey = bookCoverSharedElementKey(book.bookUrl) + sharedCoverKey = sharedCoverKey ) Spacer(modifier = Modifier.height(8.dp)) diff --git a/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesCompose.kt b/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesCompose.kt index ba1ef0be5..4e41f5a76 100644 --- a/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesCompose.kt +++ b/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesCompose.kt @@ -1,7 +1,6 @@ package io.legado.app.ui.rss.article import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -59,7 +58,7 @@ import io.legado.app.data.entities.RssSource import io.legado.app.ui.theme.LegadoTheme import io.legado.app.ui.theme.adaptiveContentPadding import io.legado.app.ui.widget.components.AppPullToRefresh -import io.legado.app.ui.widget.components.EmptyMessage +import io.legado.app.ui.widget.components.LoadMoreFooter import io.legado.app.ui.widget.components.card.GlassCard import io.legado.app.ui.widget.components.image.cover.buildCoverImageRequest import io.legado.app.utils.toastOnUi @@ -163,7 +162,9 @@ fun RssArticlesPage( } item { LoadMoreFooter( - state = loadState, + isLoading = loadState.isRefreshing || loadState.isLoadingMore, + errorMsg = loadState.errorMessage, + isEnd = !loadState.hasMore, onRetry = { rssSource?.let(viewModel::loadMore) } ) } @@ -198,7 +199,9 @@ fun RssArticlesPage( } item(span = { GridItemSpan(maxLineSpan) }) { LoadMoreFooter( - state = loadState, + isLoading = loadState.isRefreshing || loadState.isLoadingMore, + errorMsg = loadState.errorMessage, + isEnd = !loadState.hasMore, onRetry = { rssSource?.let(viewModel::loadMore) } ) } @@ -233,7 +236,9 @@ fun RssArticlesPage( } item(span = StaggeredGridItemSpan.FullLine) { LoadMoreFooter( - state = loadState, + isLoading = loadState.isRefreshing || loadState.isLoadingMore, + errorMsg = loadState.errorMessage, + isEnd = !loadState.hasMore, onRetry = { rssSource?.let(viewModel::loadMore) } ) } @@ -297,35 +302,6 @@ private fun StaggeredLoadMoreDetector( } } -@Composable -private fun LoadMoreFooter( - state: RssArticlesLoadState, - onRetry: () -> Unit -) { - val text = when { - state.isRefreshing || state.isLoadingMore -> "加载中..." - !state.hasMore -> "没有更多了" - state.errorMessage != null -> "加载失败,点击重试" - else -> "上拉加载更多" - } - val contentModifier = if (state.errorMessage != null) { - Modifier.clickable(onClick = onRetry) - } else { - Modifier - } - Box( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 12.dp), - contentAlignment = Alignment.Center - ) { - EmptyMessage( - message = text, - isLoading = state.isRefreshing || state.isLoadingMore, - modifier = contentModifier - ) - } -} @Composable private fun RssArticleItem( 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 d27ecd7e0..2ce502872 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 @@ -2,6 +2,7 @@ package io.legado.app.ui.theme import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Typography +import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.sp import top.yukonga.miuix.kmp.theme.TextStyles @@ -66,7 +67,7 @@ fun Typography.toLegadoTypography(): LegadoTypography { ) } -fun LegadoTypography.withFont(fontFamily: androidx.compose.ui.text.font.FontFamily?): LegadoTypography { +fun LegadoTypography.withFont(fontFamily: FontFamily?): LegadoTypography { if (fontFamily == null) return this return copy( headlineLarge = headlineLarge.copy(fontFamily = fontFamily), diff --git a/app/src/main/java/io/legado/app/ui/widget/components/AppFloatingActionButton.kt b/app/src/main/java/io/legado/app/ui/widget/components/AppFloatingActionButton.kt index bdf6dd409..f0d76b6fa 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/AppFloatingActionButton.kt +++ b/app/src/main/java/io/legado/app/ui/widget/components/AppFloatingActionButton.kt @@ -71,7 +71,7 @@ fun AppFloatingActionButton( MiuixIcon( imageVector = icon, contentDescription = tooltipText, - tint = Color.White + tint = containerColor ) } else { Icon( @@ -89,7 +89,8 @@ fun AppFloatingActionButton( MiuixFloatingActionButton( onClick = onClick, modifier = modifier, - content = fabContent + content = fabContent, + containerColor = LegadoTheme.colorScheme.surfaceContainer ) } else { if (tooltipText != null) { diff --git a/app/src/main/java/io/legado/app/ui/widget/components/JsonRawEditor.kt b/app/src/main/java/io/legado/app/ui/widget/components/JsonRawEditor.kt index 4c7e6959d..3f039e9ba 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/JsonRawEditor.kt +++ b/app/src/main/java/io/legado/app/ui/widget/components/JsonRawEditor.kt @@ -8,18 +8,13 @@ import androidx.compose.foundation.layout.heightIn import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.AutoFixHigh import androidx.compose.material.icons.filled.Compress -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.TextField -import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.dp import com.google.gson.GsonBuilder import com.google.gson.JsonParser +import io.legado.app.ui.theme.LegadoTheme import io.legado.app.ui.widget.components.button.SmallIconButton import io.legado.app.ui.widget.components.text.AppText import io.legado.app.utils.GSON @@ -39,8 +34,7 @@ fun JsonRawEditor( ) { AppText( text = label, - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.primary + style = LegadoTheme.typography.labelMediumEmphasized ) Row { SmallIconButton( @@ -67,21 +61,13 @@ fun JsonRawEditor( } } - TextField( + AppTextField( value = value, onValueChange = onValueChange, modifier = Modifier .fillMaxWidth() .heightIn(min = 150.dp, max = 400.dp), - textStyle = TextStyle( - fontFamily = FontFamily.Monospace, - fontSize = MaterialTheme.typography.bodySmall.fontSize - ), - colors = TextFieldDefaults.colors( - focusedContainerColor = Color.Transparent, - unfocusedContainerColor = Color.Transparent, - disabledContainerColor = Color.Transparent, - ), + backgroundColor = LegadoTheme.colorScheme.onSheetContent, maxLines = 1000 ) } diff --git a/app/src/main/java/io/legado/app/ui/widget/components/LoadMoreFooter.kt b/app/src/main/java/io/legado/app/ui/widget/components/LoadMoreFooter.kt new file mode 100644 index 000000000..e457690a7 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/components/LoadMoreFooter.kt @@ -0,0 +1,251 @@ +package io.legado.app.ui.widget.components + +import androidx.compose.animation.AnimatedContent +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material.icons.outlined.Info +import androidx.compose.material3.HorizontalDivider +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.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import io.legado.app.ui.theme.LegadoTheme +import io.legado.app.ui.theme.adaptiveHorizontalPadding +import io.legado.app.ui.widget.components.alert.AppAlertDialog +import io.legado.app.ui.widget.components.button.AnimatedTextButton +import io.legado.app.ui.widget.components.card.GlassCard +import io.legado.app.ui.widget.components.icon.AppIcon +import io.legado.app.ui.widget.components.progressIndicator.AppContainedLoadingIndicator +import io.legado.app.ui.widget.components.text.AppText +import io.legado.app.utils.sendToClip + +@Composable +fun LoadMoreFooter( + isLoading: Boolean, + errorMsg: String?, + isEnd: Boolean, + onRetry: () -> Unit +) { + val context = LocalContext.current + var showFullError by remember { mutableStateOf(null) } + + LaunchedEffect(isLoading, errorMsg, isEnd) { + if (!isLoading && errorMsg == null && !isEnd) { + onRetry() + } + } + + AppAlertDialog( + data = showFullError, + onDismissRequest = { showFullError = null }, + title = "错误详情", + textProvider = { this }, + confirmText = "复制", + onConfirm = { error -> + context.sendToClip(error) + showFullError = null + }, + dismissText = "关闭", + onDismiss = { showFullError = null } + ) + + Box( + modifier = Modifier + .fillMaxWidth() + .adaptiveHorizontalPadding(vertical = 8.dp), + contentAlignment = Alignment.Center + ) { + + AnimatedContent( + targetState = Triple(isLoading, errorMsg, isEnd), + label = "LoadMoreFooter" + ) { (loading, error, end) -> + + when { + error != null -> { + + GlassCard( + onClick = { showFullError = error }, + containerColor = LegadoTheme.colorScheme.errorContainer.copy(alpha = 0.6f), + ) { + Column( + modifier = Modifier.fillMaxWidth() + ) { + + // 信息区域 + Row( + modifier = Modifier + .fillMaxWidth() + .padding( + all = 16.dp + ), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + + AppIcon( + imageVector = Icons.Outlined.Info, + contentDescription = null, + tint = LegadoTheme.colorScheme.error + ) + + AppText( + text = error, + color = LegadoTheme.colorScheme.error, + style = LegadoTheme.typography.bodySmall, + modifier = Modifier.weight(1f), + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } + + HorizontalDivider( + color = LegadoTheme.colorScheme.outlineVariant.copy(alpha = 0.3f) + ) + + // 操作区域 + Box( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onRetry) + .padding(vertical = 10.dp), + contentAlignment = Alignment.Center + ) { + + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + + AppIcon( + imageVector = Icons.Default.Refresh, + contentDescription = null, + tint = LegadoTheme.colorScheme.error + ) + + AppText( + text = "重新加载", + color = LegadoTheme.colorScheme.error, + style = LegadoTheme.typography.labelMedium + ) + } + } + } + } + } + + loading -> { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + + AppContainedLoadingIndicator() + + AppText( + text = "正在加载更多内容…", + color = LegadoTheme.colorScheme.outline, + style = LegadoTheme.typography.bodySmall + ) + } + } + + end -> { + GlassCard( + modifier = Modifier + .fillMaxWidth(), + containerColor = LegadoTheme.colorScheme.surfaceContainer, + ) { + Column( + modifier = Modifier.fillMaxWidth() + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding( + all = 16.dp + ), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + + AppIcon( + imageVector = Icons.Outlined.Info, + contentDescription = null, + tint = LegadoTheme.colorScheme.onSurface + ) + + AppText( + text = "已经到底了~", + color = LegadoTheme.colorScheme.onSurface, + style = LegadoTheme.typography.bodySmall, + modifier = Modifier.weight(1f), + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } + } + } + } + + else -> { + GlassCard( + modifier = Modifier + .fillMaxWidth(), + containerColor = LegadoTheme.colorScheme.surfaceContainer, + onClick = onRetry + ) { + Column( + modifier = Modifier.fillMaxWidth() + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding( + all = 16.dp + ), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + + AppIcon( + imageVector = Icons.Outlined.Info, + contentDescription = null, + tint = LegadoTheme.colorScheme.onSurface + ) + + AppText( + text = "加载更多", + color = LegadoTheme.colorScheme.onSurface, + style = LegadoTheme.typography.bodySmall, + modifier = Modifier.weight(1f), + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } + } + } + AnimatedTextButton( + isLoading = false, + onClick = onRetry, + text = "尝试加载更多" + ) + } + } + } + } +} 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 b0d049a72..7bb04b265 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 @@ -5,6 +5,9 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialogDefaults import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @@ -98,12 +101,16 @@ fun AppAlertDialog( tonalElevation = AlertDialogDefaults.TonalElevation, title = title?.let { { Text(text = it) } }, text = { - Column { + Column( + modifier = Modifier.verticalScroll(rememberScrollState()) + ) { if (text != null) { - Text( - text = text, - modifier = Modifier.padding(bottom = if (content != null) 16.dp else 0.dp) - ) + SelectionContainer { + Text( + text = text, + modifier = Modifier.padding(bottom = if (content != null) 16.dp else 0.dp) + ) + } } if (content != null) { content() @@ -161,16 +168,17 @@ fun AppAlertDialog( val currentData = cachedData if (currentData != null) { val currentText = text ?: textProvider?.invoke(currentData) - var cachedText by remember { mutableStateOf(currentText) } + var lastValidText by remember { mutableStateOf(currentText) } + if (currentText != null) { - cachedText = currentText + lastValidText = currentText } AppAlertDialog( show = data != null, onDismissRequest = onDismissRequest, title = title, - text = currentText ?: cachedText, + text = currentText ?: lastValidText, modifier = modifier, confirmText = confirmText, onConfirm = onConfirm?.let { { it(currentData) } }, 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 3aa34c830..3fd6a42e3 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 @@ -82,7 +82,7 @@ fun SmallTextButton( @Composable fun SmallTonalTextButton( text: String? = null, - imageVector: ImageVector, + imageVector: ImageVector? = null, modifier: Modifier = Modifier, onClick: () -> Unit ) { @@ -102,11 +102,13 @@ fun SmallTonalTextButton( horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally), verticalAlignment = Alignment.CenterVertically ) { - MiuixIcon( - imageVector = imageVector, - contentDescription = null, - modifier = Modifier.size(16.dp) - ) + if (imageVector != null) { + MiuixIcon( + imageVector = imageVector, + contentDescription = null, + modifier = Modifier.size(16.dp) + ) + } if (text != null) { AppText( text = text, @@ -121,11 +123,13 @@ fun SmallTonalTextButton( modifier = modifier, contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp) ) { - Icon( - imageVector = imageVector, - contentDescription = null, - modifier = Modifier.size(16.dp) - ) + if (imageVector != null) { + Icon( + imageVector = imageVector, + contentDescription = null, + modifier = Modifier.size(16.dp) + ) + } Spacer(Modifier.width(4.dp)) if (text != null) { AppText( diff --git a/app/src/main/java/io/legado/app/ui/widget/components/explore/ExploreKindItem.kt b/app/src/main/java/io/legado/app/ui/widget/components/explore/ExploreKindItem.kt index 449040ab1..4e41e5346 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/explore/ExploreKindItem.kt +++ b/app/src/main/java/io/legado/app/ui/widget/components/explore/ExploreKindItem.kt @@ -3,10 +3,8 @@ package io.legado.app.ui.widget.components.explore import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material3.CardDefaults import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.LocalMinimumInteractiveComponentSize -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Alignment @@ -16,11 +14,9 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import io.legado.app.data.entities.rule.ExploreKind -import io.legado.app.ui.config.themeConfig.ThemeConfig 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 -import top.yukonga.miuix.kmp.theme.MiuixTheme @Composable fun ExploreKindItem( @@ -31,6 +27,7 @@ fun ExploreKindItem( isMiuix: Boolean, backgroundColor: androidx.compose.ui.graphics.Color = LegadoTheme.colorScheme.surfaceContainer, displayText: String = kind.title, + isSelected: Boolean = false, trailingIcon: (@Composable () -> Unit)? = null ) { CompositionLocalProvider( @@ -38,31 +35,45 @@ fun ExploreKindItem( ) { val cornerRadius = 12.dp + val containerColor = if (isSelected) { + LegadoTheme.colorScheme.primaryContainer + } else { + backgroundColor + } + val contentColor = if (isSelected) { + LegadoTheme.colorScheme.onPrimaryContainer + } else if (isClickable) { + LegadoTheme.colorScheme.onSurface + } else { + LegadoTheme.colorScheme.primary + } if (isClickable) { GlassCard( onClick = onClick, cornerRadius = cornerRadius, - containerColor = backgroundColor, - contentColor = LegadoTheme.colorScheme.onSurface, + containerColor = containerColor, + contentColor = contentColor, modifier = modifier, ) { KindText( text = displayText, isClickable = true, + contentColor = contentColor, trailingIcon = trailingIcon ) } } else { GlassCard( cornerRadius = cornerRadius, - containerColor = backgroundColor, - contentColor = LegadoTheme.colorScheme.primary, + containerColor = containerColor, + contentColor = contentColor, modifier = modifier, ) { KindText( text = displayText, isClickable = false, + contentColor = contentColor, trailingIcon = trailingIcon ) } @@ -75,6 +86,7 @@ fun ExploreKindItem( private fun KindText( text: String, isClickable: Boolean, + contentColor: androidx.compose.ui.graphics.Color = LegadoTheme.colorScheme.onSurface, trailingIcon: (@Composable () -> Unit)? = null ) { Box( @@ -84,7 +96,7 @@ private fun KindText( ) { AppText( text = text, - color = if (isClickable) LegadoTheme.colorScheme.onSurface else LegadoTheme.colorScheme.primary, + color = contentColor, modifier = Modifier .fillMaxWidth() .padding(end = if (trailingIcon == null) 0.dp else 18.dp), diff --git a/app/src/main/java/io/legado/app/ui/widget/components/explore/ExploreKindItemState.kt b/app/src/main/java/io/legado/app/ui/widget/components/explore/ExploreKindItemState.kt new file mode 100644 index 000000000..9f31909d6 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/components/explore/ExploreKindItemState.kt @@ -0,0 +1,85 @@ +package io.legado.app.ui.widget.components.explore + +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.Stable +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 io.legado.app.data.entities.rule.ExploreKind +import io.legado.app.domain.usecase.ExploreKindUiUseCase +import io.legado.app.help.source.getExploreInfoMap +import io.legado.app.utils.InfoMap +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.launch + +/** + * 封装 ExploreKind 的业务状态与交互逻辑 + */ +@Stable +class ExploreKindItemState( + val kind: ExploreKind, + val sourceUrl: String?, + private val useCase: ExploreKindUiUseCase?, + private val scope: CoroutineScope, + private val activity: AppCompatActivity?, + private val onRefreshKinds: () -> Unit +) { + val infoMap: InfoMap? = if (useCase == null) null else sourceUrl?.takeIf { it.isNotBlank() } + ?.let(::getExploreInfoMap) + var displayName by mutableStateOf(kind.title) + internal set + + fun executeAction(action: String?) { + if (action.isNullOrBlank()) return + val useCase = useCase ?: return + scope.launch(IO) { + useCase.executeAction( + action = action, + title = kind.title, + sourceUrl = sourceUrl, + infoMap = infoMap, + activity = activity, + onRefreshKinds = onRefreshKinds + ) + } + } + + fun updateValue(value: String, onValueChange: ((String) -> Unit)?) { + if (onValueChange != null) { + onValueChange(value) + } else { + infoMap?.let { + it[kind.title] = value + it.saveNow() + } + } + } + + @Composable + fun ResolveDisplayName(override: String?) { + LaunchedEffect(override, sourceUrl, kind.title, kind.viewName, useCase) { + displayName = override + ?: useCase?.resolveDisplayName(kind, sourceUrl, infoMap) + ?: kind.title + } + } +} + +@Composable +fun rememberExploreKindItemState( + kind: ExploreKind, + sourceUrl: String?, + useCase: ExploreKindUiUseCase?, + activity: AppCompatActivity?, + onRefreshKinds: () -> Unit +): ExploreKindItemState { + val scope = rememberCoroutineScope() + return remember(kind, sourceUrl, useCase, activity) { + ExploreKindItemState(kind, sourceUrl, useCase, scope, activity, onRefreshKinds) + } +} diff --git a/app/src/main/java/io/legado/app/ui/widget/components/explore/ExploreKindMultiTypeItem.kt b/app/src/main/java/io/legado/app/ui/widget/components/explore/ExploreKindMultiTypeItem.kt index 5e7033038..9efed4f4a 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/explore/ExploreKindMultiTypeItem.kt +++ b/app/src/main/java/io/legado/app/ui/widget/components/explore/ExploreKindMultiTypeItem.kt @@ -1,14 +1,9 @@ package io.legado.app.ui.widget.components.explore import androidx.appcompat.app.AppCompatActivity -import androidx.compose.foundation.background -import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.BasicTextField import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight import androidx.compose.material.icons.filled.Refresh @@ -22,24 +17,18 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.platform.LocalLayoutDirection -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import io.legado.app.data.entities.rule.ExploreKind import io.legado.app.domain.usecase.ExploreKindUiUseCase -import io.legado.app.help.source.getExploreInfoMap import io.legado.app.ui.theme.LegadoTheme 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.text.AppText import io.legado.app.ui.widget.dialog.TextDialog import io.legado.app.utils.showDialogFragment -import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -56,295 +45,362 @@ fun ExploreKindMultiTypeItem( isMiuix: Boolean, displayNameOverride: String? = null, valueOverride: String? = null, + isSelected: Boolean = false, onValueChange: ((String) -> Unit)? = null, onRunAction: (() -> Unit)? = null, - useCase: ExploreKindUiUseCase? = null + useCase: ExploreKindUiUseCase? = null, + onClick: (() -> Unit)? = null, + content: (@Composable (displayName: String, isSelected: Boolean, onClick: () -> Unit, trailingIcon: @Composable (() -> Unit)?) -> Unit)? = null ) { - val scope = rememberCoroutineScope() - val infoMap = remember(sourceUrl, useCase) { - if (useCase == null) null else sourceUrl?.takeIf { it.isNotBlank() }?.let(::getExploreInfoMap) - } - var displayName by remember(sourceUrl, kind.title, kind.viewName) { mutableStateOf(kind.title) } + val state = rememberExploreKindItemState(kind, sourceUrl, useCase, activity, onRefreshKinds) + state.ResolveDisplayName(displayNameOverride) - LaunchedEffect(displayNameOverride, sourceUrl, kind.title, kind.viewName, useCase) { - displayName = displayNameOverride - ?: useCase?.resolveDisplayName(kind, sourceUrl, infoMap) - ?: kind.title - } + val trailingIcon = rememberTrailingIcon(kind.type, isSelected) - fun runAction(action: String?) { - if (action.isNullOrBlank()) return - if (onRunAction != null) { - onRunAction() + if (onClick != null) { + if (content != null) { + content(state.displayName, isSelected, onClick, trailingIcon) } else { - val useCase = useCase ?: return - scope.launch(IO) { - useCase.executeAction( - action = action, - title = kind.title, - sourceUrl = sourceUrl, - infoMap = infoMap, - activity = activity, - onRefreshKinds = onRefreshKinds - ) - } - } - } - - fun updateValue(value: String) { - if (onValueChange != null) { - onValueChange(value) - } else { - infoMap?.let { - it[kind.title] = value - it.saveNow() - } + ExploreKindItem( + kind = kind, + isClickable = true, + onClick = onClick, + modifier = modifier, + backgroundColor = backgroundColor, + isMiuix = isMiuix, + displayText = state.displayName, + isSelected = isSelected, + trailingIcon = trailingIcon + ) } + return } when (kind.type) { ExploreKind.Type.url -> { val url = kind.url?.takeIf { it.isNotBlank() } - ExploreKindItem( - kind = kind, - isClickable = !url.isNullOrBlank(), - onClick = { - if (url.isNullOrBlank()) return@ExploreKindItem + val internalOnClick = { + if (!url.isNullOrBlank()) { if (kind.title.startsWith("ERROR:")) { activity?.showDialogFragment(TextDialog("ERROR", url)) } else { onOpenUrl(url) } - }, - modifier = modifier, - backgroundColor = backgroundColor, - isMiuix = isMiuix, - displayText = displayName - ) + } + } + if (content != null) { + content(state.displayName, isSelected, internalOnClick, trailingIcon) + } else { + ExploreKindItem( + kind = kind, + isClickable = !url.isNullOrBlank(), + onClick = internalOnClick, + modifier = modifier, + backgroundColor = backgroundColor, + isMiuix = isMiuix, + displayText = state.displayName, + isSelected = isSelected + ) + } } ExploreKind.Type.button -> { - ExploreKindItem( - kind = kind, - isClickable = !kind.action.isNullOrBlank(), - onClick = { runAction(kind.action) }, - modifier = modifier, - backgroundColor = backgroundColor, - isMiuix = isMiuix, - displayText = displayName, - trailingIcon = { - CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) { - AppIcon( - imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, - contentDescription = null, - modifier = Modifier.height(14.dp), - tint = LegadoTheme.colorScheme.outlineVariant - ) - } - } - ) + val internalOnClick = { + if (onRunAction != null) onRunAction() + else state.executeAction(kind.action) + } + if (content != null) { + content(state.displayName, isSelected, internalOnClick, trailingIcon) + } else { + ExploreKindItem( + kind = kind, + isClickable = !kind.action.isNullOrBlank(), + onClick = internalOnClick, + modifier = modifier, + backgroundColor = backgroundColor, + isMiuix = isMiuix, + displayText = state.displayName, + isSelected = isSelected, + trailingIcon = trailingIcon + ) + } } ExploreKind.Type.text -> { - var value by remember(sourceUrl, kind.title) { - mutableStateOf(valueOverride ?: infoMap?.get(kind.title).orEmpty()) + if (content != null) { + content(state.displayName, isSelected, {}, null) + } else { + TextTypeItem( + kind, + sourceUrl, + state, + valueOverride, + onValueChange, + modifier, + backgroundColor + ) } - LaunchedEffect(valueOverride) { - if (valueOverride != null) { - value = valueOverride - } - } - var actionJob by remember(sourceUrl, kind.title) { mutableStateOf(null) } - ExploreKindCompactTextField( - value = value, - onValueChange = { newValue -> - value = newValue - updateValue(newValue) - if (!kind.action.isNullOrBlank()) { - actionJob?.cancel() - actionJob = scope.launch { - delay(600) - runAction(kind.action) - } - } - }, - placeholder = displayName, - modifier = modifier, - backgroundColor = backgroundColor, - isMiuix = isMiuix - ) } ExploreKind.Type.toggle -> { - val chars = remember(kind.chars) { - kind.chars?.filterNotNull().takeUnless { it.isNullOrEmpty() } ?: listOf("chars", "is null") - } - val left = kind.style().layout_justifySelf != "right" - var char by remember(sourceUrl, kind.title, kind.default, kind.chars) { - mutableStateOf( - valueOverride - ?: infoMap?.get(kind.title) - ?.takeUnless { it.isEmpty() } - ?: (kind.default ?: chars.first()).also { - infoMap?.let { map -> - map[kind.title] = it - map.saveNow() - } - } - ) - } - LaunchedEffect(valueOverride) { - if (valueOverride != null) { - char = valueOverride - } - } - val text = if (left) "$char$displayName" else "$displayName$char" - ExploreKindItem( - kind = kind, - isClickable = true, - onClick = { - val currentIndex = chars.indexOf(char) - val nextIndex = if (currentIndex < 0) 0 else (currentIndex + 1) % chars.size - char = chars.getOrElse(nextIndex) { "" } - updateValue(char) - runAction(kind.action) - }, - modifier = modifier, - backgroundColor = backgroundColor, - isMiuix = isMiuix, - displayText = text, - trailingIcon = { - AppIcon( - imageVector = Icons.Default.Refresh, - contentDescription = null, - modifier = Modifier.height(14.dp), - tint = LegadoTheme.colorScheme.outlineVariant - ) - } + ToggleTypeItem( + kind, + sourceUrl, + state, + valueOverride, + onValueChange, + isSelected, + modifier, + backgroundColor, + isMiuix, + trailingIcon, + content ) } ExploreKind.Type.select -> { - val chars = remember(kind.chars) { - kind.chars?.filterNotNull().takeUnless { it.isNullOrEmpty() } ?: listOf("chars", "is null") - } - var selected by remember(sourceUrl, kind.title, kind.default, kind.chars) { - mutableStateOf( - valueOverride - ?: infoMap?.get(kind.title) - ?.takeUnless { it.isEmpty() } - ?: (kind.default ?: chars.first()).also { - infoMap?.let { map -> - map[kind.title] = it - map.saveNow() - } - } - ) - } - LaunchedEffect(valueOverride) { - if (valueOverride != null) { - selected = valueOverride - } - } - var showSelector by remember(sourceUrl, kind.title) { mutableStateOf(false) } - Box(modifier = modifier) { - ExploreKindItem( - kind = kind, - isClickable = chars.isNotEmpty(), - onClick = { showSelector = true }, - modifier = Modifier.fillMaxWidth(), - backgroundColor = backgroundColor, - isMiuix = isMiuix, - displayText = "$displayName $selected", - trailingIcon = { - AppIcon( - imageVector = Icons.Default.UnfoldMore, - contentDescription = null, - modifier = Modifier.height(14.dp), - tint = LegadoTheme.colorScheme.outlineVariant - ) - } - ) - RoundDropdownMenu( - expanded = showSelector, - onDismissRequest = { showSelector = false } - ) { - chars.forEach { option -> - RoundDropdownMenuItem( - text = option, - onClick = { - showSelector = false - if (selected != option) { - selected = option - updateValue(option) - runAction(kind.action) - } - } - ) - } - } - } + SelectTypeItem( + kind, + sourceUrl, + state, + valueOverride, + onValueChange, + isSelected, + modifier, + backgroundColor, + isMiuix, + trailingIcon, + content + ) } else -> { - ExploreKindItem( - kind = kind, - isClickable = false, - onClick = {}, - modifier = modifier, - backgroundColor = backgroundColor, - isMiuix = isMiuix, - displayText = displayName - ) + if (content != null) { + content(state.displayName, isSelected, {}, null) + } else { + ExploreKindItem( + kind = kind, + isClickable = false, + onClick = {}, + modifier = modifier, + backgroundColor = backgroundColor, + isMiuix = isMiuix, + displayText = state.displayName + ) + } } } } @Composable -private fun ExploreKindCompactTextField( - value: String, - onValueChange: (String) -> Unit, - placeholder: String, - modifier: Modifier = Modifier, - backgroundColor: Color = LegadoTheme.colorScheme.surfaceContainer, - isMiuix: Boolean +private fun TextTypeItem( + kind: ExploreKind, + sourceUrl: String?, + state: ExploreKindItemState, + valueOverride: String?, + onValueChange: ((String) -> Unit)?, + modifier: Modifier, + backgroundColor: Color ) { - val interactionSource = remember { MutableInteractionSource() } - val shape = RoundedCornerShape(10.dp) - - BasicTextField( + val scope = rememberCoroutineScope() + var value by remember(sourceUrl, kind.title) { + mutableStateOf(valueOverride ?: state.infoMap?.get(kind.title).orEmpty()) + } + LaunchedEffect(valueOverride) { + if (valueOverride != null) value = valueOverride + } + var actionJob by remember(sourceUrl, kind.title) { mutableStateOf(null) } + ExploreKindCompactTextField( value = value, - onValueChange = onValueChange, - singleLine = true, - textStyle = LegadoTheme.typography.bodySmall.copy(color = LegadoTheme.colorScheme.onSurface), - cursorBrush = SolidColor(LegadoTheme.colorScheme.primary), - interactionSource = interactionSource, - modifier = modifier - .height(34.dp) - .clip(shape) - .background(backgroundColor), - decorationBox = { innerTextField -> - Box( - modifier = Modifier - .fillMaxWidth() - .height(34.dp) - .padding(horizontal = 10.dp), - contentAlignment = androidx.compose.ui.Alignment.CenterStart - ) { - if (value.isEmpty()) { - AppText( - text = placeholder, - color = LegadoTheme.colorScheme.outline, - style = LegadoTheme.typography.bodySmall, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.fillMaxWidth() - ) - } - Box(modifier = Modifier.fillMaxWidth()) { - innerTextField() + onValueChange = { newValue -> + value = newValue + state.updateValue(newValue, onValueChange) + if (!kind.action.isNullOrBlank()) { + actionJob?.cancel() + actionJob = scope.launch { + delay(600) + state.executeAction(kind.action) } } - } + }, + placeholder = state.displayName, + modifier = modifier, + backgroundColor = backgroundColor ) } + +@Composable +private fun ToggleTypeItem( + kind: ExploreKind, + sourceUrl: String?, + state: ExploreKindItemState, + valueOverride: String?, + onValueChange: ((String) -> Unit)?, + isSelected: Boolean, + modifier: Modifier, + backgroundColor: Color, + isMiuix: Boolean, + trailingIcon: @Composable (() -> Unit)?, + content: (@Composable (displayName: String, isSelected: Boolean, onClick: () -> Unit, trailingIcon: @Composable (() -> Unit)?) -> Unit)? +) { + val chars = remember(kind.chars) { + kind.chars?.filterNotNull().takeUnless { it.isNullOrEmpty() } ?: listOf("chars", "is null") + } + val left = kind.style().layout_justifySelf != "right" + var char by remember(sourceUrl, kind.title, kind.default, kind.chars) { + mutableStateOf( + valueOverride + ?: state.infoMap?.get(kind.title) + ?.takeUnless { it.isEmpty() } + ?: (kind.default ?: chars.first()).also { + state.updateValue(it, onValueChange) + } + ) + } + LaunchedEffect(valueOverride) { + if (valueOverride != null) char = valueOverride + } + val text = if (left) "$char${state.displayName}" else "${state.displayName}$char" + val internalOnClick = { + val currentIndex = chars.indexOf(char) + val nextIndex = if (currentIndex < 0) 0 else (currentIndex + 1) % chars.size + char = chars.getOrElse(nextIndex) { "" } + state.updateValue(char, onValueChange) + state.executeAction(kind.action) + } + + if (content != null) { + content(text, isSelected, internalOnClick, trailingIcon) + } else { + ExploreKindItem( + kind = kind, + isClickable = true, + onClick = internalOnClick, + modifier = modifier, + backgroundColor = backgroundColor, + isMiuix = isMiuix, + displayText = text, + isSelected = isSelected, + trailingIcon = trailingIcon + ) + } +} + +@Composable +private fun SelectTypeItem( + kind: ExploreKind, + sourceUrl: String?, + state: ExploreKindItemState, + valueOverride: String?, + onValueChange: ((String) -> Unit)?, + isSelected: Boolean, + modifier: Modifier, + backgroundColor: Color, + isMiuix: Boolean, + trailingIcon: @Composable (() -> Unit)?, + content: (@Composable (displayName: String, isSelected: Boolean, onClick: () -> Unit, trailingIcon: @Composable (() -> Unit)?) -> Unit)? +) { + val chars = remember(kind.chars) { + kind.chars?.filterNotNull().takeUnless { it.isNullOrEmpty() } ?: listOf("chars", "is null") + } + var selected by remember(sourceUrl, kind.title, kind.default, kind.chars) { + mutableStateOf( + valueOverride + ?: state.infoMap?.get(kind.title) + ?.takeUnless { it.isEmpty() } + ?: (kind.default ?: chars.first()).also { + state.updateValue(it, onValueChange) + } + ) + } + LaunchedEffect(valueOverride) { + if (valueOverride != null) selected = valueOverride + } + var showSelector by remember(sourceUrl, kind.title) { mutableStateOf(false) } + + Box(modifier = modifier) { + val internalOnClick = { showSelector = true } + val displayText = "${state.displayName} $selected" + + if (content != null) { + content(displayText, isSelected, internalOnClick, trailingIcon) + } else { + ExploreKindItem( + kind = kind, + isClickable = chars.isNotEmpty(), + onClick = internalOnClick, + modifier = Modifier.fillMaxWidth(), + backgroundColor = backgroundColor, + isMiuix = isMiuix, + displayText = displayText, + isSelected = isSelected, + trailingIcon = trailingIcon + ) + } + + RoundDropdownMenu( + expanded = showSelector, + onDismissRequest = { showSelector = false } + ) { + chars.forEach { option -> + RoundDropdownMenuItem( + text = option, + onClick = { + showSelector = false + if (selected != option) { + selected = option + state.updateValue(option, onValueChange) + state.executeAction(kind.action) + } + } + ) + } + } + } +} + +@Composable +private fun rememberTrailingIcon(type: String, isSelected: Boolean): @Composable (() -> Unit)? { + return remember(type, isSelected) { + when (type) { + ExploreKind.Type.button -> { + { + CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) { + AppIcon( + imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, + contentDescription = null, + modifier = Modifier.height(14.dp), + tint = if (isSelected) LegadoTheme.colorScheme.onPrimaryContainer.copy( + alpha = 0.7f + ) else LegadoTheme.colorScheme.outlineVariant + ) + } + } + } + + ExploreKind.Type.toggle -> { + { + AppIcon( + imageVector = Icons.Default.Refresh, + contentDescription = null, + modifier = Modifier.height(14.dp), + tint = if (isSelected) LegadoTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f) else LegadoTheme.colorScheme.outlineVariant + ) + } + } + + ExploreKind.Type.select -> { + { + AppIcon( + imageVector = Icons.Default.UnfoldMore, + contentDescription = null, + modifier = Modifier.height(14.dp), + tint = if (isSelected) LegadoTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.7f) else LegadoTheme.colorScheme.outlineVariant + ) + } + } + + else -> null + } + } +} diff --git a/app/src/main/java/io/legado/app/ui/widget/components/explore/ExploreKindSelectSheet.kt b/app/src/main/java/io/legado/app/ui/widget/components/explore/ExploreKindSelectSheet.kt new file mode 100644 index 000000000..32ae3e0f7 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/components/explore/ExploreKindSelectSheet.kt @@ -0,0 +1,156 @@ +package io.legado.app.ui.widget.components.explore + +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +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.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material3.ExperimentalMaterial3Api +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.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import io.legado.app.data.entities.rule.ExploreKind +import io.legado.app.data.repository.ExploreRepository +import io.legado.app.domain.usecase.ExploreKindUiUseCase +import io.legado.app.ui.theme.LegadoTheme +import io.legado.app.ui.theme.ThemeResolver +import io.legado.app.ui.widget.components.SearchBar +import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet +import io.legado.app.ui.widget.components.topbar.TopBarActionButton +import org.koin.compose.koinInject + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ExploreKindSelectSheet( + show: Boolean, + onDismissRequest: () -> Unit, + sourceUrl: String?, + onSelected: (List) -> Unit, + multiple: Boolean = false, + initialSelectedTitles: List = emptyList(), + repository: ExploreRepository = koinInject(), + useCase: ExploreKindUiUseCase = koinInject() +) { + var kinds by remember { mutableStateOf>(emptyList()) } + var selectedTitles by remember(initialSelectedTitles, show) { + mutableStateOf(initialSelectedTitles.toSet()) + } + var query by remember { mutableStateOf("") } + val context = LocalContext.current + val activity = context as? AppCompatActivity + val isMiuix = ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine) + + LaunchedEffect(show, sourceUrl) { + if (show && !sourceUrl.isNullOrBlank()) { + kinds = repository.getSourceExploreKinds(sourceUrl) + } + } + + val filteredKinds = remember(query, kinds) { + if (query.isBlank()) kinds + else kinds.filter { kind -> + kind.title.contains(query, ignoreCase = true) || + (kind.url?.contains(query, ignoreCase = true) == true) + } + } + val kindRows = remember(filteredKinds) { + calculateExploreKindRows(filteredKinds, 6) + } + + AppModalBottomSheet( + show = show, + onDismissRequest = onDismissRequest, + endAction = { + if (multiple && selectedTitles.isNotEmpty()) { + TopBarActionButton( + onClick = { + val selectedKinds = kinds.filter { it.title in selectedTitles } + onSelected(selectedKinds) + onDismissRequest() + }, + imageVector = Icons.Default.Check, + contentDescription = "Confirm" + ) + } + } + ) { + Column { + SearchBar( + query = query, + backgroundColor = LegadoTheme.colorScheme.onSheetContent, + onQueryChange = { query = it }, + placeholder = "选择或搜索分类", + autoFocus = false + ) + + LazyColumn( + contentPadding = PaddingValues(vertical = 16.dp), + modifier = Modifier.weight(1f, fill = false) + ) { + items(kindRows) { rowItems -> + Row( + modifier = Modifier + .fillMaxWidth() + .animateItem() + .padding(vertical = 4.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + rowItems.forEach { (kind, span) -> + val isSelected = kind.title in selectedTitles + ExploreKindMultiTypeItem( + modifier = Modifier + .weight(span.toFloat()) + .animateItem(), + kind = kind, + sourceUrl = sourceUrl, + activity = activity, + onOpenUrl = { url -> + if (!multiple) { + onSelected(listOf(kind.copy(url = url))) + onDismissRequest() + } + }, + isSelected = isSelected, + onClick = { + if (multiple) { + selectedTitles = if (isSelected) { + selectedTitles - kind.title + } else { + selectedTitles + kind.title + } + } else { + onSelected(listOf(kind)) + onDismissRequest() + } + }, + backgroundColor = LegadoTheme.colorScheme.surface.copy(alpha = 0.5f), + isMiuix = isMiuix, + useCase = useCase + ) + } + + val totalSpan = rowItems.sumOf { it.second } + if (totalSpan < 6) { + Spacer( + modifier = Modifier.weight((6 - totalSpan).toFloat()) + ) + } + } + } + } + } + } +} diff --git a/app/src/main/java/io/legado/app/ui/widget/components/explore/ExploreKindTextField.kt b/app/src/main/java/io/legado/app/ui/widget/components/explore/ExploreKindTextField.kt new file mode 100644 index 000000000..70b6bf71e --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/components/explore/ExploreKindTextField.kt @@ -0,0 +1,69 @@ +package io.legado.app.ui.widget.components.explore + +import androidx.compose.foundation.background +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import io.legado.app.ui.theme.LegadoTheme +import io.legado.app.ui.widget.components.text.AppText + +@Composable +fun ExploreKindCompactTextField( + value: String, + onValueChange: (String) -> Unit, + placeholder: String, + modifier: Modifier = Modifier, + backgroundColor: Color = LegadoTheme.colorScheme.surfaceContainer, +) { + val interactionSource = remember { MutableInteractionSource() } + val shape = RoundedCornerShape(10.dp) + + BasicTextField( + value = value, + onValueChange = onValueChange, + singleLine = true, + textStyle = LegadoTheme.typography.bodySmall.copy(color = LegadoTheme.colorScheme.onSurface), + cursorBrush = SolidColor(LegadoTheme.colorScheme.primary), + interactionSource = interactionSource, + modifier = modifier + .height(34.dp) + .clip(shape) + .background(backgroundColor), + decorationBox = { innerTextField -> + Box( + modifier = Modifier + .fillMaxWidth() + .height(34.dp) + .padding(horizontal = 10.dp), + contentAlignment = Alignment.CenterStart + ) { + if (value.isEmpty()) { + AppText( + text = placeholder, + color = LegadoTheme.colorScheme.outline, + style = LegadoTheme.typography.bodySmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.fillMaxWidth() + ) + } + Box(modifier = Modifier.fillMaxWidth()) { + innerTextField() + } + } + } + ) +} diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index ad3f6d20f..f269cf728 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -952,6 +952,7 @@ 状态栏显示时隐藏 反转目录 显示发现 + 显示首页 样式 分组样式 导出文件名 @@ -1710,6 +1711,11 @@ 确定要删除该集及其包含的所有模块副本吗? 移除模块 确定要从当前集中移除该模块吗? + 从发现分类选择 + 选择多个分类 + 已选择 %1$d 个 + 选择一个分类 + 添加按钮组 模块标题 自定义标题 diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index 5e1ef53e6..1d32aac8b 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -890,6 +890,7 @@ 輸入自訂源分組名稱 反轉目錄 顯示發現 + 顯示首頁 樣式 分組樣式 導出文件名 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 47c18be04..b199f18a2 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -893,6 +893,7 @@ 狀態欄顯示時隱藏 反轉目錄 顯示發現 + 顯示首頁 樣式 分組樣式 匯出檔案名 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 44d1ec46b..61478d3ef 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -982,6 +982,7 @@ Hide when status bar show Reverse toc Show Discovery + Show Homepage Style Group style Export file name @@ -1716,6 +1717,11 @@ Are you sure you want to delete this set and all its module copies? Remove Module Are you sure you want to remove this module from the current set? + Select from Kinds + Select multiple kinds + %1$d selected + Select one kind + OR Add Button Group Module Title Custom Title