diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e0b899da0..237d489f1 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -228,6 +228,7 @@ dependencies { implementation(libs.nanohttpd.websocket) implementation(libs.zxing.lite) implementation(libs.colorpicker) + implementation(libs.colorpicker.compose) implementation(libs.libarchive) implementation(libs.commons.text) implementation(libs.markwon.core) @@ -236,6 +237,7 @@ dependencies { implementation(libs.markwon.html) implementation(libs.quick.chinese.transfer.core) implementation(libs.hutool.crypto) + //noinspection GradleDependency implementation(platform(libs.firebase.bom)) implementation(libs.firebase.analytics) implementation(libs.firebase.perf) diff --git a/app/src/main/java/io/legado/app/constant/EventBus.kt b/app/src/main/java/io/legado/app/constant/EventBus.kt index 73bc289cd..5436261d3 100644 --- a/app/src/main/java/io/legado/app/constant/EventBus.kt +++ b/app/src/main/java/io/legado/app/constant/EventBus.kt @@ -37,4 +37,7 @@ object EventBus { const val REFRESH_BOOK_INFO = "refreshBookInfo" const val REFRESH_BOOK_CONTENT = "refreshBookContent" const val UP_TOC = "upToc" -} \ No newline at end of file + + const val UP_BOOKSHELF_COUNT = "upBookshelfCount" + const val UP_ALL_BOOK_TOC = "upAllBookToc" +} 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 e2ca89471..93eec3f24 100644 --- a/app/src/main/java/io/legado/app/constant/PreferKey.kt +++ b/app/src/main/java/io/legado/app/constant/PreferKey.kt @@ -23,6 +23,7 @@ object PreferKey { const val coverDefaultColor = "coverDefaultColor" const val coverShowNameN = "coverShowNameN" const val coverShowAuthorN = "coverShowAuthorN" + const val coverInfoOrientation = "coverInfoOrientation" const val remoteServerId = "remoteServerId" const val hideStatusBar = "hideStatusBar" const val clickActionTL = "clickActionTopLeft" @@ -50,6 +51,7 @@ object PreferKey { const val bookshelfLayout = "bookshelfLayout" const val bookshelfLayoutModePortrait = "bookshelfLayoutPortrait" const val bookshelfLayoutModeLandscape = "bookshelf_layout_landscape" + const val bookshelfLayoutCompact = "bookshelfLayoutCompact" const val bookshelfSort = "bookshelfSort" const val bookExportFileName = "bookExportFileName" const val bookImportFileName = "bookImportFileName" @@ -222,6 +224,10 @@ object PreferKey { const val sharedElementEnterTransitionEnable = "sharedElementEnterTransitionEnable" const val bookshelfLayoutGridLandscape = "bookshelfLayoutGridLandscape" const val bookshelfLayoutGridPortrait = "bookshelfLayoutGridPortrait" + const val bookshelfTitleSmallFont = "bookshelfTitleSmallFont" + const val bookshelfTitleCenter = "bookshelfTitleCenter" + const val bookshelfTitleMaxLines = "bookshelfTitleMaxLines" + const val bookshelfCoverShadow = "bookshelfCoverShadow" const val exploreLayoutGridLandscape = "exploreLayoutGridLandscape" diff --git a/app/src/main/java/io/legado/app/data/entities/Book.kt b/app/src/main/java/io/legado/app/data/entities/Book.kt index e752ea55d..71639b29b 100644 --- a/app/src/main/java/io/legado/app/data/entities/Book.kt +++ b/app/src/main/java/io/legado/app/data/entities/Book.kt @@ -348,6 +348,9 @@ data class Book( newBook.customIntro = customIntro newBook.customTag = customTag newBook.canUpdate = canUpdate + if (config.fixedType) { + newBook.type = type + } newBook.readConfig = readConfig return newBook } @@ -402,7 +405,9 @@ data class Book( val mangaColorFilter: String? = null, var mangaScrollMode: Int? = null, var webtoonSidePaddingDp: Int? = null, - var mangaBackground: String? = null + var mangaBackground: String? = null, + + var fixedType: Boolean = false // 固定书籍类型,不随书源更新 ) : Parcelable @@ -414,4 +419,4 @@ data class Book( @TypeConverter fun stringToReadConfig(json: String?) = GSON.fromJsonObject(json).getOrNull() } -} \ No newline at end of file +} 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 54846f952..d36d86236 100644 --- a/app/src/main/java/io/legado/app/di/appModule.kt +++ b/app/src/main/java/io/legado/app/di/appModule.kt @@ -20,15 +20,19 @@ import io.legado.app.help.http.okHttpClient import io.legado.app.help.http.okHttpClientManga import io.legado.app.ui.book.bookmark.AllBookmarkViewModel import io.legado.app.ui.book.explore.ExploreShowViewModel +import io.legado.app.ui.book.group.GroupViewModel import io.legado.app.ui.book.import.remote.RemoteBookViewModel import io.legado.app.ui.book.info.BookInfoViewModel import io.legado.app.ui.book.readRecord.ReadRecordViewModel import io.legado.app.ui.book.searchContent.SearchContentViewModel import io.legado.app.ui.book.toc.TocViewModel import io.legado.app.ui.book.toc.rule.TxtTocRuleViewModel +import io.legado.app.ui.config.coverConfig.CoverConfigViewModel import io.legado.app.ui.config.otherConfig.OtherConfigViewModel import io.legado.app.ui.config.readConfig.ReadConfigViewModel import io.legado.app.ui.dict.rule.DictRuleViewModel +import io.legado.app.ui.main.MainViewModel +import io.legado.app.ui.main.bookshelf.BookshelfViewModel import io.legado.app.ui.main.explore.ExploreViewModel import io.legado.app.ui.main.my.MyViewModel import io.legado.app.ui.main.rss.RssViewModel @@ -76,11 +80,15 @@ val appModule = module { viewModelOf(::ReadRecordViewModel) viewModelOf(::ExploreShowViewModel) viewModelOf(::MyViewModel) + viewModelOf(::BookshelfViewModel) + viewModelOf(::MainViewModel) + viewModelOf(::GroupViewModel) viewModelOf(::ReplaceRuleViewModel) viewModelOf(::AllBookmarkViewModel) viewModelOf(::TxtTocRuleViewModel) viewModelOf(::OtherConfigViewModel) viewModelOf(::ReadConfigViewModel) + viewModelOf(::CoverConfigViewModel) viewModelOf(::TocViewModel) viewModelOf(::RemoteBookViewModel) viewModelOf(::BookInfoViewModel) diff --git a/app/src/main/java/io/legado/app/help/book/BookExtensions.kt b/app/src/main/java/io/legado/app/help/book/BookExtensions.kt index 53c57add3..cfad5af0d 100644 --- a/app/src/main/java/io/legado/app/help/book/BookExtensions.kt +++ b/app/src/main/java/io/legado/app/help/book/BookExtensions.kt @@ -339,6 +339,9 @@ fun Book.updateTo(newBook: Book): Book { newBook.customIntro = customIntro newBook.customTag = customTag newBook.canUpdate = canUpdate + if (config.fixedType) { + newBook.type = type + } newBook.readConfig = readConfig val variableMap = variableMap.toMutableMap() variableMap.keys.removeIf { diff --git a/app/src/main/java/io/legado/app/model/BookCover.kt b/app/src/main/java/io/legado/app/model/BookCover.kt index bd7b35e1b..47d6dba5d 100644 --- a/app/src/main/java/io/legado/app/model/BookCover.kt +++ b/app/src/main/java/io/legado/app/model/BookCover.kt @@ -41,6 +41,7 @@ import io.legado.app.utils.getPrefString import kotlinx.coroutines.currentCoroutineContext import splitties.init.appCtx import java.io.File +import kotlin.random.Random @Keep object BookCover { @@ -101,16 +102,32 @@ object BookCover { appCtx.getPrefInt(PreferKey.coverShadowColorN, Color.BLACK) val key = if (isNightTheme) PreferKey.defaultCoverDark else PreferKey.defaultCover - val path = appCtx.getPrefString(key) - if (path.isNullOrBlank()) { + val paths = appCtx.getPrefString(key)?.split(",")?.filter { it.isNotBlank() } + + if (paths.isNullOrEmpty()) { defaultDrawable = appCtx.resources.getDrawable(R.drawable.image_cover_default, null) return } + + val randomPath = paths[Random.nextInt(paths.size)] defaultDrawable = kotlin.runCatching { - BitmapUtils.decodeBitmap(path, 600, 900)!!.toDrawable(appCtx.resources) + BitmapUtils.decodeBitmap(randomPath, 600, 900)!!.toDrawable(appCtx.resources) }.getOrDefault(appCtx.resources.getDrawable(R.drawable.image_cover_default, null)) } + fun getRandomDefaultDrawable(seed: Any? = null): Drawable { + val isNightTheme = AppConfig.isNightTheme + val key = if (isNightTheme) PreferKey.defaultCoverDark else PreferKey.defaultCover + val paths = appCtx.getPrefString(key)?.split(",")?.filter { it.isNotBlank() } + if (paths.isNullOrEmpty()) { + return appCtx.resources.getDrawable(R.drawable.image_cover_default, null) + } + val random = if (seed != null) Random(seed.hashCode()) else Random + val randomPath = paths[random.nextInt(paths.size)] + return kotlin.runCatching { + BitmapUtils.decodeBitmap(randomPath, 600, 900)!!.toDrawable(appCtx.resources) + }.getOrDefault(appCtx.resources.getDrawable(R.drawable.image_cover_default, null)) + } /** * 加载封面 @@ -122,8 +139,9 @@ object BookCover { sourceOrigin: String? = null, onLoadFinish: (() -> Unit)? = null, ): RequestBuilder { + val currentDefault = getRandomDefaultDrawable() if (AppConfig.useDefaultCover) { - return ImageLoader.load(context, defaultDrawable) + return ImageLoader.load(context, currentDefault) .centerCrop() } var options = RequestOptions().set(OkHttpModelLoader.loadOnlyWifiOption, loadOnlyWifi) @@ -156,8 +174,8 @@ object BookCover { } }) } - return builder.placeholder(defaultDrawable) - .error(defaultDrawable) + return builder.placeholder(currentDefault) + .error(currentDefault) .centerCrop() } @@ -222,7 +240,8 @@ object BookCover { loadOnlyWifi: Boolean = false, sourceOrigin: String? = null, ): RequestBuilder { - val loadBlur = ImageLoader.load(context, defaultDrawable) + val currentDefault = getRandomDefaultDrawable() + val loadBlur = ImageLoader.load(context, currentDefault) .transform(BlurTransformation(25), CenterCrop()) if (AppConfig.useDefaultCover) { return loadBlur @@ -302,4 +321,4 @@ object BookCover { } } -} \ No newline at end of file +} diff --git a/app/src/main/java/io/legado/app/model/webBook/WebBook.kt b/app/src/main/java/io/legado/app/model/webBook/WebBook.kt index ac12f2a2c..f79820989 100644 --- a/app/src/main/java/io/legado/app/model/webBook/WebBook.kt +++ b/app/src/main/java/io/legado/app/model/webBook/WebBook.kt @@ -162,8 +162,10 @@ object WebBook { book: Book, canReName: Boolean = true, ): Book { - book.removeAllBookType() - book.addType(bookSource.getBookType()) + if (!book.config.fixedType) { + book.removeAllBookType() + book.addType(bookSource.getBookType()) + } if (!book.infoHtml.isNullOrEmpty()) { BookInfo.analyzeBookInfo( bookSource = bookSource, @@ -235,8 +237,10 @@ object WebBook { book: Book, runPerJs: Boolean = false ): Result> { - book.removeAllBookType() - book.addType(bookSource.getBookType()) + if (!book.config.fixedType) { + book.removeAllBookType() + book.addType(bookSource.getBookType()) + } return kotlin.runCatching { if (runPerJs) { runPreUpdateJs(bookSource, book).getOrThrow() @@ -420,4 +424,4 @@ object WebBook { } } -} \ No newline at end of file +} 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 3f500c2a1..499932d11 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 @@ -86,7 +86,6 @@ import io.legado.app.model.BookShelfState import io.legado.app.ui.theme.responsiveHazeEffect import io.legado.app.ui.theme.responsiveHazeSource import io.legado.app.ui.widget.components.AnimatedTextLine -import io.legado.app.ui.widget.components.Cover import io.legado.app.ui.widget.components.GlassMediumFlexibleTopAppBar import io.legado.app.ui.widget.components.GlassTopAppBarDefaults import io.legado.app.ui.widget.components.SearchBarSection @@ -94,6 +93,7 @@ import io.legado.app.ui.widget.components.button.AnimatedTextButton import io.legado.app.ui.widget.components.button.TopBarActionButton import io.legado.app.ui.widget.components.button.TopbarNavigationButton import io.legado.app.ui.widget.components.card.TextCard +import io.legado.app.ui.widget.components.cover.Cover import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem import io.legado.app.ui.widget.components.modalBottomSheet.GlassModalBottomSheet @@ -599,21 +599,22 @@ fun ExploreBookItem( if (!latestChapter.isNullOrEmpty()) { Text( text = " • ", - style = MaterialTheme.typography.labelSmall, + style = MaterialTheme.typography.bodySmall, color = Color.Gray, maxLines = 1 ) Text( text = "最新: $latestChapter", - style = MaterialTheme.typography.labelSmall, - color = Color.Gray, + style = MaterialTheme.typography.bodySmall, maxLines = 1, overflow = TextOverflow.Ellipsis ) } } + Spacer(modifier = Modifier.height(4.dp)) + val intro = book.intro?.replace("\\s+".toRegex(), "") ?: "" if (intro.isNotEmpty()) { Text( diff --git a/app/src/main/java/io/legado/app/ui/book/group/GroupEditSheet.kt b/app/src/main/java/io/legado/app/ui/book/group/GroupEditSheet.kt new file mode 100644 index 000000000..ec6014d0f --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/group/GroupEditSheet.kt @@ -0,0 +1,249 @@ +package io.legado.app.ui.book.group + +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.Restore +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.OutlinedIconButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +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.res.stringArrayResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import io.legado.app.R +import io.legado.app.data.entities.BookGroup +import io.legado.app.lib.dialogs.alert +import io.legado.app.ui.widget.components.cover.Cover +import io.legado.app.ui.widget.components.modalBottomSheet.GlassModalBottomSheet +import io.legado.app.ui.widget.components.settingItem.DropdownListSettingItem +import io.legado.app.ui.widget.components.settingItem.SettingItem +import io.legado.app.utils.FileUtils +import io.legado.app.utils.MD5Utils +import io.legado.app.utils.SelectImageContract +import io.legado.app.utils.externalFiles +import io.legado.app.utils.launch +import io.legado.app.utils.toastOnUi +import org.koin.androidx.compose.koinViewModel +import splitties.init.appCtx +import java.io.FileOutputStream + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun GroupEditSheet( + group: BookGroup? = null, + onDismissRequest: () -> Unit, + viewModel: GroupViewModel = koinViewModel() +) { + GlassModalBottomSheet(onDismissRequest = onDismissRequest) { + GroupEditContent( + group = group, + onDismissRequest = onDismissRequest, + viewModel = viewModel + ) + } +} + +@Composable +fun GroupEditContent( + group: BookGroup? = null, + onDismissRequest: () -> Unit, + viewModel: GroupViewModel = koinViewModel() +) { + val context = LocalContext.current + var groupName by remember(group) { mutableStateOf(group?.groupName ?: "") } + var coverPath by remember(group) { mutableStateOf(group?.cover) } + var enableRefresh by remember(group) { mutableStateOf(group?.enableRefresh ?: true) } + var selectedSortIndex by remember(group) { mutableIntStateOf(group?.bookSort ?: -1) } + + val sortOptions = stringArrayResource(R.array.book_sort) + val sortEntryValues = remember(sortOptions) { + Array(sortOptions.size) { (it - 1).toString() } + } + + val selectImage = rememberLauncherForActivityResult(SelectImageContract()) { result -> + result.uri?.let { uri -> + try { + val inputStream = context.contentResolver.openInputStream(uri) + ?: return@rememberLauncherForActivityResult + inputStream.use { input -> + val fileName = MD5Utils.md5Encode(input) + ".png" + val file = + FileUtils.createFileIfNotExist(context.externalFiles, "covers", fileName) + FileOutputStream(file).use { output -> + context.contentResolver.openInputStream(uri)?.use { it.copyTo(output) } + } + coverPath = file.absolutePath + } + } catch (e: Exception) { + appCtx.toastOnUi(e.localizedMessage) + } + } + } + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .padding(bottom = 16.dp) + .verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = stringResource(R.string.group_edit), + style = MaterialTheme.typography.titleLarge, + modifier = Modifier.padding(vertical = 16.dp) + ) + + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.Top, + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + Cover( + path = coverPath, + modifier = Modifier + .size(width = 90.dp, height = 130.dp) + .clickable { selectImage.launch() } + ) + + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + OutlinedTextField( + value = groupName, + onValueChange = { groupName = it }, + label = { Text(stringResource(R.string.group_name)) }, + modifier = Modifier.fillMaxWidth(), + singleLine = true + ) + + DropdownListSettingItem( + title = stringResource(R.string.sort), + selectedValue = selectedSortIndex.toString(), + displayEntries = sortOptions, + entryValues = sortEntryValues, + onValueChange = { + selectedSortIndex = it.toInt() + } + ) + } + } + + Spacer(modifier = Modifier.height(8.dp)) + + SettingItem( + title = stringResource(R.string.allow_drop_down_refresh), + trailingContent = { + Switch( + checked = enableRefresh, + onCheckedChange = { enableRefresh = it } + ) + }, + onClick = { enableRefresh = !enableRefresh } + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + if (group != null && (group.groupId > 0 || group.groupId == Long.MIN_VALUE)) { + OutlinedIconButton(onClick = { + context.alert(R.string.delete, R.string.sure_del) { + yesButton { + viewModel.delGroup(group) { + onDismissRequest() + } + } + noButton() + } + }) { + Icon( + Icons.Default.Delete, + contentDescription = stringResource(R.string.delete) + ) + } + } + + OutlinedIconButton(onClick = { + if (group != null) { + viewModel.clearCover(group) { + coverPath = null + appCtx.toastOnUi("封面已重置") + } + } else { + coverPath = null + } + }) { + Icon(Icons.Default.Restore, contentDescription = stringResource(R.string.reset)) + } + } + + Spacer(modifier = Modifier.weight(1f)) + + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + OutlinedButton(onClick = onDismissRequest) { + Text(stringResource(R.string.cancel)) + } + Button(onClick = { + if (groupName.isEmpty()) { + appCtx.toastOnUi("分组名称不能为空") + } else { + if (group != null) { + viewModel.upGroup( + group.copy( + groupName = groupName, + cover = coverPath, + bookSort = selectedSortIndex, + enableRefresh = enableRefresh + ) + ) { + onDismissRequest() + } + } else { + viewModel.addGroup( + groupName, + selectedSortIndex, + enableRefresh, + coverPath + ) { + onDismissRequest() + } + } + } + }) { + Text(stringResource(R.string.ok)) + } + } + } + } +} diff --git a/app/src/main/java/io/legado/app/ui/config/TestConfigActivity.kt b/app/src/main/java/io/legado/app/ui/config/TestConfigActivity.kt index 9bc623d9d..a2f1c3811 100644 --- a/app/src/main/java/io/legado/app/ui/config/TestConfigActivity.kt +++ b/app/src/main/java/io/legado/app/ui/config/TestConfigActivity.kt @@ -19,6 +19,7 @@ import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import io.legado.app.R import io.legado.app.base.BaseComposeActivity +import io.legado.app.ui.config.coverConfig.CoverConfigScreen import io.legado.app.ui.config.otherConfig.OtherConfigScreen import io.legado.app.ui.config.readConfig.ReadConfigScreen import io.legado.app.ui.widget.components.GlassMediumFlexibleTopAppBar @@ -38,6 +39,9 @@ class TestConfigActivity : BaseComposeActivity() { @Serializable object ReadConfigRoute + @Serializable + object CoverConfigRoute + @Composable override fun Content() { val navController = rememberNavController() @@ -50,7 +54,8 @@ class TestConfigActivity : BaseComposeActivity() { ConfigNavScreen( onBackClick = { finish() }, onNavigateToOther = { navController.navigate(OtherConfigRoute) }, - onNavigateToRead = { navController.navigate(ReadConfigRoute) } + onNavigateToRead = { navController.navigate(ReadConfigRoute) }, + onNavigateToCover = { navController.navigate(CoverConfigRoute) } ) } @@ -61,6 +66,10 @@ class TestConfigActivity : BaseComposeActivity() { composable { ReadConfigScreen(onBackClick = { navController.popBackStack() }) } + + composable { + CoverConfigScreen(onBackClick = { navController.popBackStack() }) + } } } @@ -69,7 +78,8 @@ class TestConfigActivity : BaseComposeActivity() { fun ConfigNavScreen( onBackClick: () -> Unit, onNavigateToOther: () -> Unit, - onNavigateToRead: () -> Unit + onNavigateToRead: () -> Unit, + onNavigateToCover: () -> Unit ) { val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() @@ -101,6 +111,10 @@ class TestConfigActivity : BaseComposeActivity() { title = stringResource(R.string.read_config), onClick = onNavigateToRead ) + ClickableSettingItem( + title = stringResource(R.string.cover_config), + onClick = onNavigateToCover + ) } } } diff --git a/app/src/main/java/io/legado/app/ui/config/bookshelfConfig/BookshelfConfig.kt b/app/src/main/java/io/legado/app/ui/config/bookshelfConfig/BookshelfConfig.kt new file mode 100644 index 000000000..9a54384bd --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/config/bookshelfConfig/BookshelfConfig.kt @@ -0,0 +1,120 @@ +package io.legado.app.ui.config.bookshelfConfig + +import io.legado.app.constant.EventBus +import io.legado.app.constant.PreferKey +import io.legado.app.ui.config.prefDelegate +import io.legado.app.utils.postEvent + +/** + * 书架配置 + */ +object BookshelfConfig { + + /** + * 书架分组样式: 0: Tab, 1:隐藏Tab, 2:文件夹 + */ + var bookGroupStyle by prefDelegate(PreferKey.bookGroupStyle, 0) { + postEvent(EventBus.NOTIFY_MAIN, false) + } + + /** + * 书架排序方式 + */ + var bookshelfSort by prefDelegate(PreferKey.bookshelfSort, 0) + + /** + * 书架排序顺序 + */ + var bookshelfSortOrder by prefDelegate(PreferKey.bookshelfSortOrder, 1) + + /** + * 是否显示未读标志 + */ + var showUnread by prefDelegate(PreferKey.showUnread, true) + + /** + * 是否显示新未读标志(小圆点) + */ + var showUnreadNew by prefDelegate(PreferKey.showUnreadNew, true) + + /** + * 是否显示更新提示 + */ + var showTip by prefDelegate(PreferKey.showTip, false) + + /** + * 是否在列表中显示最后更新时间 + */ + var showLastUpdateTime by prefDelegate(PreferKey.showLastUpdateTime, false) + + /** + * 是否显示等待更新的书籍数量 + */ + var showWaitUpCount by prefDelegate(PreferKey.showWaitUpCount, false) + + /** + * 是否显示书架快速滚动条 + */ + var showBookshelfFastScroller by prefDelegate(PreferKey.showBookshelfFastScroller, false) + + /** + * 是否在书架Tab处显示分类展开按钮 + */ + var shouldShowExpandButton by prefDelegate(PreferKey.shouldShowExpandButton, false) + + /** + * 书架同时更新书籍数量限制 (0为无限制) + */ + var bookshelfRefreshingLimit by prefDelegate(PreferKey.bookshelfRefreshingLimit, 0) + + /** + * 竖屏书架布局模式: 0:列表, 1:网格 + */ + var bookshelfLayoutModePortrait by prefDelegate(PreferKey.bookshelfLayoutModePortrait, 1) + + /** + * 竖屏书架网格列数 + */ + var bookshelfLayoutGridPortrait by prefDelegate(PreferKey.bookshelfLayoutGridPortrait, 3) + + /** + * 横屏书架布局模式: 0:列表, 1:网格 + */ + var bookshelfLayoutModeLandscape by prefDelegate(PreferKey.bookshelfLayoutModeLandscape, 1) + + /** + * 横屏书架网格列数 + */ + var bookshelfLayoutGridLandscape by prefDelegate(PreferKey.bookshelfLayoutGridLandscape, 7) + + /** + * 紧凑模式 + */ + var bookshelfLayoutCompact by prefDelegate(PreferKey.bookshelfLayoutCompact, false) + + /** + * 书架标题小字体 + */ + var bookshelfTitleSmallFont by prefDelegate(PreferKey.bookshelfTitleSmallFont, false) + + /** + * 书架标题居中 + */ + var bookshelfTitleCenter by prefDelegate(PreferKey.bookshelfTitleCenter, true) + + /** + * 书架标题最大行数 + */ + var bookshelfTitleMaxLines by prefDelegate(PreferKey.bookshelfTitleMaxLines, 2) + + /** + * 书架封面阴影 + */ + var bookshelfCoverShadow by prefDelegate(PreferKey.bookshelfCoverShadow, false) + + /** + * 启动时自动刷新书架 + */ + var autoRefreshBook by prefDelegate(PreferKey.autoRefresh, false) + +} diff --git a/app/src/main/java/io/legado/app/ui/config/coverConfig/CoverConfig.kt b/app/src/main/java/io/legado/app/ui/config/coverConfig/CoverConfig.kt new file mode 100644 index 000000000..50097a997 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/config/coverConfig/CoverConfig.kt @@ -0,0 +1,87 @@ +package io.legado.app.ui.config.coverConfig + +import io.legado.app.constant.PreferKey +import io.legado.app.ui.config.prefDelegate + +object CoverConfig { + + var loadCoverOnlyWifi by prefDelegate( + PreferKey.loadCoverOnlyWifi, + false + ) + + var useDefaultCover by prefDelegate( + PreferKey.useDefaultCover, + false + ) + + var coverShowShadow by prefDelegate( + PreferKey.coverShowShadow, + false + ) + + var coverShowStroke by prefDelegate( + PreferKey.coverShowStroke, + true + ) + + var coverDefaultColor by prefDelegate( + PreferKey.coverDefaultColor, + true + ) + + var defaultCover by prefDelegate( + PreferKey.defaultCover, + "" + ) + + var coverTextColor by prefDelegate( + PreferKey.coverTextColor, + -16777216 // This might need a better default or a color resource lookup + ) + + var coverShadowColor by prefDelegate( + PreferKey.coverShadowColor, + -16777216 + ) + + var coverShowName by prefDelegate( + PreferKey.coverShowName, + true + ) + + var coverShowAuthor by prefDelegate( + PreferKey.coverShowAuthor, + true + ) + + var defaultCoverDark by prefDelegate( + PreferKey.defaultCoverDark, + "" + ) + + var coverTextColorN by prefDelegate( + PreferKey.coverTextColorN, + -1 + ) + + var coverShadowColorN by prefDelegate( + PreferKey.coverShadowColorN, + -1 + ) + + var coverShowNameN by prefDelegate( + PreferKey.coverShowNameN, + true + ) + + var coverShowAuthorN by prefDelegate( + PreferKey.coverShowAuthorN, + true + ) + + var coverInfoOrientation by prefDelegate( + PreferKey.coverInfoOrientation, + "0" // 0: vertical, 1: horizontal + ) +} diff --git a/app/src/main/java/io/legado/app/ui/config/coverConfig/CoverConfigScreen.kt b/app/src/main/java/io/legado/app/ui/config/coverConfig/CoverConfigScreen.kt new file mode 100644 index 000000000..0ed3b5c7e --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/config/coverConfig/CoverConfigScreen.kt @@ -0,0 +1,285 @@ +package io.legado.app.ui.config.coverConfig + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +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.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import io.legado.app.R +import io.legado.app.constant.PreferKey +import io.legado.app.ui.widget.components.GlassMediumFlexibleTopAppBar +import io.legado.app.ui.widget.components.SplicedColumnGroup +import io.legado.app.ui.widget.components.button.TopbarNavigationButton +import io.legado.app.ui.widget.components.dialog.ColorPickerSheet +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.SwitchSettingItem +import org.koin.androidx.compose.koinViewModel + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CoverConfigScreen( + onBackClick: () -> Unit, + viewModel: CoverConfigViewModel = koinViewModel() +) { + val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() + var showCoverRuleSheet by remember { mutableStateOf(false) } + var manageKey by remember { mutableStateOf(null) } + var showColorPickerByField by remember { mutableStateOf(null) } + + Scaffold( + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + topBar = { + GlassMediumFlexibleTopAppBar( + title = { Text(stringResource(R.string.cover_config)) }, + scrollBehavior = scrollBehavior, + navigationIcon = { + TopbarNavigationButton(onClick = onBackClick) + } + ) + } + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .verticalScroll(rememberScrollState()) + .padding(16.dp) + ) { + SplicedColumnGroup { + SwitchSettingItem( + title = stringResource(R.string.only_wifi), + description = stringResource(R.string.only_wifi_summary), + checked = CoverConfig.loadCoverOnlyWifi, + onCheckedChange = { CoverConfig.loadCoverOnlyWifi = it } + ) + + ClickableSettingItem( + title = stringResource(R.string.cover_rule), + description = stringResource(R.string.cover_rule_summary), + onClick = { showCoverRuleSheet = true } + ) + + SwitchSettingItem( + title = stringResource(R.string.use_default_cover), + description = stringResource(R.string.use_default_cover_s), + checked = CoverConfig.useDefaultCover, + onCheckedChange = { CoverConfig.useDefaultCover = it } + ) + + SwitchSettingItem( + title = stringResource(R.string.cover_show_shadow), + checked = CoverConfig.coverShowShadow, + onCheckedChange = { + CoverConfig.coverShowShadow = it + viewModel.updateCoverStyle() + } + ) + + SwitchSettingItem( + title = stringResource(R.string.cover_show_stroke), + checked = CoverConfig.coverShowStroke, + onCheckedChange = { + CoverConfig.coverShowStroke = it + viewModel.updateCoverStyle() + } + ) + + SwitchSettingItem( + title = stringResource(R.string.default_color), + checked = CoverConfig.coverDefaultColor, + onCheckedChange = { + CoverConfig.coverDefaultColor = it + viewModel.updateCoverStyle() + } + ) + } + + SplicedColumnGroup { + DropdownListSettingItem( + title = "封面信息方向", + selectedValue = CoverConfig.coverInfoOrientation, + displayEntries = arrayOf( + stringResource(R.string.screen_portrait), + stringResource(R.string.screen_landscape) + ), + entryValues = arrayOf("0", "1"), + onValueChange = { + CoverConfig.coverInfoOrientation = it + viewModel.updateCoverStyle() + } + ) + } + + SplicedColumnGroup(title = stringResource(R.string.day)) { + val coverCount = CoverConfig.defaultCover.split(",").filter { it.isNotBlank() }.size + ClickableSettingItem( + title = stringResource(R.string.default_cover), + description = if (coverCount > 0) "已选择 $coverCount 张图片" else stringResource( + R.string.select_image + ), + onClick = { manageKey = PreferKey.defaultCover } + ) + + ClickableSettingItem( + title = stringResource(R.string.text_color), + option = "#${Integer.toHexString(CoverConfig.coverTextColor).uppercase()}", + onClick = { showColorPickerByField = "coverTextColor" }, + trailingContent = { + Box( + modifier = Modifier + .size(28.dp) + .clip(CircleShape) + .background(Color(CoverConfig.coverTextColor)) + .border(1.dp, MaterialTheme.colorScheme.outlineVariant, CircleShape) + ) + } + ) + + ClickableSettingItem( + title = stringResource(R.string.text_shadow_color), + option = "#${Integer.toHexString(CoverConfig.coverShadowColor).uppercase()}", + onClick = { showColorPickerByField = "coverShadowColor" }, + trailingContent = { + Box( + modifier = Modifier + .size(28.dp) + .clip(CircleShape) + .background(Color(CoverConfig.coverShadowColor)) + .border(1.dp, MaterialTheme.colorScheme.outlineVariant, CircleShape) + ) + } + ) + + SwitchSettingItem( + title = stringResource(R.string.cover_show_name), + description = stringResource(R.string.cover_show_name_summary), + checked = CoverConfig.coverShowName, + onCheckedChange = { viewModel.updateShowName(it) } + ) + + SwitchSettingItem( + title = stringResource(R.string.cover_show_author), + description = stringResource(R.string.cover_show_author_summary), + checked = CoverConfig.coverShowAuthor, + enabled = CoverConfig.coverShowName, + onCheckedChange = { viewModel.updateShowAuthor(it) } + ) + } + + SplicedColumnGroup(title = stringResource(R.string.night)) { + val coverCount = + CoverConfig.defaultCoverDark.split(",").filter { it.isNotBlank() }.size + ClickableSettingItem( + title = stringResource(R.string.default_cover), + description = if (coverCount > 0) "已选择 $coverCount 张图片" else stringResource( + R.string.select_image + ), + onClick = { manageKey = PreferKey.defaultCoverDark } + ) + + ClickableSettingItem( + title = stringResource(R.string.text_color), + option = "#${Integer.toHexString(CoverConfig.coverTextColorN).uppercase()}", + onClick = { showColorPickerByField = "coverTextColorN" }, + trailingContent = { + Box( + modifier = Modifier + .size(28.dp) + .clip(CircleShape) + .background(Color(CoverConfig.coverTextColorN)) + .border(1.dp, MaterialTheme.colorScheme.outlineVariant, CircleShape) + ) + } + ) + + ClickableSettingItem( + title = stringResource(R.string.text_shadow_color), + option = "#${Integer.toHexString(CoverConfig.coverShadowColorN).uppercase()}", + onClick = { showColorPickerByField = "coverShadowColorN" }, + trailingContent = { + Box( + modifier = Modifier + .size(28.dp) + .clip(CircleShape) + .background(Color(CoverConfig.coverShadowColorN)) + .border(1.dp, MaterialTheme.colorScheme.outlineVariant, CircleShape) + ) + } + ) + + SwitchSettingItem( + title = stringResource(R.string.cover_show_name), + description = stringResource(R.string.cover_show_name_summary), + checked = CoverConfig.coverShowNameN, + onCheckedChange = { viewModel.updateShowName(it, true) } + ) + + SwitchSettingItem( + title = stringResource(R.string.cover_show_author), + description = stringResource(R.string.cover_show_author_summary), + checked = CoverConfig.coverShowAuthorN, + enabled = CoverConfig.coverShowNameN, + onCheckedChange = { viewModel.updateShowAuthor(it, true) } + ) + } + } + } + + if (showCoverRuleSheet) { + CoverRuleConfigSheet(onDismissRequest = { showCoverRuleSheet = false }) + } + + manageKey?.let { key -> + CoverManageSheet( + preferenceKey = key, + onDismissRequest = { manageKey = null }, + viewModel = viewModel + ) + } + + showColorPickerByField?.let { field -> + val initialColor = when (field) { + "coverTextColor" -> CoverConfig.coverTextColor + "coverShadowColor" -> CoverConfig.coverShadowColor + "coverTextColorN" -> CoverConfig.coverTextColorN + "coverShadowColorN" -> CoverConfig.coverShadowColorN + else -> 0 + } + + ColorPickerSheet( + initialColor = initialColor, + onDismissRequest = { showColorPickerByField = null }, + onColorSelected = { color -> + when (field) { + "coverTextColor" -> CoverConfig.coverTextColor = color + "coverShadowColor" -> CoverConfig.coverShadowColor = color + "coverTextColorN" -> CoverConfig.coverTextColorN = color + "coverShadowColorN" -> CoverConfig.coverShadowColorN = color + } + viewModel.updateCoverStyle() + } + ) + } +} diff --git a/app/src/main/java/io/legado/app/ui/config/coverConfig/CoverConfigViewModel.kt b/app/src/main/java/io/legado/app/ui/config/coverConfig/CoverConfigViewModel.kt new file mode 100644 index 000000000..001f85e46 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/config/coverConfig/CoverConfigViewModel.kt @@ -0,0 +1,139 @@ +package io.legado.app.ui.config.coverConfig + +import android.content.Context +import android.net.Uri +import androidx.documentfile.provider.DocumentFile +import androidx.lifecycle.ViewModel +import io.legado.app.constant.AppLog +import io.legado.app.constant.PreferKey +import io.legado.app.exception.NoStackTraceException +import io.legado.app.lib.permission.Permissions +import io.legado.app.lib.permission.PermissionsCompat +import io.legado.app.model.BookCover +import io.legado.app.utils.FileDoc +import io.legado.app.utils.FileUtils +import io.legado.app.utils.MD5Utils +import io.legado.app.utils.RealPathUtil +import io.legado.app.utils.externalFiles +import io.legado.app.utils.isContentScheme +import io.legado.app.utils.printOnDebug +import io.legado.app.utils.toastOnUi +import splitties.init.appCtx +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.io.InputStream + +class CoverConfigViewModel : ViewModel() { + + fun addCoverFromUri(preferenceKey: String, uris: List) { + uris.forEach { uri -> + readUri(appCtx, uri) { fileDoc, _ -> + kotlin.runCatching { + var file = appCtx.externalFiles + val suffix = fileDoc.name.substringAfterLast(".") + val inputStreamForMd5 = appCtx.contentResolver.openInputStream(uri) + ?: throw NoStackTraceException("无法打开输入流") + val fileName = MD5Utils.md5Encode(inputStreamForMd5) + ".$suffix" + file = FileUtils.createFileIfNotExist(file, "covers", fileName) + FileOutputStream(file).use { + appCtx.contentResolver.openInputStream(uri)?.use { input -> + input.copyTo(it) + } + } + val currentCovers = if (preferenceKey == PreferKey.defaultCover) { + CoverConfig.defaultCover + } else { + CoverConfig.defaultCoverDark + } + val newList = + currentCovers.split(",").filter { it.isNotBlank() }.toMutableList() + if (!newList.contains(file.absolutePath)) { + newList.add(file.absolutePath) + } + val newCovers = newList.joinToString(",") + if (preferenceKey == PreferKey.defaultCover) { + CoverConfig.defaultCover = newCovers + } else { + CoverConfig.defaultCoverDark = newCovers + } + BookCover.upDefaultCover() + }.onFailure { + appCtx.toastOnUi(it.localizedMessage) + } + } + } + } + + private fun readUri( + context: Context, + uri: Uri?, + success: (fileDoc: FileDoc, inputStream: InputStream) -> Unit + ) { + uri ?: return + try { + if (uri.isContentScheme()) { + val doc = DocumentFile.fromSingleUri(context, uri) + doc ?: throw NoStackTraceException("未获取到文件") + val fileDoc = FileDoc.fromDocumentFile(doc) + context.contentResolver.openInputStream(uri)!!.use { inputStream -> + success.invoke(fileDoc, inputStream) + } + } else { + PermissionsCompat.Builder() + .addPermissions(*Permissions.Group.STORAGE) + .onGranted { + RealPathUtil.getPath(context, uri)?.let { path -> + val file = File(path) + val fileDoc = FileDoc.fromFile(file) + FileInputStream(file).use { inputStream -> + success.invoke(fileDoc, inputStream) + } + } + } + .request() + } + } catch (e: Exception) { + e.printOnDebug() + AppLog.put("读取Uri出错\n$e", e, true) + } + } + + fun removeCover(preferenceKey: String, path: String) { + val currentCovers = if (preferenceKey == PreferKey.defaultCover) { + CoverConfig.defaultCover + } else { + CoverConfig.defaultCoverDark + } + val newList = currentCovers.split(",").filter { it.isNotBlank() && it != path } + val newCovers = newList.joinToString(",") + if (preferenceKey == PreferKey.defaultCover) { + CoverConfig.defaultCover = newCovers + } else { + CoverConfig.defaultCoverDark = newCovers + } + BookCover.upDefaultCover() + } + + fun updateShowName(show: Boolean, isNight: Boolean = false) { + if (isNight) { + CoverConfig.coverShowNameN = show + } else { + CoverConfig.coverShowName = show + } + BookCover.upDefaultCover() + } + + fun updateShowAuthor(show: Boolean, isNight: Boolean = false) { + if (isNight) { + CoverConfig.coverShowAuthorN = show + } else { + CoverConfig.coverShowAuthor = show + } + BookCover.upDefaultCover() + } + + fun updateCoverStyle() { + BookCover.upDefaultCover() + } +} diff --git a/app/src/main/java/io/legado/app/ui/config/coverConfig/CoverManageSheet.kt b/app/src/main/java/io/legado/app/ui/config/coverConfig/CoverManageSheet.kt new file mode 100644 index 000000000..52c6298ca --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/config/coverConfig/CoverManageSheet.kt @@ -0,0 +1,154 @@ +package io.legado.app.ui.config.coverConfig + +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +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.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +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.layout.ContentScale +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import io.legado.app.R +import io.legado.app.constant.PreferKey +import io.legado.app.ui.widget.components.filePicker.FilePickerSheet +import io.legado.app.ui.widget.components.modalBottomSheet.GlassModalBottomSheet + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CoverManageSheet( + preferenceKey: String, + onDismissRequest: () -> Unit, + viewModel: CoverConfigViewModel +) { + var showFilePicker by remember { mutableStateOf(false) } + val selectImages = + rememberLauncherForActivityResult(ActivityResultContracts.GetMultipleContents()) { uris -> + if (uris.isNotEmpty()) { + viewModel.addCoverFromUri(preferenceKey, uris) + } + } + + val currentCovers = if (preferenceKey == PreferKey.defaultCover) { + CoverConfig.defaultCover + } else { + CoverConfig.defaultCoverDark + } + val coverList = currentCovers.split(",").filter { it.isNotBlank() } + + GlassModalBottomSheet( + onDismissRequest = onDismissRequest + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 24.dp) + ) { + Text( + text = stringResource(R.string.default_cover), + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(16.dp) + ) + + LazyVerticalGrid( + columns = GridCells.Fixed(3), + contentPadding = PaddingValues(16.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier.fillMaxWidth() + ) { + items(coverList) { path -> + Box { + Surface( + shape = MaterialTheme.shapes.medium, + tonalElevation = 2.dp + ) { + AsyncImage( + model = path, + contentDescription = null, + modifier = Modifier + .fillMaxWidth() + .aspectRatio(0.7f), + contentScale = ContentScale.Crop + ) + } + IconButton( + onClick = { viewModel.removeCover(preferenceKey, path) }, + modifier = Modifier + .align(Alignment.TopEnd) + .padding(4.dp) + .size(24.dp) + ) { + Surface( + shape = CircleShape, + color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.7f) + ) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = "Remove", + modifier = Modifier.size(16.dp) + ) + } + } + } + } + + item { + Surface( + onClick = { showFilePicker = true }, + shape = MaterialTheme.shapes.medium, + color = MaterialTheme.colorScheme.surfaceContainerHigh, + modifier = Modifier + .fillMaxWidth() + .aspectRatio(0.7f) + ) { + Box(contentAlignment = Alignment.Center) { + Icon( + imageVector = Icons.Default.Add, + contentDescription = "Add", + modifier = Modifier.size(32.dp), + tint = MaterialTheme.colorScheme.primary + ) + } + } + } + } + } + } + + if (showFilePicker) { + FilePickerSheet( + onDismissRequest = { showFilePicker = false }, + onSelectSysFiles = { + selectImages.launch("image/*") + showFilePicker = false + }, + allowExtensions = arrayOf("jpg", "jpeg", "png", "webp") + ) + } +} diff --git a/app/src/main/java/io/legado/app/ui/config/coverConfig/CoverRuleConfigSheet.kt b/app/src/main/java/io/legado/app/ui/config/coverConfig/CoverRuleConfigSheet.kt new file mode 100644 index 000000000..974f3f42f --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/config/coverConfig/CoverRuleConfigSheet.kt @@ -0,0 +1,126 @@ +package io.legado.app.ui.config.coverConfig + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.Checkbox +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +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.res.stringResource +import androidx.compose.ui.unit.dp +import io.legado.app.R +import io.legado.app.model.BookCover +import io.legado.app.ui.widget.components.modalBottomSheet.GlassModalBottomSheet +import io.legado.app.ui.widget.components.settingItem.InputSettingItem +import io.legado.app.utils.toastOnUi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import splitties.init.appCtx + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CoverRuleConfigSheet( + onDismissRequest: () -> Unit +) { + var enable by remember { mutableStateOf(false) } + var searchUrl by remember { mutableStateOf("") } + var coverRule by remember { mutableStateOf("") } + + LaunchedEffect(Unit) { + val rule = withContext(Dispatchers.IO) { + BookCover.getCoverRule() + } + enable = rule.enable + searchUrl = rule.searchUrl + coverRule = rule.coverRule + } + + GlassModalBottomSheet( + onDismissRequest = onDismissRequest + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Text( + text = stringResource(R.string.cover_rule), + style = MaterialTheme.typography.titleLarge, + modifier = Modifier.padding(bottom = 16.dp) + ) + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + Checkbox( + checked = enable, + onCheckedChange = { enable = it } + ) + Text(text = stringResource(R.string.enable)) + } + + InputSettingItem( + title = "搜索URL", + value = searchUrl, + onConfirm = { searchUrl = it } + ) + + InputSettingItem( + title = "封面规则", + value = coverRule, + onConfirm = { coverRule = it } + ) + + Spacer(modifier = Modifier.height(24.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + TextButton(onClick = { + BookCover.delCoverRule() + onDismissRequest() + }) { + Text(stringResource(R.string.delete), color = MaterialTheme.colorScheme.error) + } + + Spacer(modifier = Modifier.weight(1f)) + + OutlinedButton(onClick = onDismissRequest) { + Text(stringResource(R.string.cancel)) + } + + Spacer(modifier = Modifier.padding(horizontal = 4.dp)) + + Button(onClick = { + if (searchUrl.isBlank() || coverRule.isBlank()) { + appCtx.toastOnUi("搜索url和cover规则不能为空") + } else { + BookCover.CoverRule(enable, searchUrl, coverRule).let { config -> + BookCover.saveCoverRule(config) + } + onDismissRequest() + } + }) { + Text(stringResource(R.string.ok)) + } + } + } + } +} diff --git a/app/src/main/java/io/legado/app/ui/config/mainConfig/MainConfig.kt b/app/src/main/java/io/legado/app/ui/config/mainConfig/MainConfig.kt new file mode 100644 index 000000000..6cb709d02 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/config/mainConfig/MainConfig.kt @@ -0,0 +1,19 @@ +package io.legado.app.ui.config.mainConfig + +import io.legado.app.constant.PreferKey +import io.legado.app.ui.config.prefDelegate + +object MainConfig { + var showDiscovery by prefDelegate(PreferKey.showDiscovery, true) + var showRSS by prefDelegate(PreferKey.showRss, true) + var showBottomView by prefDelegate(PreferKey.showBottomView, true) + var defaultHomePage by prefDelegate(PreferKey.defaultHomePage, "bookshelf") + var tabletInterface by prefDelegate(PreferKey.tabletInterface, "auto") + var labelVisibilityMode by prefDelegate(PreferKey.labelVisibilityMode, "auto") + var swipeAnimation by prefDelegate(PreferKey.swipeAnimation, true) + var navExtended by prefDelegate("navExtended", false) + var webServiceAutoStart by prefDelegate(PreferKey.webServiceAutoStart, false) + var autoRefreshBook by prefDelegate(PreferKey.autoRefresh, false) + var autoCheckNewBackup by prefDelegate(PreferKey.autoCheckNewBackup, true) + var showStatusBar by prefDelegate(PreferKey.showStatusBar, true) +} 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 9b13128db..8c2a75568 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 @@ -1,39 +1,16 @@ -@file:Suppress("DEPRECATION") - package io.legado.app.ui.main -import android.annotation.SuppressLint import android.content.res.Configuration -import android.os.Build import android.os.Bundle import android.text.format.DateUtils -import android.view.Gravity -import android.view.MenuItem -import android.widget.FrameLayout -import android.widget.ImageView -import androidx.activity.OnBackPressedCallback -import androidx.activity.viewModels +import androidx.compose.runtime.Composable import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen -import androidx.core.view.get -import androidx.core.view.postDelayed -import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentActivity import androidx.lifecycle.lifecycleScope -import androidx.viewpager2.adapter.FragmentStateAdapter -import androidx.viewpager2.widget.ViewPager2 -import com.google.android.material.bottomnavigation.BottomNavigationView -import com.google.android.material.bottomnavigation.LabelVisibilityMode -import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton -import com.google.android.material.navigation.NavigationBarView -import com.google.android.material.transition.platform.MaterialContainerTransformSharedElementCallback import io.legado.app.BuildConfig import io.legado.app.R -import io.legado.app.base.VMBaseActivity +import io.legado.app.base.BaseComposeActivity import io.legado.app.constant.AppConst.appInfo -import io.legado.app.constant.EventBus import io.legado.app.constant.PreferKey -import io.legado.app.data.entities.BookGroup -import io.legado.app.databinding.ActivityMainBinding import io.legado.app.help.AppWebDav import io.legado.app.help.book.BookHelp import io.legado.app.help.config.AppConfig @@ -46,114 +23,36 @@ import io.legado.app.service.WebService import io.legado.app.ui.about.CrashLogsDialog import io.legado.app.ui.about.UpdateDialog import io.legado.app.ui.book.read.ReadBookActivity -import io.legado.app.ui.book.search.SearchActivity -import io.legado.app.ui.main.bookshelf.BaseBookshelfFragment -import io.legado.app.ui.main.bookshelf.books.BookshelfFragment1 -import io.legado.app.ui.main.bookshelf.books.BookshelfFragment2 -import io.legado.app.ui.main.bookshelf.books.BookshelfFragment3 -import io.legado.app.ui.main.bookshelf.books.BookshelfFragment4 -import io.legado.app.ui.main.explore.ExploreFragment -import io.legado.app.ui.main.my.MyFragment -import io.legado.app.ui.main.rss.RssFragment import io.legado.app.ui.welcome.WelcomeActivity import io.legado.app.ui.widget.dialog.TextDialog import io.legado.app.utils.getPrefBoolean -import io.legado.app.utils.gone -import io.legado.app.utils.observeEvent -import io.legado.app.utils.setNavigationBarColorAuto import io.legado.app.utils.showDialogFragment import io.legado.app.utils.startActivity -import io.legado.app.utils.themeColor -import io.legado.app.utils.toggleSystemBar -import io.legado.app.utils.viewbindingdelegate.viewBinding -import io.legado.app.utils.visible import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.koin.androidx.viewmodel.ext.android.viewModel import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine /** * 主界面 */ -open class MainActivity : VMBaseActivity(), - BottomNavigationView.OnNavigationItemSelectedListener, - BottomNavigationView.OnNavigationItemReselectedListener { +open class MainActivity : BaseComposeActivity() { - override val binding by viewBinding(ActivityMainBinding::inflate) - override val viewModel by viewModels() - private val idBookshelf = 0 - private val idBookshelf1 = 11 - private val idBookshelf2 = 12 - private val idBookshelf3 = 13 - private val idBookshelf4 = 14 - private val idExplore = 1 - private val idRss = 2 - private val idMy = 3 - private var bookshelfReselected: Long = 0 - private var exploreReselected: Long = 0 - private var pagePosition = 0 - private val fragmentMap = hashMapOf() - private var bottomMenuCount = 4 - private val realPositions = arrayOf(idBookshelf, idExplore, idRss, idMy) - private val swipeAnimation = AppConfig.swipeAnimation - private val adapter by lazy { - TabFragmentPageAdapter(this) - } - private lateinit var backCallback: OnBackPressedCallback - private val badge by lazy { - getNavigationBarView().getOrCreateBadge(R.id.menu_bookshelf) - } - - override fun onConfigurationChanged(newConfig: Configuration) { - super.onConfigurationChanged(newConfig) - recreate() - } - - private fun setupBackCallback() { - backCallback = object : OnBackPressedCallback(false) { - override fun handleOnBackPressed() { - if (pagePosition != 0) { - binding.viewPagerMain.currentItem = 0 - return - } - } - } - onBackPressedDispatcher.addCallback(this, backCallback) - } + private val viewModel by viewModel() override fun onCreate(savedInstanceState: Bundle?) { installSplashScreen() super.onCreate(savedInstanceState) - toggleSystemBar(AppConfig.showStatusBar) - if (checkStartupRoute()) return - setExitSharedElementCallback(MaterialContainerTransformSharedElementCallback()) - if (savedInstanceState != null) { - pagePosition = savedInstanceState.getInt("currentPagePosition", 0) - } - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S) - binding.viewPagerMain.fitsSystemWindows = true - // 其他初始化逻辑 - setupBackCallback() + if (checkStartupRoute()) return // 智能自启:如果上次是手动开启状态(web_service_auto 为 true),则自启 if (AppConfig.webServiceAutoStart) { WebService.startForeground(this) } - - upBottomMenu() - initView() - upHomePage() - } - override fun onResume() { - super.onResume() - toggleSystemBar(AppConfig.showStatusBar) - } - - override fun onPostCreate(savedInstanceState: Bundle?) { - super.onPostCreate(savedInstanceState) lifecycleScope.launch { //版本更新 upVersion() @@ -164,51 +63,27 @@ open class MainActivity : VMBaseActivity(), //自动更新书籍 val isAutoRefreshedBook = savedInstanceState?.getBoolean("isAutoRefreshedBook") ?: false if (AppConfig.autoRefreshBook && !isAutoRefreshedBook) { - binding.viewPagerMain.postDelayed(1000) { - viewModel.upAllBookToc() - } - } - binding.viewPagerMain.postDelayed(3000) { - viewModel.postLoad() + viewModel.upAllBookToc() } + viewModel.postLoad() } } - override fun onNavigationItemSelected(item: MenuItem): Boolean { - val index = when (item.itemId) { - R.id.menu_bookshelf -> 0 - R.id.menu_discovery -> realPositions.indexOf(idExplore) - R.id.menu_rss -> realPositions.indexOf(idRss) - R.id.menu_my_config -> realPositions.indexOf(idMy) - else -> 0 - } - binding.viewPagerMain.setCurrentItem(index, swipeAnimation) - return true - } + @Composable + override fun Content() { + val orientation = resources.configuration.orientation + val smallestWidthDp = resources.configuration.smallestScreenWidthDp + val tabletInterface = AppConfig.tabletInterface - override fun onNavigationItemReselected(item: MenuItem) { - when (item.itemId) { - R.id.menu_bookshelf -> { - val fragment = fragmentMap[getFragmentId(0)] as? BookshelfFragment2 - if (fragment != null && fragment.groupId != BookGroup.IdRoot) { - fragment.back() - } else { - if (System.currentTimeMillis() - bookshelfReselected > 300) { - bookshelfReselected = System.currentTimeMillis() - } else { - fragment?.gotoTop() - } - } - } - - R.id.menu_discovery -> { - if (System.currentTimeMillis() - exploreReselected > 300) { - exploreReselected = System.currentTimeMillis() - } else { - (fragmentMap[1] as? ExploreFragment)?.compressExplore() - } - } + val useRail = when (tabletInterface) { + "always" -> true + "landscape" -> orientation == Configuration.ORIENTATION_LANDSCAPE + "off" -> false + "auto" -> smallestWidthDp >= 600 + else -> false } + + MainScreen(useRail = useRail) } private fun checkStartupRoute(): Boolean { @@ -226,12 +101,6 @@ open class MainActivity : VMBaseActivity(), } } - private fun initView() = binding.run { - viewPagerMain.offscreenPageLimit = 3 - viewPagerMain.adapter = adapter - viewPagerMain.registerOnPageChangeCallback(PageChangeCallback()) - } - /** * 版本更新日志 */ @@ -275,10 +144,6 @@ open class MainActivity : VMBaseActivity(), } } - /** - * 设置本地密码 - */ - private fun notifyAppCrash() { if (!LocalConfig.appCrash || BuildConfig.DEBUG) { return @@ -316,7 +181,6 @@ open class MainActivity : VMBaseActivity(), override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) - outState.putInt("currentPagePosition", pagePosition) if (AppConfig.autoRefreshBook) { outState.putBoolean("isAutoRefreshedBook", true) } @@ -332,268 +196,6 @@ open class MainActivity : VMBaseActivity(), } } - /** - * 如果重启太快fragment不会重建,这里更新一下书架的排序 - */ - override fun recreate() { - (fragmentMap[getFragmentId(0)] as? BaseBookshelfFragment)?.run { - upSort() - } - super.recreate() - } - - override fun observeLiveBus() { - super.observeLiveBus() - viewModel.onUpBooksLiveData.observe(this) { - badge.isVisible = it > 0 - badge.number = it - } - - observeEvent(EventBus.NOTIFY_MAIN) { - binding.apply { - upBottomMenu() - updateBookshelfIcon(true) - if (it) { - viewPagerMain.setCurrentItem(bottomMenuCount - 1, false) - } - } - } - observeEvent(PreferKey.threadCount) { - viewModel.upPool() - } - } - - private fun getNavigationBarView(): NavigationBarView { - val tabletInterface = AppConfig.tabletInterface - val orientation = resources.configuration.orientation - val smallestWidthDp = resources.configuration.smallestScreenWidthDp - - val useRail = when (tabletInterface) { - "always" -> true - "landscape" -> orientation == Configuration.ORIENTATION_LANDSCAPE - "off" -> false - "auto" -> smallestWidthDp >= 600 - else -> false - } - - return if (useRail) binding.navigationRailView else binding.bottomNavigationView - } - - - @SuppressLint("NotifyDataSetChanged") - private fun upBottomMenu() { - val navView = getNavigationBarView() - val menu = navView.menu - - menu.findItem(R.id.menu_discovery).isVisible = AppConfig.showDiscovery - menu.findItem(R.id.menu_rss).isVisible = AppConfig.showRSS - - var index = 0 - if (AppConfig.showDiscovery) { - index++ - realPositions[index] = idExplore - } - if (AppConfig.showRSS) { - index++ - realPositions[index] = idRss - } - index++ - realPositions[index] = idMy - bottomMenuCount = index + 1 - - binding.viewPagerMain.adapter?.notifyDataSetChanged() - - if (AppConfig.showBottomView) { - window.setNavigationBarColorAuto(themeColor(com.google.android.material.R.attr.colorSurfaceContainer)) - val navView = getNavigationBarView() - navView.visible() - if (navView === binding.bottomNavigationView) { - binding.navigationRailView.gone() - } else { - binding.bottomNavigationView.gone() - } - - navView.labelVisibilityMode = when (AppConfig.labelVisibilityMode) { - "auto" -> LabelVisibilityMode.LABEL_VISIBILITY_AUTO - "selected" -> LabelVisibilityMode.LABEL_VISIBILITY_SELECTED - "labeled" -> LabelVisibilityMode.LABEL_VISIBILITY_LABELED - "unlabeled" -> LabelVisibilityMode.LABEL_VISIBILITY_UNLABELED - else -> LabelVisibilityMode.LABEL_VISIBILITY_AUTO - } - - } else { - window.setNavigationBarColorAuto(themeColor(com.google.android.material.R.attr.colorSurface)) - binding.bottomNavigationView.gone() - binding.navigationRailView.gone() - } - - val lp = - binding.navigationRailView.headerView!!.layoutParams as FrameLayout.LayoutParams - lp.gravity = Gravity.START - - binding.navigationRailView.headerView!! - .setPadding( - binding.navigationRailView.itemActiveIndicatorExpandedMarginHorizontal, - 0, - binding.navigationRailView.itemActiveIndicatorExpandedMarginHorizontal, - 0) - - val efab = - binding.navigationRailView.headerView!!.findViewById(R.id.nav_fab) - val button = - binding.navigationRailView.headerView!!.findViewById(R.id.nav_botton) - - efab.let { - if (LocalConfig.navExtended) { - it.isExtended = true - binding.navigationRailView.expand() - button.setImageResource(R.drawable.ic_menu_open) - } else { - it.isExtended = false - binding.navigationRailView.collapse() - button.setImageResource(R.drawable.ic_menu) - } - } - - efab.setOnClickListener { - startActivity() - } - - button.setOnClickListener { - efab.let { it1 -> - if (it1.isExtended) { - efab.isExtended = false - binding.navigationRailView.collapse() - button.setImageResource(R.drawable.ic_menu) - } else { - efab.isExtended = true - binding.navigationRailView.expand() - button.setImageResource(R.drawable.ic_menu_open) - } - LocalConfig.navExtended = it1.isExtended - } - } - - navView.setOnItemSelectedListener { onNavigationItemSelected(it) } - navView.setOnItemReselectedListener { onNavigationItemReselected(it) } - - binding.viewPagerMain.post { - val currentPosition = if (pagePosition < bottomMenuCount) pagePosition else 0 - binding.viewPagerMain.setCurrentItem(currentPosition, false) - getNavigationBarView().menu[currentPosition].isChecked = true - updateBackCallbackState() - } - - } - - private fun updateBookshelfIcon(isRoot: Boolean) { - val navView = getNavigationBarView() - val bookshelfMenuItem = navView.menu.findItem(R.id.menu_bookshelf) ?: return - - if (isRoot) { - bookshelfMenuItem.setIcon(R.drawable.ic_bottom_books) - } else { - val currentFragment = fragmentMap[getFragmentId(0)] - if (currentFragment is BookshelfFragment2) { - bookshelfMenuItem.setIcon(R.drawable.ic_arrow_back) - } else { - bookshelfMenuItem.setIcon(R.drawable.ic_bottom_books) - } - } - } - - private fun upHomePage() { - when (AppConfig.defaultHomePage) { - "bookshelf" -> {} - "explore" -> if (AppConfig.showDiscovery && AppConfig.showBottomView) { - binding.viewPagerMain.setCurrentItem(realPositions.indexOf(idExplore), false) - } - - "rss" -> if (AppConfig.showRSS && AppConfig.showBottomView) { - binding.viewPagerMain.setCurrentItem(realPositions.indexOf(idRss), false) - } - - "my" -> if (AppConfig.showBottomView) { - binding.viewPagerMain.setCurrentItem(realPositions.indexOf(idMy), false) - } - } - } - - private fun getFragmentId(position: Int): Int { - val id = realPositions[position] - if (id == idBookshelf) { - return when (AppConfig.bookGroupStyle) { - 0 -> idBookshelf1 - 1 -> idBookshelf2 - 2 -> idBookshelf3 - 3 -> idBookshelf4 - else -> idBookshelf1 - } - } - return id - } - - private inner class PageChangeCallback : ViewPager2.OnPageChangeCallback() { - override fun onPageSelected(position: Int) { - pagePosition = position - getNavigationBarView().selectedItemId = when (realPositions[position]) { - idBookshelf -> R.id.menu_bookshelf - idExplore -> R.id.menu_discovery - idRss -> R.id.menu_rss - idMy -> R.id.menu_my_config - else -> R.id.menu_bookshelf - } - if (position == 0) { - val fragment = fragmentMap[getFragmentId(0)] as? BookshelfFragment2 - updateBookshelfIcon(fragment?.groupId == BookGroup.IdRoot) - } else { - updateBookshelfIcon(true) - } - updateBackCallbackState() - } - } - - private fun updateBackCallbackState() { - backCallback.isEnabled = (pagePosition != 0) - } - - private inner class TabFragmentPageAdapter( - activity: FragmentActivity - ) : FragmentStateAdapter(activity) { - - override fun getItemCount(): Int = bottomMenuCount - - override fun createFragment(position: Int): Fragment { - val fragment = when (getFragmentId(position)) { - idBookshelf1 -> BookshelfFragment1(position) - idBookshelf2 -> BookshelfFragment2(position).apply { - this.onGroupIdChangedListener = { isRoot -> - if (pagePosition == 0) { - updateBookshelfIcon(isRoot) - } - } - } - idBookshelf3 -> BookshelfFragment3(position) - idBookshelf4 -> BookshelfFragment4(position) - idExplore -> ExploreFragment(position) - idRss -> RssFragment(position) - else -> MyFragment.newInstance(position) - } - - fragmentMap[getFragmentId(position)] = fragment - return fragment - } - - override fun getItemId(position: Int): Long { - return getFragmentId(position).toLong() - } - - override fun containsItem(itemId: Long): Boolean { - return (0 until bottomMenuCount).any { getItemId(it) == itemId } - } - - } - } class LauncherW : MainActivity() diff --git a/app/src/main/java/io/legado/app/ui/main/MainDestination.kt b/app/src/main/java/io/legado/app/ui/main/MainDestination.kt new file mode 100644 index 000000000..adecaa674 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/main/MainDestination.kt @@ -0,0 +1,53 @@ +package io.legado.app.ui.main + +import androidx.annotation.StringRes +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.LibraryBooks +import androidx.compose.material.icons.automirrored.outlined.LibraryBooks +import androidx.compose.material.icons.filled.Explore +import androidx.compose.material.icons.filled.Person +import androidx.compose.material.icons.filled.RssFeed +import androidx.compose.material.icons.outlined.Explore +import androidx.compose.material.icons.outlined.Person +import androidx.compose.material.icons.outlined.RssFeed +import androidx.compose.ui.graphics.vector.ImageVector +import io.legado.app.R + +sealed class MainDestination( + val route: String, + @StringRes val labelId: Int, + val icon: ImageVector, + val selectedIcon: ImageVector +) { + object Bookshelf : MainDestination( + "bookshelf", + R.string.bookshelf, + Icons.AutoMirrored.Outlined.LibraryBooks, + Icons.AutoMirrored.Filled.LibraryBooks + ) + + object Explore : MainDestination( + "explore", + R.string.screen_find, + Icons.Outlined.Explore, + Icons.Default.Explore + ) + + object Rss : MainDestination( + "rss", + R.string.rss, + Icons.Outlined.RssFeed, + Icons.Default.RssFeed + ) + + object My : MainDestination( + "my", + R.string.my, + Icons.Outlined.Person, + Icons.Default.Person + ) + + companion object { + val mainDestinations = listOf(Bookshelf, Explore, Rss, My) + } +} 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 new file mode 100644 index 000000000..518ae1412 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/main/MainScreen.kt @@ -0,0 +1,207 @@ +package io.legado.app.ui.main + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +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.MenuOpen +import androidx.compose.material.icons.filled.Menu +import androidx.compose.material.icons.filled.Search +import androidx.compose.material3.Badge +import androidx.compose.material3.BadgedBox +import androidx.compose.material3.BottomAppBarDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.ExtendedFloatingActionButton +import androidx.compose.material3.FlexibleBottomAppBar +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.NavigationBarItemDefaults +import androidx.compose.material3.NavigationRail +import androidx.compose.material3.NavigationRailItem +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import dev.chrisbanes.haze.HazeState +import dev.chrisbanes.haze.hazeSource +import io.legado.app.R +import io.legado.app.ui.book.info.BookInfoActivity +import io.legado.app.ui.book.read.ReadBookActivity +import io.legado.app.ui.book.search.SearchActivity +import io.legado.app.ui.config.mainConfig.MainConfig +import io.legado.app.ui.main.bookshelf.BookshelfScreen +import io.legado.app.ui.main.explore.ExploreScreen +import io.legado.app.ui.main.my.MyScreen +import io.legado.app.ui.main.rss.RssScreen +import io.legado.app.ui.theme.regularHazeEffect +import io.legado.app.ui.widget.components.GlassDefaults +import io.legado.app.utils.startActivity +import kotlinx.coroutines.launch +import org.koin.androidx.compose.koinViewModel + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) +@Composable +fun MainScreen( + viewModel: MainViewModel = koinViewModel(), + useRail: Boolean +) { + val context = LocalContext.current + val uiState by viewModel.state.collectAsStateWithLifecycle() + val coroutineScope = rememberCoroutineScope() + + val hazeState = remember { HazeState() } + val destinations = remember(MainConfig.showDiscovery, MainConfig.showRSS) { + MainDestination.mainDestinations.filter { + when (it) { + MainDestination.Explore -> MainConfig.showDiscovery + MainDestination.Rss -> MainConfig.showRSS + else -> true + } + } + } + + val initialPage = remember(destinations) { + val index = destinations.indexOfFirst { it.route == MainConfig.defaultHomePage } + if (index != -1) index else 0 + } + val pagerState = rememberPagerState(initialPage = initialPage) { destinations.size } + + Row(modifier = Modifier.fillMaxSize()) { + if (useRail && MainConfig.showBottomView) { + NavigationRail( + header = { + IconButton(onClick = { MainConfig.navExtended = !MainConfig.navExtended }) { + Icon( + if (MainConfig.navExtended) Icons.AutoMirrored.Filled.MenuOpen else Icons.Default.Menu, + contentDescription = null + ) + } + ExtendedFloatingActionButton( + onClick = { context.startActivity() }, + expanded = MainConfig.navExtended, + icon = { Icon(Icons.Default.Search, contentDescription = null) }, + text = { Text(text = stringResource(R.string.search)) } + ) + } + ) { + destinations.forEachIndexed { index, destination -> + val selected = pagerState.currentPage == index + NavigationRailItem( + selected = selected, + onClick = { + coroutineScope.launch { + pagerState.animateScrollToPage(index) + } + }, + icon = { + NavigationIcon(destination, selected, uiState.upBooksCount) + }, + label = { Text(stringResource(destination.labelId)) } + ) + } + } + } + + Scaffold( + modifier = Modifier.weight(1f), + bottomBar = { + if (!useRail && MainConfig.showBottomView) { + FlexibleBottomAppBar( + modifier = Modifier.regularHazeEffect(state = hazeState), + containerColor = GlassDefaults.glassColor( + noBlurColor = BottomAppBarDefaults.containerColor, + blurAlpha = GlassDefaults.DefaultBlurAlpha + ) + ) { + destinations.forEachIndexed { index, destination -> + val selected = pagerState.currentPage == index + NavigationBarItem( + selected = selected, + onClick = { + coroutineScope.launch { + pagerState.animateScrollToPage(index) + } + }, + icon = { + NavigationIcon(destination, selected, uiState.upBooksCount) + }, + colors = NavigationBarItemDefaults.colors( + indicatorColor = GlassDefaults.glassColor( + noBlurColor = MaterialTheme.colorScheme.primaryContainer, + blurAlpha = GlassDefaults.ThickBlurAlpha + ), + ), + label = { Text(stringResource(destination.labelId)) }, + alwaysShowLabel = false + ) + } + } + } + }, + contentWindowInsets = WindowInsets(0) + ) { _ -> + Box(modifier = Modifier.hazeSource(hazeState)) { + HorizontalPager( + state = pagerState, + modifier = Modifier.fillMaxSize(), + userScrollEnabled = true, + beyondViewportPageCount = 1 + ) { page -> + when (destinations[page]) { + MainDestination.Bookshelf -> BookshelfScreen( + onBookClick = { book -> + context.startActivity { + putExtra("bookUrl", book.bookUrl) + } + }, + onBookLongClick = { book -> + context.startActivity { + putExtra("name", book.name) + putExtra("author", book.author) + putExtra("bookUrl", book.bookUrl) + } + } + ) + + MainDestination.Explore -> ExploreScreen() + MainDestination.Rss -> RssScreen() + MainDestination.My -> MyScreen( + viewModel = koinViewModel(), + onNavigate = { event -> + viewModel.onPrefClickEvent(context, event) + } + ) + } + } + } + } + } +} + +@Composable +private fun NavigationIcon( + destination: MainDestination, + selected: Boolean, + upBooksCount: Int +) { + val icon = if (selected) destination.selectedIcon else destination.icon + if (destination == MainDestination.Bookshelf && upBooksCount > 0) { + BadgedBox(badge = { Badge { Text(upBooksCount.toString()) } }) { + Icon(icon, contentDescription = null) + } + } else { + Icon(icon, contentDescription = null) + } +} 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 ffa490ee2..2ee5a4f3c 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 @@ -1,251 +1,47 @@ package io.legado.app.ui.main import android.app.Application -import androidx.lifecycle.MutableLiveData +import android.content.Context +import android.content.Intent import androidx.lifecycle.viewModelScope -import androidx.recyclerview.widget.RecyclerView.RecycledViewPool import io.legado.app.base.BaseViewModel -import io.legado.app.constant.AppConst -import io.legado.app.constant.AppLog -import io.legado.app.constant.BookType import io.legado.app.constant.EventBus import io.legado.app.data.appDb -import io.legado.app.data.entities.Book -import io.legado.app.data.entities.BookSource import io.legado.app.help.AppWebDav import io.legado.app.help.DefaultData -import io.legado.app.help.book.BookHelp -import io.legado.app.help.book.addType -import io.legado.app.help.book.isLocal -import io.legado.app.help.book.isUpError -import io.legado.app.help.book.removeType -import io.legado.app.help.book.sync -import io.legado.app.help.config.AppConfig -import io.legado.app.model.CacheBook -import io.legado.app.model.ReadBook -import io.legado.app.model.SourceCallBack -import io.legado.app.model.webBook.WebBook -import io.legado.app.service.CacheBookService -import io.legado.app.utils.onEachParallel -import io.legado.app.utils.postEvent -import kotlinx.coroutines.Job -import kotlinx.coroutines.asCoroutineDispatcher -import kotlinx.coroutines.currentCoroutineContext -import kotlinx.coroutines.delay -import kotlinx.coroutines.ensureActive -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.onCompletion -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.isActive +import io.legado.app.ui.main.my.PrefClickEvent +import io.legado.app.utils.eventBus.FlowEventBus +import io.legado.app.utils.sendToClip +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch -import java.util.LinkedList -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.Executors -import kotlin.math.min + +data class MainUiState( + val upBooksCount: Int = 0 +) class MainViewModel(application: Application) : BaseViewModel(application) { - private var threadCount = AppConfig.threadCount - private var poolSize = min(threadCount, AppConst.MAX_THREAD) - private var upTocPool = Executors.newFixedThreadPool(poolSize).asCoroutineDispatcher() - private val waitUpTocBooks = LinkedList() - private val onUpTocBooks = ConcurrentHashMap.newKeySet() - val onUpBooksLiveData = MutableLiveData() - private var upTocJob: Job? = null - private var cacheBookJob: Job? = null - private val eventListenerSource = ConcurrentHashMap() - val booksListRecycledViewPool = RecycledViewPool().apply { - setMaxRecycledViews(0, 30) - } - val booksGridRecycledViewPool = RecycledViewPool().apply { - setMaxRecycledViews(0, 100) - } + + private val upBooksCountFlow = MutableStateFlow(0) + + val state: StateFlow = upBooksCountFlow + .map { count -> MainUiState(count) } + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), MainUiState()) init { + viewModelScope.launch { + FlowEventBus.with(EventBus.UP_BOOKSHELF_COUNT).collect { + upBooksCountFlow.value = it + } + } deleteNotShelfBook() } - override fun onCleared() { - super.onCleared() - upTocPool.close() - } - - fun upPool() { - threadCount = AppConfig.threadCount - if (upTocJob?.isActive == true || cacheBookJob?.isActive == true) { - return - } - val newPoolSize = min(threadCount, AppConst.MAX_THREAD) - if (poolSize == newPoolSize) { - return - } - poolSize = newPoolSize - upTocPool.close() - upTocPool = Executors.newFixedThreadPool(poolSize).asCoroutineDispatcher() - } - - fun isUpdate(bookUrl: String): Boolean { - return onUpTocBooks.contains(bookUrl) - } - fun upAllBookToc() { - execute { - addToWaitUp(appDb.bookDao.hasUpdateBooks) - } - } - - fun upToc(books: List) { - execute(context = upTocPool) { - books.filter { - !it.isLocal && it.canUpdate - }.let { - addToWaitUp(it) - } - } - } - - @Synchronized - private fun addToWaitUp(books: List) { - books.forEach { book -> - if (!waitUpTocBooks.contains(book.bookUrl) && !onUpTocBooks.contains(book.bookUrl)) { - waitUpTocBooks.add(book.bookUrl) - } - } - if (upTocJob == null) { - startUpTocJob() - } - } - - private fun startUpTocJob() { - upPool() - postUpBooksLiveData() - upTocJob = viewModelScope.launch(upTocPool) { - flow { - while (true) { - emit(waitUpTocBooks.poll() ?: break) - } - }.onEachParallel(threadCount) { - onUpTocBooks.add(it) - postEvent(EventBus.UP_BOOKSHELF, it) - updateToc(it) - }.onEach { - onUpTocBooks.remove(it) - postEvent(EventBus.UP_BOOKSHELF, it) - postUpBooksLiveData() - }.onCompletion { - upTocJob = null - if (waitUpTocBooks.isNotEmpty()) { - startUpTocJob() - } - if (it == null && cacheBookJob == null && !CacheBookService.isRun) { - //所有目录更新完再开始缓存章节 - cacheBook() - } - }.catch { - AppLog.put("更新目录出错\n${it.localizedMessage}", it) - }.collect() - } - } - - private suspend fun updateToc(bookUrl: String) { - val book = appDb.bookDao.getBook(bookUrl) ?: return - val source = appDb.bookSourceDao.getBookSource(book.origin) - if (source == null) { - if (!book.isUpError) { - book.addType(BookType.updateError) - appDb.bookDao.update(book) - } - return - } - if (source.eventListener) { - // 使用 putIfAbsent 确保只添加一次 - if (eventListenerSource.putIfAbsent(source, true) == null) { - // 通知监听事件的书源,书架刷新开始 - SourceCallBack.callBackSource( - viewModelScope, - SourceCallBack.START_SHELF_REFRESH, - source - ) - } - } - kotlin.runCatching { - val oldBook = book.copy() - if (book.tocUrl.isBlank()) { - WebBook.getBookInfoAwait(source, book) - } else { - WebBook.runPreUpdateJs(source, book) - } - val toc = WebBook.getChapterListAwait(source, book).getOrThrow() - book.sync(oldBook) - book.removeType(BookType.updateError) - if (book.bookUrl == bookUrl) { - appDb.bookDao.update(book) - } else { - appDb.bookDao.replace(oldBook, book) - BookHelp.updateCacheFolder(oldBook, book) - } - appDb.bookChapterDao.delByBook(bookUrl) - appDb.bookChapterDao.insert(*toc.toTypedArray()) - ReadBook.onChapterListUpdated(book) - addDownload(source, book) - }.onFailure { - currentCoroutineContext().ensureActive() - AppLog.put("${book.name} 更新目录失败\n${it.localizedMessage}", it) - //这里可能因为时间太长书籍信息已经更改,所以重新获取 - appDb.bookDao.getBook(book.bookUrl)?.let { book -> - book.addType(BookType.updateError) - appDb.bookDao.update(book) - } - } - } - - fun postUpBooksLiveData(reset: Boolean = false) { - if (AppConfig.showWaitUpCount) { - onUpBooksLiveData.postValue(waitUpTocBooks.size + onUpTocBooks.size) - } else if (reset) { - onUpBooksLiveData.postValue(0) - } - } - - @Synchronized - private fun addDownload(source: BookSource, book: Book) { - if (AppConfig.preDownloadNum == 0) return - val endIndex = min( - book.totalChapterNum - 1, - book.durChapterIndex.plus(AppConfig.preDownloadNum) - ) - val cacheBook = CacheBook.getOrCreate(source, book) - cacheBook.addDownload(book.durChapterIndex, endIndex) - } - - /** - * 缓存书籍 - */ - private fun cacheBook() { - //开始缓存前,通知监听事件的书源,书架刷新已完成 - eventListenerSource.toList().forEach { - SourceCallBack.callBackSource( - viewModelScope, - SourceCallBack.END_SHELF_REFRESH, - it.first - ) - } - eventListenerSource.clear() - if (AppConfig.preDownloadNum == 0) return - cacheBookJob?.cancel() - cacheBookJob = viewModelScope.launch(upTocPool) { - launch { - while (isActive && CacheBook.isRun) { - val isOnUpTocBooksEmpty = - onUpTocBooks.isEmpty() - //有目录更新是不缓存,优先更新目录,现在更多网站限制并发 - CacheBook.setWorkingState(waitUpTocBooks.isEmpty() && isOnUpTocBooksEmpty) - delay(1000) - } - } - CacheBook.startProcessJob(upTocPool) - } + FlowEventBus.post(EventBus.UP_ALL_BOOK_TOC, Unit) } fun postLoad() { @@ -270,4 +66,34 @@ class MainViewModel(application: Application) : BaseViewModel(application) { } } -} \ No newline at end of file + fun onPrefClickEvent(context: Context, event: PrefClickEvent) { + when (event) { + is PrefClickEvent.OpenUrl -> context.startActivity( + Intent( + Intent.ACTION_VIEW, + android.net.Uri.parse(event.url) + ) + ) + + is PrefClickEvent.CopyUrl -> context.sendToClip(event.url) + is PrefClickEvent.ShowMd -> { + // Handle showing MD dialog + } + + is PrefClickEvent.StartActivity -> { + context.startActivity(Intent(context, event.destination).apply { + event.configTag?.let { putExtra("configTag", it) } + }) + } + + PrefClickEvent.ExitApp -> { + if (context is androidx.activity.ComponentActivity) { + context.finish() + } + } + + else -> Unit + } + } + +} diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/BaseBookshelfFragment.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/BaseBookshelfFragment.kt deleted file mode 100644 index f52ea20d1..000000000 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/BaseBookshelfFragment.kt +++ /dev/null @@ -1,179 +0,0 @@ -package io.legado.app.ui.main.bookshelf - -import android.annotation.SuppressLint -import android.view.Menu -import android.view.MenuItem -import androidx.fragment.app.activityViewModels -import androidx.fragment.app.viewModels -import androidx.lifecycle.LiveData -import io.legado.app.R -import io.legado.app.base.VMBaseFragment -import io.legado.app.data.appDb -import io.legado.app.data.entities.Book -import io.legado.app.data.entities.BookGroup -import io.legado.app.databinding.DialogEditTextBinding -import io.legado.app.help.DirectLinkUpload -import io.legado.app.lib.dialogs.alert -import io.legado.app.ui.about.AppLogDialog -import io.legado.app.ui.book.cache.CacheActivity -import io.legado.app.ui.book.group.GroupManageDialog -import io.legado.app.ui.book.import.local.ImportBookActivity -import io.legado.app.ui.book.import.remote.RemoteBookActivity -import io.legado.app.ui.book.manage.BookshelfManageActivity -import io.legado.app.ui.book.search.SearchActivity -import io.legado.app.ui.file.HandleFileContract -import io.legado.app.ui.main.MainFragmentInterface -import io.legado.app.ui.main.MainViewModel -import io.legado.app.ui.widget.dialog.WaitDialog -import io.legado.app.utils.isAbsUrl -import io.legado.app.utils.readText -import io.legado.app.utils.sendToClip -import io.legado.app.utils.showDialogFragment -import io.legado.app.utils.startActivity -import io.legado.app.utils.toastOnUi - -abstract class BaseBookshelfFragment(layoutId: Int) : VMBaseFragment(layoutId), - MainFragmentInterface { - - override val position: Int? get() = arguments?.getInt("position") - - val activityViewModel by activityViewModels() - override val viewModel by viewModels() - - private val importBookshelf = registerForActivityResult(HandleFileContract()) { - kotlin.runCatching { - it.uri?.readText(requireContext())?.let { text -> - viewModel.importBookshelf(text, groupId) - } - }.onFailure { - toastOnUi(it.localizedMessage ?: "ERROR") - } - } - private val exportResult = registerForActivityResult(HandleFileContract()) { - it.uri?.let { uri -> - alert(R.string.export_success) { - if (uri.toString().isAbsUrl()) { - setMessage(DirectLinkUpload.getSummary()) - } - val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { - editView.hint = getString(R.string.path) - editView.setText(uri.toString()) - } - customView { alertBinding.root } - okButton { - requireContext().sendToClip(uri.toString()) - } - } - } - } - abstract val groupId: Long - abstract val books: List - private var groupsLiveData: LiveData>? = null - private val waitDialog by lazy { - WaitDialog(requireContext()).apply { - setOnCancelListener { - viewModel.addBookJob?.cancel() - } - } - } - - abstract fun gotoTop() - - override fun onCompatCreateOptionsMenu(menu: Menu) { - menuInflater.inflate(R.menu.main_bookshelf, menu) - } - - override fun onCompatOptionsItemSelected(item: MenuItem) { - super.onCompatOptionsItemSelected(item) - when (item.itemId) { - R.id.menu_remote -> startActivity() - R.id.menu_search -> startActivity() - R.id.menu_update_toc -> activityViewModel.upToc(books) - R.id.menu_bookshelf_layout -> showDialogFragment() - R.id.menu_group_manage -> showDialogFragment() - R.id.menu_add_local -> startActivity() - R.id.menu_add_url -> showAddBookByUrlAlert() - R.id.menu_bookshelf_manage -> startActivity { - putExtra("groupId", groupId) - } - - R.id.menu_download -> startActivity { - putExtra("groupId", groupId) - } - - R.id.menu_export_bookshelf -> viewModel.exportBookshelf(books) { file -> - exportResult.launch { - mode = HandleFileContract.EXPORT - fileData = - HandleFileContract.FileData("bookshelf.json", file, "application/json") - } - } - - R.id.menu_import_bookshelf -> importBookshelfAlert(groupId) - R.id.menu_log -> showDialogFragment() - } - } - - protected fun initBookGroupData() { - groupsLiveData?.removeObservers(viewLifecycleOwner) - groupsLiveData = appDb.bookGroupDao.show.apply { - observe(viewLifecycleOwner) { - upGroup(it) - } - } - } - - abstract fun upGroup(data: List) - - abstract fun upSort() - - override fun observeLiveBus() { - viewModel.addBookProgressLiveData.observe(this) { count -> - if (count < 0) { - waitDialog.dismiss() - } else { - waitDialog.setText("添加中... ($count)") - } - } - } - - @SuppressLint("InflateParams") - fun showAddBookByUrlAlert() { - alert(titleResource = R.string.add_book_url) { - val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { - editView.hint = "url" - } - customView { alertBinding.root } - okButton { - alertBinding.editView.text?.toString()?.let { - waitDialog.setText("添加中...") - waitDialog.show() - viewModel.addBookByUrl(it) - } - } - cancelButton() - } - } - - private fun importBookshelfAlert(groupId: Long) { - alert(titleResource = R.string.import_bookshelf) { - val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { - editView.hint = "url/json" - } - customView { alertBinding.root } - okButton { - alertBinding.editView.text?.toString()?.let { - viewModel.importBookshelf(it, groupId) - } - } - cancelButton() - neutralButton(R.string.select_file) { - importBookshelf.launch { - mode = HandleFileContract.FILE - allowExtensions = arrayOf("txt", "json") - } - } - } - } - -} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookItem.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookItem.kt new file mode 100644 index 000000000..ca5b173c7 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookItem.kt @@ -0,0 +1,325 @@ +package io.legado.app.ui.main.bookshelf + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.combinedClickable +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.RowScope +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shadow +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookGroup +import io.legado.app.ui.widget.components.cover.BookCover + +/** + * 通用的书架条目布局组件 + * 支持 列表/网格 模式及 紧凑/常规 样式 + */ +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun BookshelfItem( + isGrid: Boolean, + isCompact: Boolean, + cover: @Composable (Modifier) -> Unit, + title: String, + modifier: Modifier = Modifier, + subTitle: String? = null, + desc: String? = null, + extra: @Composable (RowScope.() -> Unit)? = null, + titleSmallFont: Boolean = false, + titleCenter: Boolean = true, + titleMaxLines: Int = 2, + coverShadow: Boolean = false, + onClick: () -> Unit, + onLongClick: () -> Unit +) { + if (isGrid) { + + Box( + modifier = modifier + .clip(MaterialTheme.shapes.small) + .combinedClickable( + onClick = onClick, + onLongClick = onLongClick + ) + .padding(4.dp) + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + val coverModifier = if (coverShadow) { + Modifier + .fillMaxWidth() + .shadow(4.dp, RoundedCornerShape(4.dp)) + } else { + Modifier.fillMaxWidth() + } + + Box { + cover(coverModifier) + if (isCompact) { + Text( + text = title, + style = (if (titleSmallFont) MaterialTheme.typography.labelSmall else MaterialTheme.typography.labelMedium).copy( + color = Color.White, + shadow = Shadow( + color = Color.Black.copy(alpha = 0.5f), + blurRadius = 4f + ) + ), + maxLines = 2, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .align(Alignment.BottomStart) + .fillMaxWidth() + .background( + Brush.verticalGradient( + colors = listOf( + Color.Transparent, + Color.Black.copy(alpha = 0.7f) + ) + ) + ) + .padding(horizontal = 6.dp, vertical = 6.dp) + ) + } + } + + if (!isCompact) { + Text( + text = title, + style = if (titleSmallFont) MaterialTheme.typography.labelSmall else MaterialTheme.typography.labelMedium, + maxLines = titleMaxLines, + overflow = TextOverflow.Ellipsis, + textAlign = if (titleCenter) TextAlign.Center else TextAlign.Start, + modifier = Modifier + .fillMaxWidth() + .padding(top = 4.dp) + ) + } + } + } + } else { + // 列表布局 + Column { + Row( + modifier = modifier + .fillMaxWidth() + .combinedClickable( + onClick = onClick, + onLongClick = onLongClick + ) + .padding(horizontal = 16.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + cover( + Modifier + .width(if (!isCompact) 70.dp else 55.dp) + .padding(end = 12.dp) + ) + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.Center + ) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold), + maxLines = if (!isCompact) 2 else 1, + overflow = TextOverflow.Ellipsis + ) + subTitle?.let { + Text( + text = it, + style = MaterialTheme.typography.bodySmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.padding(top = 2.dp) + ) + } + if (!isCompact) { + desc?.let { + Text( + text = it, + style = MaterialTheme.typography.bodySmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.padding(top = 2.dp) + ) + } + } + extra?.let { + Row( + modifier = Modifier.padding(top = 2.dp), + verticalAlignment = Alignment.CenterVertically, + content = it + ) + } + } + } + if (!isCompact) HorizontalDivider( + modifier = Modifier.padding(horizontal = 16.dp), + thickness = 0.5.dp, + color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f) + ) + } + } +} + +@Composable +fun BookGroupCover( + books: List, + modifier: Modifier = Modifier +) { + Box( + modifier = modifier + .aspectRatio(5f / 7f) + .clip(RoundedCornerShape(4.dp)) + .background(MaterialTheme.colorScheme.surfaceVariant) + ) { + Column(modifier = Modifier.fillMaxSize()) { + Row(modifier = Modifier.weight(1f)) { + Box( + modifier = Modifier + .weight(1f) + .fillMaxHeight() + .padding(1.dp) + ) { + books.getOrNull(0)?.let { + BookCover( + name = it.name, + author = it.author, + path = it.getDisplayCover(), + modifier = Modifier.fillMaxSize() + ) + } + } + Box( + modifier = Modifier + .weight(1f) + .fillMaxHeight() + .padding(1.dp) + ) { + books.getOrNull(1)?.let { + BookCover( + name = it.name, + author = it.author, + path = it.getDisplayCover(), + modifier = Modifier.fillMaxSize() + ) + } + } + } + Row(modifier = Modifier.weight(1f)) { + Box( + modifier = Modifier + .weight(1f) + .fillMaxHeight() + .padding(1.dp) + ) { + books.getOrNull(2)?.let { + BookCover( + name = it.name, + author = it.author, + path = it.getDisplayCover(), + modifier = Modifier.fillMaxSize() + ) + } + } + Box( + modifier = Modifier + .weight(1f) + .fillMaxHeight() + .padding(1.dp) + ) { + books.getOrNull(3)?.let { + BookCover( + name = it.name, + author = it.author, + path = it.getDisplayCover(), + modifier = Modifier.fillMaxSize() + ) + } + } + } + } + } +} + +@Composable +fun BookGroupItemGrid( + group: BookGroup, + previewBooks: List, + isCompact: Boolean = false, + titleSmallFont: Boolean = false, + titleCenter: Boolean = true, + titleMaxLines: Int = 2, + coverShadow: Boolean = false, + modifier: Modifier = Modifier, + onClick: () -> Unit, + onLongClick: () -> Unit +) { + BookshelfItem( + isGrid = true, + isCompact = isCompact, + cover = { BookGroupCover(books = previewBooks, modifier = it) }, + title = group.groupName, + modifier = modifier, + titleSmallFont = titleSmallFont, + titleCenter = titleCenter, + titleMaxLines = titleMaxLines, + coverShadow = coverShadow, + onClick = onClick, + onLongClick = onLongClick + ) +} + +@Composable +fun BookGroupItemList( + group: BookGroup, + previewBooks: List, + isCompact: Boolean = false, + titleSmallFont: Boolean = false, + titleCenter: Boolean = true, + titleMaxLines: Int = 2, + coverShadow: Boolean = false, + modifier: Modifier = Modifier, + onClick: () -> Unit, + onLongClick: () -> Unit +) { + BookshelfItem( + isGrid = false, + isCompact = isCompact, + cover = { BookGroupCover(books = previewBooks, modifier = it) }, + title = group.groupName, + titleSmallFont = titleSmallFont, + titleCenter = titleCenter, + titleMaxLines = titleMaxLines, + coverShadow = coverShadow, + modifier = modifier, + subTitle = "${previewBooks.size} 本书籍", + desc = "点击打开文件夹", + onClick = onClick, + onLongClick = onLongClick + ) +} diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfConfigBottomSheet.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfConfigBottomSheet.kt deleted file mode 100644 index 03be5771e..000000000 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfConfigBottomSheet.kt +++ /dev/null @@ -1,228 +0,0 @@ -package io.legado.app.ui.main.bookshelf - -import android.content.res.Configuration -import android.os.Bundle -import android.view.View -import androidx.fragment.app.activityViewModels -import com.google.android.material.chip.Chip -import io.legado.app.R -import io.legado.app.base.BaseBottomSheetDialogFragment -import io.legado.app.constant.EventBus -import io.legado.app.databinding.DialogBookshelfConfigBinding -import io.legado.app.help.config.AppConfig -import io.legado.app.ui.main.MainViewModel -import io.legado.app.ui.widget.number.NumberPickerDialog -import io.legado.app.utils.bookshelfLayoutGrid -import io.legado.app.utils.postEvent -import io.legado.app.utils.viewbindingdelegate.viewBinding - -class BookshelfConfigBottomSheet : BaseBottomSheetDialogFragment(R.layout.dialog_bookshelf_config) { - - private val binding by viewBinding(DialogBookshelfConfigBinding::bind) - private val activityViewModel by activityViewModels() - - override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { - - val orientation = requireContext().resources.configuration.orientation - - val bookshelfLayout = if (orientation == Configuration.ORIENTATION_LANDSCAPE) { - AppConfig.bookshelfLayoutModeLandscape - } else { - AppConfig.bookshelfLayoutModePortrait - } - - val columnCount = requireContext().bookshelfLayoutGrid.takeIf { it > 0 } ?: 1 - - val bookshelfSort = AppConfig.bookshelfSort - - binding.apply { - - resources.getStringArray(R.array.group_style).forEachIndexed { index, label -> - val chip = Chip(context).apply { - text = label - isCheckable = true - isClickable = true - id = View.generateViewId() - } - chipGroupStyle.addView(chip) - if (index == AppConfig.bookGroupStyle) { - chipGroupStyle.check(chip.id) - } - } - - resources.getStringArray(R.array.bookshelf_px_array).forEachIndexed { index, label -> - val chip = Chip(context).apply { - text = label - isCheckable = true - isClickable = true - id = index - } - chipGroupSort.addView(chip) - if (index == AppConfig.bookshelfSort) { - chipGroupSort.check(chip.id) - } - } - - swShowUnread.isChecked = AppConfig.showUnread - swShowUnreadNew.isChecked = AppConfig.showUnreadNew - swShowTip.isChecked = AppConfig.showTip - swShowLastUpdateTime.isChecked = AppConfig.showLastUpdateTime - swShowWaitUpBooks.isChecked = AppConfig.showWaitUpCount - swShowBookshelfFastScroller.isChecked = AppConfig.showBookshelfFastScroller - swShowBookshelfTabMenu.isChecked = AppConfig.shouldShowExpandButton - - when (bookshelfLayout) { - 0 -> chipGroupLayout.check(R.id.chip_list) - 1 -> chipGroupLayout.check(R.id.chip_grid) - 2 -> chipGroupLayout.check(R.id.chip_grid_compact) - 3 -> chipGroupLayout.check(R.id.chip_grid_cover) - 4 -> chipGroupLayout.check(R.id.chip_list_compact) - } - - sliderGridCount.progress = columnCount - - updateControlsEnableState() - - chipGroupLayout.setOnCheckedStateChangeListener { group, checkedIds -> - updateControlsEnableState() - } - - chipGroupStyle.setOnCheckedStateChangeListener { group, checkedIds -> - updateControlsEnableState() - } - - binding.tvValue.text = if (AppConfig.bookshelfRefreshingLimit <= 0) "无限制" - else "${AppConfig.bookshelfRefreshingLimit} 本" - - binding.tvValue.setOnClickListener { - showNumberPicker() - } - binding.layoutRefreshLimit.setOnClickListener { - showNumberPicker() - } - - when (AppConfig.bookshelfSortOrder) { - 0 -> binding.chipGroupOrder.check(binding.chipAsc.id) - 1 -> binding.chipGroupOrder.check(binding.chipDesc.id) - } - - btnOk.setOnClickListener { - var notifyMain = false - var recreate = false - - if (AppConfig.bookGroupStyle != chipGroupStyle.checkedChipId) { - AppConfig.bookGroupStyle = chipGroupStyle.indexOfChild( - chipGroupStyle.findViewById(chipGroupStyle.checkedChipId) - ) - notifyMain = true - } - - if (AppConfig.showUnread != swShowUnread.isChecked) { - AppConfig.showUnread = swShowUnread.isChecked - postEvent(EventBus.BOOKSHELF_REFRESH, "") - } - if (AppConfig.showUnreadNew != swShowUnreadNew.isChecked) { - AppConfig.showUnreadNew = swShowUnreadNew.isChecked - postEvent(EventBus.BOOKSHELF_REFRESH, "") - } - if (AppConfig.showTip != swShowTip.isChecked) { - AppConfig.showTip = swShowTip.isChecked - postEvent(EventBus.BOOKSHELF_REFRESH, "") - } - if (AppConfig.showLastUpdateTime != swShowLastUpdateTime.isChecked) { - AppConfig.showLastUpdateTime = swShowLastUpdateTime.isChecked - postEvent(EventBus.BOOKSHELF_REFRESH, "") - } - if (AppConfig.showWaitUpCount != swShowWaitUpBooks.isChecked) { - AppConfig.showWaitUpCount = swShowWaitUpBooks.isChecked - activityViewModel.postUpBooksLiveData(true) - } - if (AppConfig.showBookshelfFastScroller != swShowBookshelfFastScroller.isChecked) { - AppConfig.showBookshelfFastScroller = swShowBookshelfFastScroller.isChecked - postEvent(EventBus.BOOKSHELF_REFRESH, "") - } - if (AppConfig.shouldShowExpandButton != swShowBookshelfTabMenu.isChecked) { - AppConfig.shouldShowExpandButton = swShowBookshelfTabMenu.isChecked - postEvent(EventBus.BOOKSHELF_REFRESH, "") - } - if (bookshelfSort != chipGroupSort.checkedChipId) { - AppConfig.bookshelfSort = chipGroupSort.checkedChipId - (requireParentFragment() as? BaseBookshelfFragment)?.upSort() - } - - val selectedColumn = sliderGridCount.progress - val newLayout = when (chipGroupLayout.checkedChipId) { - R.id.chip_list -> 0 - R.id.chip_grid -> 1 - R.id.chip_grid_compact -> 2 - R.id.chip_grid_cover -> 3 - R.id.chip_list_compact -> 4 - else -> 0 - } - - val oldColumn = requireContext().bookshelfLayoutGrid - if (bookshelfLayout != newLayout || oldColumn != selectedColumn) { - if (orientation == Configuration.ORIENTATION_LANDSCAPE) { - AppConfig.bookshelfLayoutModeLandscape = newLayout - AppConfig.bookshelfLayoutGridLandscape = selectedColumn - } else { - AppConfig.bookshelfLayoutModePortrait = newLayout - AppConfig.bookshelfLayoutGridPortrait = selectedColumn - } - - if (newLayout == 0 || newLayout == 4) { - activityViewModel.booksGridRecycledViewPool.clear() - } else { - activityViewModel.booksListRecycledViewPool.clear() - } - recreate = true - } - - val newOrder = when (binding.chipGroupOrder.checkedChipId) { - binding.chipAsc.id -> 0 - binding.chipDesc.id -> 1 - else -> 1 - } - if (AppConfig.bookshelfSortOrder != newOrder) { - AppConfig.bookshelfSortOrder = newOrder - (requireParentFragment() as? BaseBookshelfFragment)?.upSort() - } - - if (recreate) { - dismiss() - postEvent(EventBus.RECREATE, "") - } else if (notifyMain) { - dismiss() - postEvent(EventBus.NOTIFY_MAIN, false) - } - dismiss() - } - - btnCancel.setOnClickListener { - dismiss() - } - } - } - private fun showNumberPicker() { - NumberPickerDialog(requireContext()) - .setTitle("书架更新数量限制") - .setMinValue(0) // 0 表示无限制 - .setMaxValue(500) // 可根据需求设定最大值 - .setValue(AppConfig.bookshelfRefreshingLimit) - .show { selected -> - AppConfig.bookshelfRefreshingLimit = selected - binding.tvValue.text = if (selected <= 0) "无限制" else "$selected 本" - } - } - - private fun updateControlsEnableState() { - val checkedLayoutId = binding.chipGroupLayout.checkedChipId - val isListMode = checkedLayoutId == R.id.chip_list || checkedLayoutId == R.id.chip_list_compact - binding.swShowLastUpdateTime.isEnabled = isListMode - - - val checkedStyleChip = binding.chipGroupStyle.findViewById(binding.chipGroupStyle.checkedChipId) - val isGroupStyle0 = binding.chipGroupStyle.indexOfChild(checkedStyleChip) == 0 - binding.swShowBookshelfTabMenu.isEnabled = isGroupStyle0 - } -} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfConfigSheet.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfConfigSheet.kt new file mode 100644 index 000000000..630c4b2d8 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfConfigSheet.kt @@ -0,0 +1,228 @@ +package io.legado.app.ui.main.bookshelf + +import android.content.res.Configuration +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.Sort +import androidx.compose.material.icons.automirrored.filled.ViewList +import androidx.compose.material.icons.filled.BlurOn +import androidx.compose.material.icons.filled.History +import androidx.compose.material.icons.filled.Info +import androidx.compose.material.icons.filled.Menu +import androidx.compose.material.icons.filled.Notifications +import androidx.compose.material.icons.filled.SortByAlpha +import androidx.compose.material.icons.filled.Style +import androidx.compose.material.icons.filled.TextFormat +import androidx.compose.material.icons.filled.ViewCompact +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.res.stringArrayResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import io.legado.app.R +import io.legado.app.ui.config.bookshelfConfig.BookshelfConfig +import io.legado.app.ui.widget.components.modalBottomSheet.GlassModalBottomSheet +import io.legado.app.ui.widget.components.settingItem.CompactDropdownSettingItem +import io.legado.app.ui.widget.components.settingItem.CompactSliderSettingItem +import io.legado.app.ui.widget.components.settingItem.SwitchSettingItem + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun BookshelfConfigSheet( + onDismissRequest: () -> Unit +) { + val configuration = LocalConfiguration.current + val isLandscape = configuration.orientation == Configuration.ORIENTATION_LANDSCAPE + + GlassModalBottomSheet(onDismissRequest = onDismissRequest) { + Column( + modifier = Modifier + .fillMaxWidth() + .verticalScroll(rememberScrollState()) + .padding(horizontal = 16.dp) + .padding(bottom = 32.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = stringResource(R.string.bookshelf_layout), + style = MaterialTheme.typography.titleLarge, + modifier = Modifier.padding(vertical = 16.dp) + ) + + // Group Style + CompactDropdownSettingItem( + title = stringResource(R.string.group_style), + selectedValue = BookshelfConfig.bookGroupStyle.toString(), + displayEntries = stringArrayResource(R.array.group_style), + entryValues = Array(stringArrayResource(R.array.group_style).size) { it.toString() }, + imageVector = Icons.Default.Style, + onValueChange = { BookshelfConfig.bookGroupStyle = it.toInt() } + ) + + // Sort + CompactDropdownSettingItem( + title = stringResource(R.string.sort), + selectedValue = BookshelfConfig.bookshelfSort.toString(), + displayEntries = stringArrayResource(R.array.bookshelf_px_array), + entryValues = Array(stringArrayResource(R.array.bookshelf_px_array).size) { it.toString() }, + imageVector = Icons.AutoMirrored.Filled.Sort, + onValueChange = { BookshelfConfig.bookshelfSort = it.toInt() } + ) + + // Sort Order + CompactDropdownSettingItem( + title = "排序方向", + selectedValue = BookshelfConfig.bookshelfSortOrder.toString(), + displayEntries = arrayOf( + stringResource(R.string.ascending_order), + stringResource(R.string.descending_order) + ), + entryValues = arrayOf("0", "1"), + imageVector = Icons.Default.SortByAlpha, + onValueChange = { BookshelfConfig.bookshelfSortOrder = it.toInt() } + ) + + // Layout Mode + val layoutMode = + if (isLandscape) BookshelfConfig.bookshelfLayoutModeLandscape else BookshelfConfig.bookshelfLayoutModePortrait + CompactDropdownSettingItem( + title = "布局模式", + description = stringResource(if (isLandscape) R.string.screen_landscape else R.string.screen_portrait), + selectedValue = layoutMode.toString(), + displayEntries = arrayOf("列表", "网格"), + entryValues = arrayOf("0", "1"), + imageVector = Icons.AutoMirrored.Filled.ViewList, + onValueChange = { + if (isLandscape) BookshelfConfig.bookshelfLayoutModeLandscape = it.toInt() + else BookshelfConfig.bookshelfLayoutModePortrait = it.toInt() + } + ) + + SwitchSettingItem( + title = "紧凑模式", + checked = BookshelfConfig.bookshelfLayoutCompact, + imageVector = Icons.Default.ViewCompact, + color = MaterialTheme.colorScheme.surface, + onCheckedChange = { BookshelfConfig.bookshelfLayoutCompact = it } + ) + + // Grid Count + val gridCount = + if (isLandscape) BookshelfConfig.bookshelfLayoutGridLandscape else BookshelfConfig.bookshelfLayoutGridPortrait + CompactSliderSettingItem( + title = stringResource(R.string.number_rows_columns), + value = gridCount.toFloat(), + valueRange = 1f..15f, + steps = 14, + onValueChange = { + if (isLandscape) BookshelfConfig.bookshelfLayoutGridLandscape = it.toInt() + else BookshelfConfig.bookshelfLayoutGridPortrait = it.toInt() + } + ) + + SwitchSettingItem( + title = "标题小字体", + checked = BookshelfConfig.bookshelfTitleSmallFont, + imageVector = Icons.Default.TextFormat, + color = MaterialTheme.colorScheme.surface, + onCheckedChange = { BookshelfConfig.bookshelfTitleSmallFont = it } + ) + + SwitchSettingItem( + title = "标题居中", + checked = BookshelfConfig.bookshelfTitleCenter, + color = MaterialTheme.colorScheme.surface, + onCheckedChange = { BookshelfConfig.bookshelfTitleCenter = it } + ) + + CompactSliderSettingItem( + title = "标题最大行数", + value = BookshelfConfig.bookshelfTitleMaxLines.toFloat(), + valueRange = 1f..5f, + steps = 4, + onValueChange = { BookshelfConfig.bookshelfTitleMaxLines = it.toInt() } + ) + + SwitchSettingItem( + title = "封面阴影", + checked = BookshelfConfig.bookshelfCoverShadow, + imageVector = Icons.Default.BlurOn, + color = MaterialTheme.colorScheme.surface, + onCheckedChange = { BookshelfConfig.bookshelfCoverShadow = it } + ) + + // Switches + SwitchSettingItem( + title = stringResource(R.string.show_unread), + checked = BookshelfConfig.showUnread, + imageVector = Icons.Default.Notifications, + color = MaterialTheme.colorScheme.surface, + onCheckedChange = { BookshelfConfig.showUnread = it } + ) + + SwitchSettingItem( + title = stringResource(R.string.show_unread_new), + checked = BookshelfConfig.showUnreadNew, + color = MaterialTheme.colorScheme.surface, + onCheckedChange = { BookshelfConfig.showUnreadNew = it } + ) + + SwitchSettingItem( + title = stringResource(R.string.show_tip), + checked = BookshelfConfig.showTip, + imageVector = Icons.Default.Info, + color = MaterialTheme.colorScheme.surface, + onCheckedChange = { BookshelfConfig.showTip = it } + ) + + SwitchSettingItem( + title = stringResource(R.string.show_last_update_time), + checked = BookshelfConfig.showLastUpdateTime, + imageVector = Icons.Default.History, + color = MaterialTheme.colorScheme.surface, + onCheckedChange = { BookshelfConfig.showLastUpdateTime = it } + ) + + SwitchSettingItem( + title = stringResource(R.string.show_wait_up_count), + checked = BookshelfConfig.showWaitUpCount, + color = MaterialTheme.colorScheme.surface, + onCheckedChange = { BookshelfConfig.showWaitUpCount = it } + ) + + SwitchSettingItem( + title = stringResource(R.string.show_bookshelf_fast_scroller), + checked = BookshelfConfig.showBookshelfFastScroller, + color = MaterialTheme.colorScheme.surface, + onCheckedChange = { BookshelfConfig.showBookshelfFastScroller = it } + ) + + SwitchSettingItem( + title = stringResource(R.string.show_bookshelf_tab_menu), + checked = BookshelfConfig.shouldShowExpandButton, + imageVector = Icons.Default.Menu, + color = MaterialTheme.colorScheme.surface, + onCheckedChange = { BookshelfConfig.shouldShowExpandButton = it } + ) + + // Refresh Limit + CompactSliderSettingItem( + title = stringResource(R.string.bookshelf_update_limit), + description = if (BookshelfConfig.bookshelfRefreshingLimit <= 0) "无限制" else "${BookshelfConfig.bookshelfRefreshingLimit} 本", + value = BookshelfConfig.bookshelfRefreshingLimit.toFloat(), + valueRange = 0f..100f, + steps = 100, + onValueChange = { BookshelfConfig.bookshelfRefreshingLimit = it.toInt() } + ) + } + } +} 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 new file mode 100644 index 000000000..f1a77b13c --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt @@ -0,0 +1,534 @@ +package io.legado.app.ui.main.bookshelf + +import androidx.activity.compose.BackHandler +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Download +import androidx.compose.material.icons.filled.Edit +import androidx.compose.material.icons.filled.FileOpen +import androidx.compose.material.icons.filled.GridView +import androidx.compose.material.icons.filled.History +import androidx.compose.material.icons.filled.ImportExport +import androidx.compose.material.icons.filled.Link +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material.icons.filled.Save +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material.icons.filled.Wifi +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SecondaryScrollableTabRow +import androidx.compose.material3.Surface +import androidx.compose.material3.Tab +import androidx.compose.material3.Text +import androidx.compose.material3.pulltorefresh.PullToRefreshBox +import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults +import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import io.legado.app.R +import io.legado.app.data.entities.Book +import io.legado.app.help.book.isLocal +import io.legado.app.ui.book.cache.CacheActivity +import io.legado.app.ui.book.import.local.ImportBookActivity +import io.legado.app.ui.book.import.remote.RemoteBookActivity +import io.legado.app.ui.book.manage.BookshelfManageActivity +import io.legado.app.ui.book.search.SearchActivity +import io.legado.app.ui.config.bookshelfConfig.BookshelfConfig +import io.legado.app.ui.file.HandleFileContract +import io.legado.app.ui.widget.components.cover.BookCoverWithProgress +import io.legado.app.ui.widget.components.filePicker.FilePickerSheet +import io.legado.app.ui.widget.components.importComponents.SourceInputDialog +import io.legado.app.ui.widget.components.list.ListScaffold +import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem +import io.legado.app.utils.readText +import io.legado.app.utils.startActivity +import io.legado.app.utils.toTimeAgo +import io.legado.app.utils.toastOnUi +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import org.koin.androidx.compose.koinViewModel + +@OptIn( + ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class, + ExperimentalMaterial3ExpressiveApi::class +) +@Composable +fun BookshelfScreen( + viewModel: BookshelfViewModel = koinViewModel(), + onBookClick: (Book) -> Unit, + onBookLongClick: (Book) -> Unit +) { + val context = LocalContext.current + val uiState by viewModel.uiState.collectAsState() + val scope = rememberCoroutineScope() + + var showAddUrlDialog by remember { mutableStateOf(false) } + var showImportSheet by remember { mutableStateOf(false) } + var showConfigSheet by remember { mutableStateOf(false) } + var showGroupManageSheet by remember { mutableStateOf(false) } + + val importLauncher = rememberLauncherForActivityResult(HandleFileContract()) { + runCatching { + it.uri?.readText(context)?.let { text -> + val groupId = uiState.groups.getOrNull(uiState.selectedGroupIndex)?.groupId ?: -1L + viewModel.importBookshelf(text, groupId) + } + }.onFailure { + context.toastOnUi(it.localizedMessage ?: "ERROR") + } + } + + val pagerState = rememberPagerState( + initialPage = uiState.selectedGroupIndex, + pageCount = { uiState.groups.size } + ) + + LaunchedEffect(pagerState) { + snapshotFlow { pagerState.currentPage }.collect { page -> + if (uiState.groups.isNotEmpty() && page < uiState.groups.size) { + viewModel.changeGroup(uiState.groups[page].groupId) + } + } + } + + LaunchedEffect(uiState.selectedGroupIndex) { + if (uiState.selectedGroupIndex != pagerState.currentPage && uiState.selectedGroupIndex < pagerState.pageCount) { + pagerState.scrollToPage(uiState.selectedGroupIndex) + } + } + + val bookGroupStyle = BookshelfConfig.bookGroupStyle + // 控制是否处于“文件夹列表”根视图,还是“文件夹内部”书籍视图 + var isInFolderRoot by remember(bookGroupStyle) { mutableStateOf(bookGroupStyle == 2) } + val title = if (bookGroupStyle == 2 && uiState.groups.isNotEmpty()) { + if (isInFolderRoot) "书架" + else uiState.groups.getOrNull(pagerState.currentPage)?.groupName ?: "书架" + } else { + "书架" + } + + if (bookGroupStyle == 2 && !isInFolderRoot) { + BackHandler { + isInFolderRoot = true + } + } + + ListScaffold( + title = title, + state = uiState, + onSearchToggle = { context.startActivity() }, + onSearchQueryChange = { viewModel.setSearchKey(it) }, + topBarActions = { }, + dropDownMenuContent = { dismiss -> + RoundDropdownMenuItem( + text = { Text(stringResource(R.string.add_remote_book)) }, + onClick = { context.startActivity(); dismiss() }, + leadingIcon = { Icon(Icons.Default.Wifi, null) } + ) + RoundDropdownMenuItem( + text = { Text(stringResource(R.string.book_local)) }, + onClick = { context.startActivity(); dismiss() }, + leadingIcon = { Icon(Icons.Default.Save, null) } + ) + RoundDropdownMenuItem( + text = { Text(stringResource(R.string.update_toc)) }, + onClick = { viewModel.upToc(uiState.items); dismiss() }, + leadingIcon = { Icon(Icons.Default.Refresh, null) } + ) + RoundDropdownMenuItem( + text = { Text("布局设置") }, + onClick = { showConfigSheet = true; dismiss() }, + leadingIcon = { Icon(Icons.Default.GridView, null) } + ) + RoundDropdownMenuItem( + text = { Text(stringResource(R.string.group_manage)) }, + onClick = { showGroupManageSheet = true; dismiss() }, + leadingIcon = { Icon(Icons.Default.Edit, null) } + ) + RoundDropdownMenuItem( + text = { Text(stringResource(R.string.add_url)) }, + onClick = { showAddUrlDialog = true; dismiss() }, + leadingIcon = { Icon(Icons.Default.Link, null) } + ) + RoundDropdownMenuItem( + text = { Text(stringResource(R.string.bookshelf_management)) }, + onClick = { + val groupId = + uiState.groups.getOrNull(uiState.selectedGroupIndex)?.groupId ?: -1L + context.startActivity { + putExtra("groupId", groupId) + } + dismiss() + }, + leadingIcon = { Icon(Icons.Default.Settings, null) } + ) + RoundDropdownMenuItem( + text = { Text(stringResource(R.string.cache_export)) }, + onClick = { + val groupId = + uiState.groups.getOrNull(uiState.selectedGroupIndex)?.groupId ?: -1L + context.startActivity { + putExtra("groupId", groupId) + } + dismiss() + }, + leadingIcon = { Icon(Icons.Default.Download, null) } + ) + RoundDropdownMenuItem( + text = { Text(stringResource(R.string.export_bookshelf)) }, + onClick = { + dismiss() + }, + leadingIcon = { Icon(Icons.Default.ImportExport, null) } + ) + RoundDropdownMenuItem( + text = { Text(stringResource(R.string.import_bookshelf)) }, + onClick = { showImportSheet = true; dismiss() }, + leadingIcon = { Icon(Icons.Default.FileOpen, null) } + ) + RoundDropdownMenuItem( + text = { Text(stringResource(R.string.log)) }, + onClick = { + dismiss() + }, + leadingIcon = { Icon(Icons.Default.History, null) } + ) + }, + bottomContent = if (bookGroupStyle == 0) { + { + if (uiState.groups.isNotEmpty()) { + SecondaryScrollableTabRow( + selectedTabIndex = pagerState.currentPage, + edgePadding = 16.dp, + containerColor = Color.Transparent, + divider = {} + ) { + uiState.groups.forEachIndexed { index, group -> + Tab( + selected = pagerState.currentPage == index, + onClick = { + scope.launch { pagerState.animateScrollToPage(index) } + }, + text = { Text(group.groupName) } + ) + } + } + } + } + } else null + ) { paddingValues -> + var isRefreshing by remember { mutableStateOf(false) } + val pullToRefreshState = rememberPullToRefreshState() + + PullToRefreshBox( + isRefreshing = isRefreshing, + onRefresh = { + scope.launch { + isRefreshing = true + viewModel.upAllBookToc() + delay(1000) + isRefreshing = false + } + }, + state = pullToRefreshState, + modifier = Modifier.fillMaxSize(), + indicator = { + PullToRefreshDefaults.LoadingIndicator( + state = pullToRefreshState, + isRefreshing = isRefreshing, + modifier = Modifier + .align(Alignment.TopCenter) + .padding(top = paddingValues.calculateTopPadding()) + ) + } + ) { + if (bookGroupStyle == 2 && isInFolderRoot) { + + if (uiState.bookshelfLayoutMode == 0) { + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues( + top = paddingValues.calculateTopPadding(), + bottom = 56.dp + ) + ) { + items(uiState.groups) { group -> + BookGroupItemList( + group = group, + previewBooks = uiState.groupPreviews[group.groupId] ?: emptyList(), + isCompact = uiState.bookshelfLayoutCompact, + titleSmallFont = uiState.titleSmallFont, + titleCenter = uiState.titleCenter, + titleMaxLines = uiState.titleMaxLines, + onClick = { + val index = uiState.groups.indexOf(group) + if (index != -1) { + scope.launch { pagerState.scrollToPage(index) } + isInFolderRoot = false + } + }, + onLongClick = { showGroupManageSheet = true } + ) + } + } + } else { + LazyVerticalGrid( + columns = GridCells.Fixed(uiState.bookshelfLayoutGrid), + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues( + top = paddingValues.calculateTopPadding(), + bottom = paddingValues.calculateBottomPadding() + 16.dp, + start = 12.dp, + end = 12.dp + ), + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + items(uiState.groups) { group -> + + BookGroupItemGrid( + group = group, + previewBooks = uiState.groupPreviews[group.groupId] ?: emptyList(), + isCompact = uiState.bookshelfLayoutCompact, + titleSmallFont = uiState.titleSmallFont, + titleCenter = uiState.titleCenter, + titleMaxLines = uiState.titleMaxLines, + coverShadow = uiState.coverShadow, + onClick = { + val index = uiState.groups.indexOf(group) + if (index != -1) { + scope.launch { pagerState.scrollToPage(index) } + isInFolderRoot = false + } + }, + onLongClick = { showGroupManageSheet = true } + ) + } + } + } + } else { + HorizontalPager( + state = pagerState, + modifier = Modifier + .fillMaxSize(), + beyondViewportPageCount = 1 + ) { + BookshelfPage( + paddingValues = paddingValues, + uiState = uiState, + onBookClick = onBookClick, + onBookLongClick = onBookLongClick + ) + } + } + } + } + + if (showConfigSheet) { + BookshelfConfigSheet(onDismissRequest = { showConfigSheet = false }) + } + + if (showGroupManageSheet) { + GroupManageSheet(onDismissRequest = { showGroupManageSheet = false }) + } + + if (showAddUrlDialog) { + SourceInputDialog( + title = stringResource(R.string.add_book_url), + onDismissRequest = { showAddUrlDialog = false }, + onConfirm = { url -> + viewModel.addBookByUrl(url) + showAddUrlDialog = false + } + ) + } + + if (showImportSheet) { + FilePickerSheet( + onDismissRequest = { showImportSheet = false }, + title = stringResource(R.string.import_bookshelf), + onSelectSysFile = { types -> + importLauncher.launch { + mode = HandleFileContract.FILE + allowExtensions = types + } + showImportSheet = false + }, + onManualInput = { + showAddUrlDialog = true + showImportSheet = false + } + ) + } + + if (uiState.isLoading) { + Dialog(onDismissRequest = {}) { + Surface( + shape = MaterialTheme.shapes.medium, + color = MaterialTheme.colorScheme.surfaceContainerHigh + ) { + Column( + modifier = Modifier.padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + CircularProgressIndicator() + uiState.loadingText?.let { + Text( + text = it, + modifier = Modifier.padding(top = 16.dp), + style = MaterialTheme.typography.bodyMedium + ) + } + } + } + } + } +} + +@Composable +fun BookshelfPage( + paddingValues: PaddingValues, + uiState: BookshelfUiState, + onBookClick: (Book) -> Unit, + onBookLongClick: (Book) -> Unit +) { + if (uiState.bookshelfLayoutMode == 0) { + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues( + top = paddingValues.calculateTopPadding(), + bottom = 56.dp + ) + ) { + items(uiState.items, key = { it.bookUrl }) { book -> + BookItem( + book = book, + layoutMode = uiState.bookshelfLayoutMode, + isCompact = uiState.bookshelfLayoutCompact, + isUpdating = uiState.updatingBooks.contains(book.bookUrl), + onClick = { onBookClick(book) }, + onLongClick = { onBookLongClick(book) } + ) + } + } + } else { + LazyVerticalGrid( + columns = GridCells.Fixed(uiState.bookshelfLayoutGrid), + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues( + top = paddingValues.calculateTopPadding(), + bottom = paddingValues.calculateBottomPadding() + 16.dp, + start = 12.dp, + end = 12.dp + ), + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + items(uiState.items, key = { it.bookUrl }) { book -> + BookItem( + book = book, + layoutMode = uiState.bookshelfLayoutMode, + isCompact = uiState.bookshelfLayoutCompact, + isUpdating = uiState.updatingBooks.contains(book.bookUrl), + titleSmallFont = uiState.titleSmallFont, + titleCenter = uiState.titleCenter, + titleMaxLines = uiState.titleMaxLines, + coverShadow = uiState.coverShadow, + onClick = { onBookClick(book) }, + onLongClick = { onBookLongClick(book) } + ) + } + } + } +} + +@Composable +fun BookItem( + book: Book, + layoutMode: Int, + isCompact: Boolean = false, + isUpdating: Boolean = false, + titleSmallFont: Boolean = false, + titleCenter: Boolean = true, + titleMaxLines: Int = 2, + coverShadow: Boolean = false, + onClick: () -> Unit, + onLongClick: () -> Unit +) { + val unreadCount = (book.totalChapterNum - book.durChapterIndex).coerceAtLeast(0) + + BookshelfItem( + isGrid = layoutMode != 0, + isCompact = isCompact, + cover = { modifier -> + BookCoverWithProgress( + name = book.name, + author = book.author, + path = book.getDisplayCover(), + isUpdating = isUpdating, + modifier = modifier + ) + }, + title = book.name, + subTitle = if (isCompact && layoutMode == 0) { + stringResource(R.string.author_read, book.author, unreadCount) + } else { + book.author + }, + desc = stringResource(R.string.read_dur_progress, book.durChapterTitle ?: ""), + extra = { + if (BookshelfConfig.showLastUpdateTime && !book.isLocal) { + Text( + text = book.latestChapterTime.toTimeAgo(), + style = MaterialTheme.typography.bodySmall, + color = if (!isCompact) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.secondary, + modifier = Modifier.padding(end = 4.dp) + ) + } + Text( + text = book.latestChapterTitle ?: "", + style = MaterialTheme.typography.bodySmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f) + ) + }, + titleSmallFont = titleSmallFont, + titleCenter = titleCenter, + titleMaxLines = titleMaxLines, + coverShadow = coverShadow, + onClick = onClick, + onLongClick = onLongClick + ) +} diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfUiState.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfUiState.kt new file mode 100644 index 000000000..cc4f719e9 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfUiState.kt @@ -0,0 +1,26 @@ +package io.legado.app.ui.main.bookshelf + +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookGroup +import io.legado.app.ui.config.bookshelfConfig.BookshelfConfig +import io.legado.app.ui.widget.components.list.ListUiState + +data class BookshelfUiState( + override val items: List = emptyList(), + override val selectedIds: Set = emptySet(), + override val searchKey: String = "", + override val isSearch: Boolean = false, + override val isLoading: Boolean = false, + val groups: List = emptyList(), + val groupPreviews: Map> = emptyMap(), + val selectedGroupIndex: Int = 0, + val loadingText: String? = null, + val bookshelfLayoutMode: Int = 1, + val bookshelfLayoutGrid: Int = 3, + val bookshelfLayoutCompact: Boolean = BookshelfConfig.bookshelfLayoutCompact, + val titleSmallFont: Boolean = BookshelfConfig.bookshelfTitleSmallFont, + val titleCenter: Boolean = BookshelfConfig.bookshelfTitleCenter, + val titleMaxLines: Int = BookshelfConfig.bookshelfTitleMaxLines, + val coverShadow: Boolean = BookshelfConfig.bookshelfCoverShadow, + val updatingBooks: Set = emptySet() +) : ListUiState diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt index fbab5efcc..f010f5d8e 100644 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt @@ -1,42 +1,416 @@ package io.legado.app.ui.main.bookshelf import android.app.Application -import androidx.lifecycle.MutableLiveData +import android.content.res.Configuration +import androidx.lifecycle.asFlow +import androidx.lifecycle.viewModelScope import com.google.gson.stream.JsonWriter import io.legado.app.R import io.legado.app.base.BaseViewModel +import io.legado.app.constant.AppConst import io.legado.app.constant.AppLog +import io.legado.app.constant.BookType +import io.legado.app.constant.EventBus import io.legado.app.data.appDb import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookGroup +import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.BookSourcePart import io.legado.app.exception.NoStackTraceException +import io.legado.app.help.book.BookHelp +import io.legado.app.help.book.addType +import io.legado.app.help.book.isLocal +import io.legado.app.help.book.isUpError +import io.legado.app.help.book.removeType +import io.legado.app.help.book.sync import io.legado.app.help.config.AppConfig import io.legado.app.help.coroutine.Coroutine import io.legado.app.help.http.decompressed import io.legado.app.help.http.newCallResponseBody import io.legado.app.help.http.okHttpClient import io.legado.app.help.http.text +import io.legado.app.model.CacheBook +import io.legado.app.model.ReadBook +import io.legado.app.model.SourceCallBack import io.legado.app.model.webBook.WebBook +import io.legado.app.service.CacheBookService +import io.legado.app.ui.config.bookshelfConfig.BookshelfConfig import io.legado.app.utils.FileUtils import io.legado.app.utils.GSON import io.legado.app.utils.NetworkUtils +import io.legado.app.utils.cnCompare +import io.legado.app.utils.eventBus.FlowEventBus import io.legado.app.utils.fromJsonArray import io.legado.app.utils.isAbsUrl import io.legado.app.utils.isJsonArray +import io.legado.app.utils.onEachParallel +import io.legado.app.utils.postEvent import io.legado.app.utils.printOnDebug import io.legado.app.utils.toastOnUi +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.Job +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.delay +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit import java.io.File import java.io.FileOutputStream import java.io.OutputStreamWriter +import java.util.LinkedList +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.Executors +import kotlin.math.max +import kotlin.math.min class BookshelfViewModel(application: Application) : BaseViewModel(application) { - val addBookProgressLiveData = MutableLiveData(-1) var addBookJob: Coroutine<*>? = null + private val groupIdFlow = MutableStateFlow(BookGroup.IdAll) + private val searchKeyFlow = MutableStateFlow("") + private val refreshTrigger = MutableStateFlow(0) + private val loadingTextFlow = MutableStateFlow(null) + + // 更新相关 + private var threadCount = AppConfig.threadCount + private var poolSize = min(threadCount, AppConst.MAX_THREAD) + private var upTocPool = Executors.newFixedThreadPool(poolSize).asCoroutineDispatcher() + private val waitUpTocBooks = LinkedList() + private val onUpTocBooks = ConcurrentHashMap.newKeySet() + private val updatingBooksFlow = MutableStateFlow>(emptySet()) + private var upTocJob: Job? = null + private var cacheBookJob: Job? = null + private val eventListenerSource = ConcurrentHashMap() + + val scrollTrigger = MutableSharedFlow(extraBufferCapacity = 1) + + val groupsFlow = appDb.bookGroupDao.show.asFlow() + + @OptIn(ExperimentalCoroutinesApi::class) + val booksFlow = combine(groupIdFlow, refreshTrigger) { groupId, _ -> groupId } + .flatMapLatest { groupId -> + appDb.bookDao.flowByGroup(groupId).map { list -> + sortBooks(list, groupId) + } + }.flowOn(Dispatchers.Default) + + private val internalStateFlow = combine( + groupIdFlow, + searchKeyFlow, + loadingTextFlow, + updatingBooksFlow + ) { groupId, searchKey, loadingText, updatingBooks -> + InternalState(groupId, searchKey, loadingText, updatingBooks) + } + + data class InternalState( + val groupId: Long, + val searchKey: String, + val loadingText: String?, + val updatingBooks: Set + ) + + val uiState: StateFlow = combine( + booksFlow, + groupsFlow, + internalStateFlow + ) { books, groups, internal -> + val filteredBooks = if (internal.searchKey.isEmpty()) { + books + } else { + books.filter { + it.name.contains( + internal.searchKey, + true + ) || it.author.contains(internal.searchKey, true) + } + } + val currentGroups = groups ?: emptyList() + val isLandscape = + context.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE + + // 计算分组预览图 + val previews = mutableMapOf>() + if (BookshelfConfig.bookGroupStyle in 2..3) { + currentGroups.forEach { group -> + val groupBooks = if (group.groupId == BookGroup.IdAll) { + books.take(4) + } else { + books.filter { (it.group and group.groupId) != 0L }.take(4) + } + previews[group.groupId] = groupBooks + } + } + + BookshelfUiState( + items = filteredBooks, + groups = currentGroups, + groupPreviews = previews, + selectedGroupIndex = currentGroups.indexOfFirst { it.groupId == internal.groupId } + .coerceAtLeast(0), + searchKey = internal.searchKey, + isSearch = internal.searchKey.isNotEmpty(), + isLoading = internal.loadingText != null, + loadingText = internal.loadingText, + bookshelfLayoutMode = if (isLandscape) BookshelfConfig.bookshelfLayoutModeLandscape else BookshelfConfig.bookshelfLayoutModePortrait, + bookshelfLayoutGrid = if (isLandscape) BookshelfConfig.bookshelfLayoutGridLandscape else BookshelfConfig.bookshelfLayoutGridPortrait, + bookshelfLayoutCompact = BookshelfConfig.bookshelfLayoutCompact, + titleSmallFont = BookshelfConfig.bookshelfTitleSmallFont, + titleCenter = BookshelfConfig.bookshelfTitleCenter, + titleMaxLines = BookshelfConfig.bookshelfTitleMaxLines, + coverShadow = BookshelfConfig.bookshelfCoverShadow, + updatingBooks = internal.updatingBooks + ) + }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), BookshelfUiState()) + + init { + viewModelScope.launch { + FlowEventBus.with(EventBus.UP_ALL_BOOK_TOC).collect { + upAllBookToc() + } + } + viewModelScope.launch { + FlowEventBus.with(EventBus.BOOKSHELF_REFRESH).collect { + refresh() + } + } + + if (BookshelfConfig.autoRefreshBook) { + upAllBookToc() + } + } + + override fun onCleared() { + super.onCleared() + upTocPool.close() + } + + private fun sortBooks(list: List, groupId: Long): List { + val group = appDb.bookGroupDao.getByID(groupId) + val bookSort = group?.getRealBookSort() ?: AppConfig.bookshelfSort + val isDescending = AppConfig.bookshelfSortOrder == 1 + + return when (bookSort) { + 1 -> if (isDescending) list.sortedByDescending { it.latestChapterTime } + else list.sortedBy { it.latestChapterTime } + + 2 -> if (isDescending) + list.sortedWith { o1, o2 -> o2.name.cnCompare(o1.name) } + else + list.sortedWith { o1, o2 -> o1.name.cnCompare(o2.name) } + + 3 -> if (isDescending) list.sortedByDescending { it.order } + else list.sortedBy { it.order } + + 4 -> if (isDescending) list.sortedByDescending { + max( + it.latestChapterTime, + it.durChapterTime + ) + } + else list.sortedBy { max(it.latestChapterTime, it.durChapterTime) } + + 5 -> if (isDescending) + list.sortedWith { o1, o2 -> o2.author.cnCompare(o1.author) } + else + list.sortedWith { o1, o2 -> o1.author.cnCompare(o2.author) } + + else -> if (isDescending) list.sortedByDescending { it.durChapterTime } + else list.sortedBy { it.durChapterTime } + } + } + + fun changeGroup(groupId: Long) { + groupIdFlow.value = groupId + } + + fun setSearchKey(key: String) { + searchKeyFlow.value = key + } + + fun refresh() { + refreshTrigger.value++ + } + + fun gotoTop() { + scrollTrigger.tryEmit(Unit) + } + + // 更新逻辑移入 + fun upAllBookToc() { + execute { + addToWaitUp(appDb.bookDao.hasUpdateBooks) + } + } + + fun upToc(books: List) { + execute(context = upTocPool) { + books.filter { + !it.isLocal && it.canUpdate + }.let { + addToWaitUp(it) + } + } + } + + @Synchronized + private fun addToWaitUp(books: List) { + books.forEach { book -> + if (!waitUpTocBooks.contains(book.bookUrl) && !onUpTocBooks.contains(book.bookUrl)) { + waitUpTocBooks.add(book.bookUrl) + } + } + if (upTocJob == null) { + startUpTocJob() + } + } + + private fun startUpTocJob() { + upPool() + postUpBooksCount() + upTocJob = viewModelScope.launch(upTocPool) { + flow { + while (true) { + emit(waitUpTocBooks.poll() ?: break) + } + }.onEachParallel(threadCount) { + onUpTocBooks.add(it) + updatingBooksFlow.value = onUpTocBooks.toSet() + postEvent(EventBus.UP_BOOKSHELF, it) + updateToc(it) + }.onEach { + onUpTocBooks.remove(it) + updatingBooksFlow.value = onUpTocBooks.toSet() + postEvent(EventBus.UP_BOOKSHELF, it) + postUpBooksCount() + }.onCompletion { + upTocJob = null + if (waitUpTocBooks.isNotEmpty()) { + startUpTocJob() + } + if (it == null && cacheBookJob == null && !CacheBookService.isRun) { + cacheBook() + } + }.catch { + AppLog.put("更新目录出错\n${it.localizedMessage}", it) + }.collect() + } + } + + private fun upPool() { + threadCount = AppConfig.threadCount + val newPoolSize = min(threadCount, AppConst.MAX_THREAD) + if (poolSize == newPoolSize) return + poolSize = newPoolSize + upTocPool.close() + upTocPool = Executors.newFixedThreadPool(poolSize).asCoroutineDispatcher() + } + + private suspend fun updateToc(bookUrl: String) { + val book = appDb.bookDao.getBook(bookUrl) ?: return + val source = appDb.bookSourceDao.getBookSource(book.origin) + if (source == null) { + if (!book.isUpError) { + book.addType(BookType.updateError) + appDb.bookDao.update(book) + } + return + } + if (source.eventListener) { + if (eventListenerSource.putIfAbsent(source, true) == null) { + SourceCallBack.callBackSource( + viewModelScope, + SourceCallBack.START_SHELF_REFRESH, + source + ) + } + } + kotlin.runCatching { + val oldBook = book.copy() + if (book.tocUrl.isBlank()) { + WebBook.getBookInfoAwait(source, book) + } else { + WebBook.runPreUpdateJs(source, book) + } + val toc = WebBook.getChapterListAwait(source, book).getOrThrow() + book.sync(oldBook) + book.removeType(BookType.updateError) + if (book.bookUrl == bookUrl) { + appDb.bookDao.update(book) + } else { + appDb.bookDao.replace(oldBook, book) + BookHelp.updateCacheFolder(oldBook, book) + } + appDb.bookChapterDao.delByBook(bookUrl) + appDb.bookChapterDao.insert(*toc.toTypedArray()) + ReadBook.onChapterListUpdated(book) + addDownload(source, book) + }.onFailure { + currentCoroutineContext().ensureActive() + AppLog.put("${book.name} 更新目录失败\n${it.localizedMessage}", it) + appDb.bookDao.getBook(book.bookUrl)?.let { book -> + book.addType(BookType.updateError) + appDb.bookDao.update(book) + } + } + } + + private fun postUpBooksCount() { + val count = if (AppConfig.showWaitUpCount) waitUpTocBooks.size + onUpTocBooks.size else 0 + FlowEventBus.post(EventBus.UP_BOOKSHELF_COUNT, count) + } + + private fun addDownload(source: BookSource, book: Book) { + if (AppConfig.preDownloadNum == 0) return + val endIndex = + min(book.totalChapterNum - 1, book.durChapterIndex + AppConfig.preDownloadNum) + val cacheBook = CacheBook.getOrCreate(source, book) + cacheBook.addDownload(book.durChapterIndex, endIndex) + } + + private fun cacheBook() { + eventListenerSource.toList().forEach { + SourceCallBack.callBackSource( + viewModelScope, + SourceCallBack.END_SHELF_REFRESH, + it.first + ) + } + eventListenerSource.clear() + if (AppConfig.preDownloadNum == 0) return + cacheBookJob?.cancel() + cacheBookJob = viewModelScope.launch(upTocPool) { + launch { + while (isActive && CacheBook.isRun) { + CacheBook.setWorkingState(waitUpTocBooks.isEmpty() && onUpTocBooks.isEmpty()) + delay(1000) + } + } + CacheBook.startProcessJob(upTocPool) + } + } + fun addBookByUrl(bookUrls: String) { var successCount = 0 + loadingTextFlow.value = "添加中..." addBookJob = execute { val hasBookUrlPattern: List by lazy { appDb.bookSourceDao.hasBookUrlPattern @@ -83,7 +457,7 @@ class BookshelfViewModel(application: Application) : BaseViewModel(application) it.save() } successCount++ - addBookProgressLiveData.postValue(successCount) + loadingTextFlow.value = "添加中... ($successCount)" } } }.onSuccess { @@ -95,7 +469,7 @@ class BookshelfViewModel(application: Application) : BaseViewModel(application) }.onError { AppLog.put("添加网址出错\n${it.localizedMessage}", it, true) }.onFinally { - addBookProgressLiveData.postValue(-1) + loadingTextFlow.value = null } } @@ -154,6 +528,7 @@ class BookshelfViewModel(application: Application) : BaseViewModel(application) } private fun importBookshelfByJson(json: String, groupId: Long) { + loadingTextFlow.value = "导入中..." execute { val bookSourceParts = appDb.bookSourceDao.allEnabledPart val semaphore = Semaphore(AppConfig.threadCount) @@ -181,6 +556,7 @@ class BookshelfViewModel(application: Application) : BaseViewModel(application) }.onError { it.printOnDebug() }.onFinally { + loadingTextFlow.value = null context.toastOnUi(R.string.success) } } diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/GroupManageSheet.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/GroupManageSheet.kt new file mode 100644 index 000000000..0e1bd01f1 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/GroupManageSheet.kt @@ -0,0 +1,139 @@ +package io.legado.app.ui.main.bookshelf + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +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.Add +import androidx.compose.material.icons.filled.Edit +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +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.res.stringResource +import androidx.compose.ui.unit.dp +import io.legado.app.R +import io.legado.app.data.entities.BookGroup +import io.legado.app.ui.book.group.GroupEditContent +import io.legado.app.ui.book.group.GroupViewModel +import io.legado.app.ui.widget.components.modalBottomSheet.GlassModalBottomSheet +import io.legado.app.ui.widget.components.settingItem.SettingItem +import org.koin.androidx.compose.koinViewModel + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun GroupManageSheet( + onDismissRequest: () -> Unit, + viewModel: GroupViewModel = koinViewModel(), + bookshelfViewModel: BookshelfViewModel = koinViewModel() +) { + val groups by bookshelfViewModel.groupsFlow.collectAsState(initial = emptyList()) + var editingGroup by remember { mutableStateOf(null) } + var isEditing by remember { mutableStateOf(false) } + + GlassModalBottomSheet(onDismissRequest = onDismissRequest) { + AnimatedContent( + targetState = isEditing, + transitionSpec = { + fadeIn().togetherWith(fadeOut()) + }, + label = "GroupManageTransition" + ) { editing -> + if (editing) { + GroupEditContent( + group = editingGroup, + onDismissRequest = { isEditing = false }, + viewModel = viewModel + ) + } else { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .padding(bottom = 32.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = stringResource(R.string.group_manage), + style = MaterialTheme.typography.titleLarge + ) + IconButton(onClick = { + editingGroup = null + isEditing = true + }) { + Icon( + Icons.Default.Add, + contentDescription = stringResource(R.string.add) + ) + } + } + + LazyColumn( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + items(groups, key = { it.groupId }) { group -> + GroupItem( + group = group, + onEdit = { + editingGroup = group + isEditing = true + }, + onToggle = { isChecked -> + viewModel.upGroup(group.copy(show = isChecked)) + } + ) + } + } + } + } + } + } +} + +@Composable +private fun GroupItem( + group: BookGroup, + onEdit: () -> Unit, + onToggle: (Boolean) -> Unit +) { + SettingItem( + title = group.groupName, + onClick = onEdit, + trailingContent = { + Row(verticalAlignment = Alignment.CenterVertically) { + IconButton(onClick = onEdit) { + Icon(Icons.Default.Edit, contentDescription = stringResource(R.string.edit)) + } + Switch( + checked = group.show, + onCheckedChange = onToggle + ) + } + } + ) +} diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/books/BooksFragment.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/books/BooksFragment.kt deleted file mode 100644 index cb838133e..000000000 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/books/BooksFragment.kt +++ /dev/null @@ -1,375 +0,0 @@ -package io.legado.app.ui.main.bookshelf.books - -//import io.legado.app.lib.theme.accentColor -//import io.legado.app.lib.theme.primaryColor -import android.annotation.SuppressLint -import android.content.Intent -import android.os.Bundle -import android.view.View -import android.view.ViewConfiguration -import androidx.core.app.ActivityOptionsCompat -import androidx.core.view.ViewCompat -import androidx.core.view.WindowInsetsCompat -import androidx.core.view.isGone -import androidx.fragment.app.activityViewModels -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle -import androidx.recyclerview.widget.GridLayoutManager -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import androidx.recyclerview.widget.RecyclerView.Adapter.StateRestorationPolicy -import io.legado.app.R -import io.legado.app.base.BaseFragment -import io.legado.app.constant.AppLog -import io.legado.app.constant.EventBus -import io.legado.app.data.AppDatabase -import io.legado.app.data.appDb -import io.legado.app.data.entities.Book -import io.legado.app.data.entities.BookGroup -import io.legado.app.databinding.FragmentBooksBinding -import io.legado.app.help.book.isAudio -import io.legado.app.help.book.isImage -import io.legado.app.help.config.AppConfig -import io.legado.app.ui.book.audio.AudioPlayActivity -import io.legado.app.ui.book.info.BookInfoActivity -import io.legado.app.ui.book.manga.ReadMangaActivity -import io.legado.app.ui.book.read.ReadBookActivity -import io.legado.app.ui.main.MainViewModel -import io.legado.app.ui.main.bookshelf.books.styleDefalut.BaseBooksAdapter -import io.legado.app.ui.main.bookshelf.books.styleDefalut.BooksAdapterGrid -import io.legado.app.ui.main.bookshelf.books.styleDefalut.BooksAdapterGridCompact -import io.legado.app.ui.main.bookshelf.books.styleDefalut.BooksAdapterGridCover -import io.legado.app.ui.main.bookshelf.books.styleDefalut.BooksAdapterList -import io.legado.app.ui.main.bookshelf.books.styleDefalut.BooksAdapterListCompact -import io.legado.app.utils.bookshelfLayoutGrid -import io.legado.app.utils.bookshelfLayoutMode -import io.legado.app.utils.cnCompare -import io.legado.app.utils.flowWithLifecycleAndDatabaseChangeFirst -import io.legado.app.utils.observeEvent -import io.legado.app.utils.startActivity -import io.legado.app.utils.startActivityForBook -import io.legado.app.utils.viewbindingdelegate.viewBinding -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.conflate -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch -import kotlin.math.max - -/** - * 书架界面 - */ -class BooksFragment() : BaseFragment(R.layout.fragment_books), - BaseBooksAdapter.CallBack { - - constructor(position: Int, group: BookGroup) : this() { - val bundle = Bundle() - bundle.putInt("position", position) - bundle.putLong("groupId", group.groupId) - bundle.putInt("bookSort", group.getRealBookSort()) - bundle.putBoolean("enableRefresh", group.enableRefresh) - arguments = bundle - } - - private val binding by viewBinding(FragmentBooksBinding::bind) - private val activityViewModel by activityViewModels() - - private val bookshelfLayoutMode by lazy { requireContext().bookshelfLayoutMode } - - private val bookshelfLayoutGrid by lazy { requireContext().bookshelfLayoutGrid } - - private val booksAdapter: BaseBooksAdapter<*> by lazy { - when (bookshelfLayoutMode) { - 0 -> { - BooksAdapterList(requireContext(), this, this, viewLifecycleOwner.lifecycle) - } - - 1 -> { - BooksAdapterGrid(requireContext(), this) - } - - 2 -> { - BooksAdapterGridCompact(requireContext(), this) - } - - 3 -> { - BooksAdapterGridCover(requireContext(), this) - } - - else -> { - BooksAdapterListCompact(requireContext(), this, this, viewLifecycleOwner.lifecycle) - } - } - } - private var booksFlowJob: Job? = null - var position = 0 - private set - var groupId = -1L - private set - var bookSort = 0 - private set - private var upLastUpdateTimeJob: Job? = null - private var enableRefresh = true - - override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { - arguments?.let { - position = it.getInt("position", 0) - groupId = it.getLong("groupId", -1) - bookSort = it.getInt("bookSort", 0) - enableRefresh = it.getBoolean("enableRefresh", true) - binding.refreshLayout.isEnabled = enableRefresh - } - initRecyclerView() - upRecyclerData() - } - - private fun initRecyclerView() { - //binding.rvBookshelf.setEdgeEffectColor(primaryColor) - upFastScrollerBar() - //binding.refreshLayout.setColorSchemeColors(accentColor) - binding.refreshLayout.setOnRefreshListener { - val books = booksAdapter.getItems() - val refreshList = if (AppConfig.bookshelfRefreshingLimit > 0) { - books.take(AppConfig.bookshelfRefreshingLimit) - } else { - books - } - binding.refreshLayout.isRefreshing = false - activityViewModel.upToc(refreshList) - } - binding.rvBookshelf.layoutManager = GridLayoutManager(context, bookshelfLayoutGrid) - binding.rvBookshelf.setRecycledViewPool(activityViewModel.booksGridRecycledViewPool) - booksAdapter.stateRestorationPolicy = StateRestorationPolicy.PREVENT_WHEN_EMPTY - binding.rvBookshelf.adapter = booksAdapter - booksAdapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { - override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { - val layoutManager = binding.rvBookshelf.layoutManager - if (positionStart == 0 && itemCount == 1 && layoutManager is LinearLayoutManager) { - val scrollTo = layoutManager.findFirstVisibleItemPosition() - itemCount - binding.rvBookshelf.scrollToPosition(max(0, scrollTo)) - } - } - - override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) { - val layoutManager = binding.rvBookshelf.layoutManager - if (toPosition == 0 && itemCount == 1 && layoutManager is LinearLayoutManager) { - val scrollTo = layoutManager.findFirstVisibleItemPosition() - itemCount - binding.rvBookshelf.scrollToPosition(max(0, scrollTo)) - } - } - }) - ViewCompat.setOnApplyWindowInsetsListener(binding.rvBookshelf) { view, insets -> - val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) - view.setPadding( - view.paddingLeft, - view.paddingTop, - view.paddingRight, - systemBars.bottom - ) - WindowInsetsCompat.CONSUMED - } - startLastUpdateTimeJob() - } - - private fun upFastScrollerBar() { - val showBookshelfFastScroller = AppConfig.showBookshelfFastScroller - binding.rvBookshelf.setFastScrollEnabled(showBookshelfFastScroller) - if (showBookshelfFastScroller) { - binding.rvBookshelf.scrollBarSize = 0 - } else { - binding.rvBookshelf.scrollBarSize = - ViewConfiguration.get(requireContext()).scaledScrollBarSize - } - } - - fun upBookSort(sort: Int) { - binding.root.post { - if (!isAdded || view == null) return@post - arguments?.putInt("bookSort", sort) - bookSort = sort - upRecyclerData() - } - } - - fun setEnableRefresh(enable: Boolean) { - enableRefresh = enable - binding.refreshLayout.isEnabled = enable - } - - /** - * 更新书籍列表信息 - */ - private fun upRecyclerData() { - booksFlowJob?.cancel() - booksFlowJob = viewLifecycleOwner.lifecycleScope.launch { - appDb.bookDao.flowByGroup(groupId).map { list -> - //排序 - - val isDescending = AppConfig.bookshelfSortOrder == 1 - - when (bookSort) { - 1 -> if (isDescending) list.sortedByDescending { it.latestChapterTime } - else list.sortedBy { it.latestChapterTime } - - 2 -> if (isDescending) - list.sortedWith { o1, o2 -> o2.name.cnCompare(o1.name) } - else - list.sortedWith { o1, o2 -> o1.name.cnCompare(o2.name) } - - 3 -> if (isDescending) list.sortedByDescending { it.order } - else list.sortedBy { it.order } - - 4 -> if (isDescending) list.sortedByDescending { - max( - it.latestChapterTime, - it.durChapterTime - ) - } - else list.sortedBy { max(it.latestChapterTime, it.durChapterTime) } - - 5 -> if (isDescending) - list.sortedWith { o1, o2 -> o2.author.cnCompare(o1.author) } - else - list.sortedWith { o1, o2 -> o1.author.cnCompare(o2.author) } - - else -> if (isDescending) list.sortedByDescending { it.durChapterTime } - else list.sortedBy { it.durChapterTime } - } - }.flowWithLifecycleAndDatabaseChangeFirst( - viewLifecycleOwner.lifecycle, - Lifecycle.State.RESUMED, - AppDatabase.BOOK_TABLE_NAME - ).catch { - AppLog.put("书架更新出错", it) - }.conflate().flowOn(Dispatchers.Default).collect { list -> - if (view == null) return@collect - binding.emptyView.isGone = list.isNotEmpty() - binding.refreshLayout.isEnabled = enableRefresh && list.isNotEmpty() - booksAdapter.setItems(list) - delay(500) - } - } - } - - private fun startLastUpdateTimeJob() { - upLastUpdateTimeJob?.cancel() - if (!AppConfig.showLastUpdateTime || (bookshelfLayoutMode != 0 && bookshelfLayoutMode != 4)) { - return - } - upLastUpdateTimeJob = viewLifecycleOwner.lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.RESUMED) { - while (isActive) { - booksAdapter.upLastUpdateTime() - delay(30 * 1000) - } - } - } - } - - fun getBooks(): List { - return booksAdapter.getItems() - } - - fun gotoTop() { - if (AppConfig.isEInkMode) { - binding.rvBookshelf.scrollToPosition(0) - } else { - binding.rvBookshelf.smoothScrollToPosition(0) - } - } - - fun getBooksCount(): Int { - return booksAdapter.itemCount - } - - override fun onDestroyView() { - super.onDestroyView() - /** - * 将 RecyclerView 中的视图全部回收到 RecycledViewPool 中 - */ - upLastUpdateTimeJob?.cancel() - booksFlowJob?.cancel() - binding.rvBookshelf.setItemViewCacheSize(0) - binding.rvBookshelf.adapter = null - } - - override fun open(book: Book, sharedView: View) { - if (AppConfig.sharedElementEnterTransitionEnable){ - val transitionName = "book_${book.bookUrl}" - sharedView.transitionName = transitionName - - val cls = when { - book.isAudio -> AudioPlayActivity::class.java - book.isImage && AppConfig.showMangaUi -> ReadMangaActivity::class.java - else -> ReadBookActivity::class.java - } - - val intent = Intent(requireContext(), cls).apply { - putExtra("bookUrl", book.bookUrl) - putExtra("transitionName", transitionName) - } - - val options = ActivityOptionsCompat.makeSceneTransitionAnimation( - requireActivity(), - sharedView, - transitionName - ) - - startActivity(intent, options.toBundle()) - } else { - startActivityForBook(book) - } - } - - override fun openBookInfo(book: Book, sharedView: View) { - if (AppConfig.sharedElementEnterTransitionEnable){ - val intent = Intent(requireContext(), BookInfoActivity::class.java).apply { - putExtra("name", book.name) - putExtra("author", book.author) - putExtra("bookUrl", book.bookUrl) - putExtra("transitionName", "book_${book.bookUrl}") - } - - sharedView.transitionName = "book_${book.bookUrl}" - - val options = ActivityOptionsCompat.makeSceneTransitionAnimation( - requireActivity(), - sharedView, - sharedView.transitionName - ) - - startActivity(intent, options.toBundle()) - } else { - startActivity { - putExtra("name", book.name) - putExtra("author", book.author) - putExtra("bookUrl", book.bookUrl) - } - - } - - } - - - override fun isUpdate(bookUrl: String): Boolean { - return activityViewModel.isUpdate(bookUrl) - } - - @SuppressLint("NotifyDataSetChanged") - override fun observeLiveBus() { - super.observeLiveBus() - observeEvent(EventBus.UP_BOOKSHELF) { - booksAdapter.notification(it) - } - observeEvent(EventBus.BOOKSHELF_REFRESH) { - booksAdapter.notifyDataSetChanged() - startLastUpdateTimeJob() - upFastScrollerBar() - } - } -} diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/books/BookshelfFragment1.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/books/BookshelfFragment1.kt deleted file mode 100644 index cc62ff18a..000000000 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/books/BookshelfFragment1.kt +++ /dev/null @@ -1,229 +0,0 @@ -@file:Suppress("DEPRECATION") - -package io.legado.app.ui.main.bookshelf.books - -import android.annotation.SuppressLint -import android.graphics.Color -import android.graphics.drawable.GradientDrawable -import android.os.Build -import android.os.Bundle -import android.view.View -import androidx.appcompat.widget.PopupMenu -import androidx.appcompat.widget.SearchView -import androidx.core.graphics.ColorUtils -import androidx.fragment.app.Fragment -import androidx.viewpager2.adapter.FragmentStateAdapter -import com.google.android.material.appbar.AppBarLayout -import com.google.android.material.tabs.TabLayout -import com.google.android.material.tabs.TabLayoutMediator -import io.legado.app.R -import io.legado.app.data.appDb -import io.legado.app.data.entities.Book -import io.legado.app.data.entities.BookGroup -import io.legado.app.databinding.FragmentBookshelf1Binding -import io.legado.app.help.config.AppConfig -import io.legado.app.ui.book.group.GroupEditDialog -import io.legado.app.ui.book.search.SearchActivity -import io.legado.app.ui.config.themeConfig.ThemeConfig -import io.legado.app.ui.main.bookshelf.BaseBookshelfFragment -import io.legado.app.utils.showDialogFragment -import io.legado.app.utils.themeColor -import io.legado.app.utils.toastOnUi -import io.legado.app.utils.viewbindingdelegate.viewBinding -import kotlin.collections.set - -/** - * 书架界面 - */ -class BookshelfFragment1() : BaseBookshelfFragment(R.layout.fragment_bookshelf1), - TabLayout.OnTabSelectedListener, - SearchView.OnQueryTextListener { - - constructor(position: Int) : this() { - val bundle = Bundle() - bundle.putInt("position", position) - arguments = bundle - } - - private val binding by viewBinding(FragmentBookshelf1Binding::bind) - - private val bookGroups = mutableListOf() - private val fragmentMap = hashMapOf() - override val groupId: Long get() = selectedGroup?.groupId ?: 0 - - private lateinit var adapter: TabFragmentPageAdapter - private var tabFadeListener: AppBarLayout.OnOffsetChangedListener? = null - - override val books: List - get() { - val fragment = fragmentMap[groupId] - return fragment?.getBooks() ?: emptyList() - } - - override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { - setSupportToolbar(binding.topBar) - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.S) - binding.titleBar.fitsSystemWindows = true - initView() - initBookGroupData() - setupExpandButton() - } - - private val selectedGroup: BookGroup? - get() = bookGroups.getOrNull(binding.viewPagerBookshelf.currentItem) - - private fun initView() { - adapter = TabFragmentPageAdapter(this) - binding.viewPagerBookshelf.adapter = adapter - binding.viewPagerBookshelf.offscreenPageLimit = 3 - - TabLayoutMediator(binding.tabLayout, binding.viewPagerBookshelf) { tab, position -> - tab.text = bookGroups[position].groupName - }.attach() - - updateTabFadeEffect() - } - - private fun setupExpandButton() { - binding.btnExpandTabs.setOnClickListener { - showTabSelectionMenu() - } - } - - private fun updateTabFadeEffect() { - tabFadeListener?.let { binding.titleBar.removeOnOffsetChangedListener(it) } - val listener = AppBarLayout.OnOffsetChangedListener { appBarLayout, verticalOffset -> - val drawable = binding.tabRightFade.background as? GradientDrawable - val opacity = if (ThemeConfig.enableBlur) ThemeConfig.containerOpacity / 100f else 1f - val baseColor = if (-verticalOffset >= appBarLayout.totalScrollRange) { - requireContext().themeColor(com.google.android.material.R.attr.colorSurfaceContainer) - } else { - requireContext().themeColor(com.google.android.material.R.attr.colorSurface) - } - val newAlpha = (Color.alpha(baseColor) * opacity).toInt() - drawable?.colors = intArrayOf(Color.TRANSPARENT, ColorUtils.setAlphaComponent(baseColor, newAlpha)) - } - tabFadeListener = listener - binding.titleBar.addOnOffsetChangedListener(listener) - } - - private fun showTabSelectionMenu() { - val popup = PopupMenu(requireContext(), binding.btnExpandTabs) - - bookGroups.forEachIndexed { index, bookGroup -> - popup.menu.add(0, index, index, bookGroup.groupName) - } - - popup.setOnMenuItemClickListener { item -> - val position = item.itemId - if (position in bookGroups.indices) { - binding.viewPagerBookshelf.setCurrentItem(position, true) - } - true - } - - popup.setOnDismissListener { - binding.btnExpandTabs.isChecked = false - } - - popup.show() - } - - private fun updateExpandButtonVisibility() { - binding.btnExpandTabs.visibility = if (AppConfig.shouldShowExpandButton) { - View.VISIBLE - } else { - View.GONE - } - } - - override fun onQueryTextSubmit(query: String?): Boolean { - SearchActivity.start(requireContext(), query) - return false - } - - override fun onQueryTextChange(newText: String?): Boolean { - return false - } - - @SuppressLint("NotifyDataSetChanged") - @Synchronized - override fun upGroup(data: List) { - if (data.isEmpty()) { - appDb.bookGroupDao.enableGroup(BookGroup.IdAll) - } else { - if (data != bookGroups) { - bookGroups.clear() - bookGroups.addAll(data) - adapter.notifyDataSetChanged() - adapter = TabFragmentPageAdapter(this) - binding.viewPagerBookshelf.adapter = adapter - - TabLayoutMediator(binding.tabLayout, binding.viewPagerBookshelf) { tab, position -> - tab.text = bookGroups[position].groupName - }.attach() - - updateExpandButtonVisibility() - updateTabFadeEffect() - selectLastTab() - - binding.tabLayout.post { - for (i in 0 until bookGroups.size) { - binding.tabLayout.getTabAt(i)?.view?.setOnLongClickListener { - showDialogFragment(GroupEditDialog(bookGroups[i])) - true - } - } - } - } - } - } - - override fun upSort() { - childFragmentManager.fragments.forEach { - if (it is BooksFragment) { - val position = it.position - val group = bookGroups.getOrNull(position) ?: return@forEach - it.setEnableRefresh(group.enableRefresh) - it.upBookSort(group.getRealBookSort()) - } - } - } - - private fun selectLastTab() { - binding.tabLayout.post { - binding.tabLayout.removeOnTabSelectedListener(this) - binding.tabLayout.getTabAt(AppConfig.saveTabPosition)?.select() - binding.tabLayout.addOnTabSelectedListener(this) - } - } - - override fun onTabReselected(tab: TabLayout.Tab) { - selectedGroup?.let { group -> - fragmentMap[group.groupId]?.let { - toastOnUi("${group.groupName}(${it.getBooksCount()})") - } - } - } - - override fun onTabUnselected(tab: TabLayout.Tab) = Unit - - override fun onTabSelected(tab: TabLayout.Tab) { - AppConfig.saveTabPosition = tab.position - } - - override fun gotoTop() { - fragmentMap[groupId]?.gotoTop() - } - - private inner class TabFragmentPageAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) { - override fun getItemCount(): Int = bookGroups.size - - override fun createFragment(position: Int): Fragment { - val group = bookGroups[position] - val fragment = BooksFragment(position, group) - fragmentMap[group.groupId] = fragment - return fragment - } - } -} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/books/BookshelfFragment2.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/books/BookshelfFragment2.kt deleted file mode 100644 index c02a9edf8..000000000 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/books/BookshelfFragment2.kt +++ /dev/null @@ -1,467 +0,0 @@ -package io.legado.app.ui.main.bookshelf.books - -import android.annotation.SuppressLint -import android.content.Intent -import android.os.Build -import android.os.Bundle -import android.view.View -import android.view.ViewConfiguration -import androidx.activity.OnBackPressedCallback -import androidx.appcompat.widget.SearchView -import androidx.core.app.ActivityOptionsCompat -import androidx.core.view.ViewCompat -import androidx.core.view.WindowInsetsCompat -import androidx.core.view.isGone -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle -import androidx.recyclerview.widget.DefaultItemAnimator -import androidx.recyclerview.widget.GridLayoutManager -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import io.legado.app.R -import io.legado.app.constant.AppLog -import io.legado.app.constant.EventBus -import io.legado.app.data.AppDatabase -import io.legado.app.data.appDb -import io.legado.app.data.entities.Book -import io.legado.app.data.entities.BookGroup -import io.legado.app.databinding.FragmentBookshelf2Binding -import io.legado.app.help.book.isAudio -import io.legado.app.help.book.isImage -import io.legado.app.help.config.AppConfig -import io.legado.app.ui.book.audio.AudioPlayActivity -import io.legado.app.ui.book.group.GroupEditDialog -import io.legado.app.ui.book.info.BookInfoActivity -import io.legado.app.ui.book.manga.ReadMangaActivity -import io.legado.app.ui.book.read.ReadBookActivity -import io.legado.app.ui.book.search.SearchActivity -import io.legado.app.ui.main.bookshelf.BaseBookshelfFragment -import io.legado.app.ui.main.bookshelf.books.styleFold.BaseBooksAdapter -import io.legado.app.ui.main.bookshelf.books.styleFold.BooksAdapterGrid -import io.legado.app.ui.main.bookshelf.books.styleFold.BooksAdapterGridCompact -import io.legado.app.ui.main.bookshelf.books.styleFold.BooksAdapterGridCover -import io.legado.app.ui.main.bookshelf.books.styleFold.BooksAdapterList -import io.legado.app.ui.main.bookshelf.books.styleFold.BooksAdapterListCompact -import io.legado.app.utils.bookshelfLayoutGrid -import io.legado.app.utils.bookshelfLayoutMode -import io.legado.app.utils.cnCompare -import io.legado.app.utils.flowWithLifecycleAndDatabaseChangeFirst -import io.legado.app.utils.observeEvent -import io.legado.app.utils.showDialogFragment -import io.legado.app.utils.startActivity -import io.legado.app.utils.startActivityForBook -import io.legado.app.utils.viewbindingdelegate.viewBinding -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.conflate -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.launch -import kotlin.math.max - -/** - * 书架界面 - */ -class BookshelfFragment2() : BaseBookshelfFragment(R.layout.fragment_bookshelf2), - SearchView.OnQueryTextListener, - BaseBooksAdapter.CallBack { - - fun interface OnGroupIdChangeListener { - fun onGroupIdChanged() - } - - constructor(position: Int) : this() { - val bundle = Bundle() - bundle.putInt("position", position) - arguments = bundle - } - - private val binding by viewBinding(FragmentBookshelf2Binding::bind) - - private val bookshelfLayoutMode by lazy { requireContext().bookshelfLayoutMode } - - private val bookshelfLayoutGrid by lazy { requireContext().bookshelfLayoutGrid } - - private var groupIdChangeListener: OnGroupIdChangeListener? = null - - private val booksAdapter: BaseBooksAdapter<*> by lazy { - when (bookshelfLayoutMode) { - 0 -> { - BooksAdapterList(requireContext(), this) - } - - 1 -> { - BooksAdapterGrid(requireContext(), this) - } - - 2 -> { - BooksAdapterGridCompact(requireContext(), this) - } - - 3 -> { - BooksAdapterGridCover(requireContext(), this) - } - - else -> { - BooksAdapterListCompact(requireContext(), this) - } - } - } - - var onGroupIdChangedListener: ((isRoot: Boolean) -> Unit)? = null - - private var bookGroups: List = emptyList() - private var booksFlowJob: Job? = null - override var groupId = BookGroup.Companion.IdRoot - private var allBooks: List = emptyList() - override var books: List = emptyList() - private var enableRefresh = true - - private lateinit var backCallback: OnBackPressedCallback - - override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { - setSupportToolbar(binding.topBar) - initRecyclerView() - initBookGroupData() - initAllBooksData() - initBooksData() - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.S) - binding.titleBar.fitsSystemWindows = true - backCallback = object : OnBackPressedCallback(false) { // 初始禁用 - override fun handleOnBackPressed() { - if (back()) { - updateBackCallbackState() - } - } - } - requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, backCallback) - } - - override fun onDestroyView() { - onGroupIdChangedListener?.invoke(true) - super.onDestroyView() - } - - private fun updateBackCallbackState() { - val isRoot = groupId == BookGroup.IdRoot - backCallback.isEnabled = !isRoot - onGroupIdChangedListener?.invoke(isRoot) - } - - @Suppress("UNCHECKED_CAST") - fun BaseBooksAdapter<*>.getBookItems(): List { - return getItems() as List - } - - private fun initRecyclerView() { - upFastScrollerBar() - binding.refreshLayout.setOnRefreshListener { - val books = booksAdapter.getBookItems() - val refreshList = if (AppConfig.bookshelfRefreshingLimit > 0) { - books.take(AppConfig.bookshelfRefreshingLimit) - } else { - books - } - binding.refreshLayout.isRefreshing = false - activityViewModel.upToc(refreshList) - } - - binding.rvBookshelf.layoutManager = GridLayoutManager(context, bookshelfLayoutGrid) - binding.rvBookshelf.itemAnimator = null - binding.rvBookshelf.adapter = booksAdapter - booksAdapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { - override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { - val layoutManager = binding.rvBookshelf.layoutManager - if (positionStart == 0 && layoutManager is LinearLayoutManager) { - val scrollTo = layoutManager.findFirstVisibleItemPosition() - itemCount - binding.rvBookshelf.scrollToPosition(max(0, scrollTo)) - } - } - - override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) { - val layoutManager = binding.rvBookshelf.layoutManager - if (toPosition == 0 && layoutManager is LinearLayoutManager) { - val scrollTo = layoutManager.findFirstVisibleItemPosition() - itemCount - binding.rvBookshelf.scrollToPosition(max(0, scrollTo)) - } - } - }) - binding.rvBookshelf.itemAnimator = DefaultItemAnimator() - ViewCompat.setOnApplyWindowInsetsListener(binding.rvBookshelf) { view, insets -> - val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) - view.setPadding( - view.paddingLeft, - view.paddingTop, - view.paddingRight, - systemBars.bottom - ) - WindowInsetsCompat.CONSUMED - } - } - - private fun upFastScrollerBar() { - val showBookshelfFastScroller = AppConfig.showBookshelfFastScroller - binding.rvBookshelf.setFastScrollEnabled(showBookshelfFastScroller) - if (showBookshelfFastScroller) { - binding.rvBookshelf.scrollBarSize = 0 - } else { - binding.rvBookshelf.scrollBarSize = - ViewConfiguration.get(requireContext()).scaledScrollBarSize - } - } - - override fun upGroup(data: List) { - if (data != bookGroups) { - bookGroups = data - booksAdapter.updateItems() - binding.emptyView.isGone = getItemCount() > 0 - binding.refreshLayout.isEnabled = enableRefresh && getItemCount() > 0 - } - } - - override fun upSort() { - initAllBooksData() - initBooksData() - } - - private var allBooksFlowJob: Job? = null - - private fun initAllBooksData() { - allBooksFlowJob?.cancel() - allBooksFlowJob = viewLifecycleOwner.lifecycleScope.launch { - appDb.bookDao.flowAll().map { list -> - val sortType = BookGroup(BookGroup.Companion.IdRoot, "").getRealBookSort() - when (sortType) { - 1 -> list.sortedByDescending { it.latestChapterTime } - 2 -> list.sortedWith { o1, o2 -> o1.name.cnCompare(o2.name) } - 3 -> list.sortedBy { it.order } - 4 -> list.sortedByDescending { max(it.latestChapterTime, it.durChapterTime) } - 5 -> list.sortedWith { o1, o2 -> o1.author.cnCompare(o2.author) } - else -> list.sortedByDescending { it.durChapterTime } - } - }.flowWithLifecycleAndDatabaseChangeFirst( - viewLifecycleOwner.lifecycle, - Lifecycle.State.RESUMED, - AppDatabase.Companion.BOOK_TABLE_NAME - ).catch { - AppLog.put("所有书籍更新出错", it) - }.conflate().flowOn(Dispatchers.Default).collect { list -> - allBooks = list - booksAdapter.setAllBooks(allBooks) - booksAdapter.updateItems() - } - } - } - - private fun initBooksData() { - if (groupId == BookGroup.IdRoot) { - if (isAdded) { - binding.collTopBar.title = getString(R.string.bookshelf) - binding.refreshLayout.isEnabled = true - enableRefresh = true - } - } else { - bookGroups.firstOrNull { groupId == it.groupId }?.let { - binding.collTopBar.title = it.groupName - binding.refreshLayout.isEnabled = it.enableRefresh - enableRefresh = it.enableRefresh - } - } - booksFlowJob?.cancel() - booksFlowJob = viewLifecycleOwner.lifecycleScope.launch { - viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) { - appDb.bookDao.flowByGroup(groupId) - .map { list -> - val isDescending = AppConfig.bookshelfSortOrder == 1 - val sortType = AppConfig.getBookSortByGroupId(groupId) - - when (sortType) { - 1 -> if (isDescending) list.sortedByDescending { it.latestChapterTime } - else list.sortedBy { it.latestChapterTime } - - 2 -> if (isDescending) - list.sortedWith { o1, o2 -> o2.name.cnCompare(o1.name) } - else - list.sortedWith { o1, o2 -> o1.name.cnCompare(o2.name) } - - 3 -> if (isDescending) list.sortedByDescending { it.order } - else list.sortedBy { it.order } - - 4 -> if (isDescending) - list.sortedByDescending { max(it.latestChapterTime, it.durChapterTime) } - else - list.sortedBy { max(it.latestChapterTime, it.durChapterTime) } - - 5 -> if (isDescending) - list.sortedWith { o1, o2 -> o2.author.cnCompare(o1.author) } - else - list.sortedWith { o1, o2 -> o1.author.cnCompare(o2.author) } - - else -> - if (isDescending) list.sortedByDescending { it.durChapterTime } - else list.sortedBy { it.durChapterTime } - } - } - .flowOn(Dispatchers.Default) - .catch { AppLog.put("书架更新出错", it) } - .collect { list -> - - books = list - booksAdapter.updateItems() - val empty = getItemCount() > 0 - binding.emptyView.isGone = empty - binding.refreshLayout.isEnabled = enableRefresh && empty - } - } - } - } - - fun back(): Boolean { - if (groupId != BookGroup.Companion.IdRoot) { - groupId = BookGroup.Companion.IdRoot - initBooksData() - groupIdChangeListener?.onGroupIdChanged() - updateBackCallbackState() - return false - } - return true - //需要提前设置,因为在返回时触发 - } - - override fun onQueryTextSubmit(query: String?): Boolean { - SearchActivity.Companion.start(requireContext(), query) - return false - } - - override fun onQueryTextChange(newText: String?): Boolean { - return false - } - - override fun gotoTop() { - if (AppConfig.isEInkMode) { - binding.rvBookshelf.scrollToPosition(0) - } else { - binding.rvBookshelf.smoothScrollToPosition(0) - } - } - - override fun onItemClick(item: Any, sharedView: View) { - if (AppConfig.sharedElementEnterTransitionEnable){ - when (item) { - is Book -> { - val transitionName = "book_${item.bookUrl}" - sharedView.transitionName = transitionName - - val cls = when { - item.isAudio -> AudioPlayActivity::class.java - item.isImage && AppConfig.showMangaUi -> ReadMangaActivity::class.java - else -> ReadBookActivity::class.java - } - - val intent = Intent(requireContext(), cls).apply { - putExtra("bookUrl", item.bookUrl) - putExtra("transitionName", transitionName) - } - - val options = ActivityOptionsCompat.makeSceneTransitionAnimation( - requireActivity(), - sharedView, - transitionName - ) - - startActivity(intent, options.toBundle()) - } - - is BookGroup -> { - groupId = item.groupId - initBooksData() - groupIdChangeListener?.onGroupIdChanged() - updateBackCallbackState() - } - } - } else { - when (item) { - is Book -> startActivityForBook(item) - - is BookGroup -> { - groupId = item.groupId - initBooksData() - groupIdChangeListener?.onGroupIdChanged() - updateBackCallbackState() - } - } - } - - } - - override fun onItemLongClick(item: Any, sharedView: View) { - if (AppConfig.sharedElementEnterTransitionEnable){ - when (item) { - is Book -> { - val intent = Intent(requireContext(), BookInfoActivity::class.java).apply { - putExtra("name", item.name) - putExtra("author", item.author) - putExtra("bookUrl", item.bookUrl) - putExtra("transitionName", "book_${item.bookUrl}") // 给共享元素唯一标识 - } - - sharedView.transitionName = "book_${item.bookUrl}" - - val options = ActivityOptionsCompat.makeSceneTransitionAnimation( - requireActivity(), - sharedView, - sharedView.transitionName - ) - - startActivity(intent, options.toBundle()) - } - - is BookGroup -> showDialogFragment(GroupEditDialog(item)) - } - } else { - when (item) { - is Book -> startActivity { - putExtra("name", item.name) - putExtra("author", item.author) - putExtra("bookUrl", item.bookUrl) - } - - is BookGroup -> showDialogFragment(GroupEditDialog(item)) - } - } - } - - override fun isUpdate(bookUrl: String): Boolean { - return activityViewModel.isUpdate(bookUrl) - } - - fun getItemCount(): Int { - return if (groupId == BookGroup.Companion.IdRoot) { - bookGroups.size + books.size - } else { - books.size - } - } - - override fun getItems(): List { - return if (groupId == BookGroup.Companion.IdRoot) { - bookGroups + books - } else { - books - } - } - - - @SuppressLint("NotifyDataSetChanged") - override fun observeLiveBus() { - super.observeLiveBus() - observeEvent(EventBus.UP_BOOKSHELF) { - booksAdapter.notification(it) - } - observeEvent(EventBus.BOOKSHELF_REFRESH) { - booksAdapter.notifyDataSetChanged() - } - } -} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/books/BookshelfFragment3.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/books/BookshelfFragment3.kt deleted file mode 100644 index 12b5002bc..000000000 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/books/BookshelfFragment3.kt +++ /dev/null @@ -1,147 +0,0 @@ -@file:Suppress("DEPRECATION") - -package io.legado.app.ui.main.bookshelf.books - -import android.annotation.SuppressLint -import android.os.Build -import android.os.Bundle -import android.view.View -import androidx.appcompat.widget.SearchView -import androidx.fragment.app.Fragment -import androidx.viewpager2.adapter.FragmentStateAdapter -import androidx.viewpager2.widget.ViewPager2 -import com.google.android.material.tabs.TabLayout -import io.legado.app.R -import io.legado.app.data.appDb -import io.legado.app.data.entities.Book -import io.legado.app.data.entities.BookGroup -import io.legado.app.databinding.FragmentBookshelf3Binding -import io.legado.app.help.config.AppConfig -import io.legado.app.ui.book.search.SearchActivity -import io.legado.app.ui.main.bookshelf.BaseBookshelfFragment -import io.legado.app.utils.viewbindingdelegate.viewBinding -import kotlin.collections.set - -/** - * 书架界面 - */ -class BookshelfFragment3() : BaseBookshelfFragment(R.layout.fragment_bookshelf3), - SearchView.OnQueryTextListener { - - constructor(position: Int) : this() { - val bundle = Bundle() - bundle.putInt("position", position) - arguments = bundle - } - - private val binding by viewBinding(FragmentBookshelf3Binding::bind) - - private val bookGroups = mutableListOf() - private val fragmentMap = hashMapOf() - override val groupId: Long get() = selectedGroup?.groupId ?: 0 - - private lateinit var adapter: TabFragmentPageAdapter - - override val books: List - get() { - val fragment = fragmentMap[groupId] - return fragment?.getBooks() ?: emptyList() - } - - override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { - setSupportToolbar(binding.topBar) - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.S) - binding.titleBar.fitsSystemWindows = true - initView() - initBookGroupData() - } - - private val selectedGroup: BookGroup? - get() = bookGroups.getOrNull(binding.viewPagerBookshelf.currentItem) - - private fun initView() { - adapter = TabFragmentPageAdapter(this) - binding.viewPagerBookshelf.adapter = adapter - binding.viewPagerBookshelf.offscreenPageLimit = 3 - - binding.viewPagerBookshelf.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { - override fun onPageSelected(position: Int) { - super.onPageSelected(position) - updateToolbarTitle(position) - AppConfig.saveTabPosition = position - } - }) - } - - private fun updateToolbarTitle(position: Int) { - if (bookGroups.isNotEmpty() && position in 0 until bookGroups.size) { - binding.collTopBar.title = bookGroups[position].groupName - } - } - - override fun onQueryTextSubmit(query: String?): Boolean { - SearchActivity.start(requireContext(), query) - return false - } - - override fun onQueryTextChange(newText: String?): Boolean { - return false - } - - @SuppressLint("NotifyDataSetChanged") - @Synchronized - override fun upGroup(data: List) { - if (data.isEmpty()) { - appDb.bookGroupDao.enableGroup(BookGroup.IdAll) - } else { - if (data != bookGroups) { - bookGroups.clear() - bookGroups.addAll(data) - adapter.notifyDataSetChanged() - adapter = TabFragmentPageAdapter(this) - binding.viewPagerBookshelf.adapter = adapter - updateToolbarTitle(binding.viewPagerBookshelf.currentItem) - selectLastTab() - } - } - } - - override fun upSort() { - childFragmentManager.fragments.forEach { - if (it is BooksFragment) { - val position = it.position - val group = bookGroups.getOrNull(position) ?: return@forEach - it.setEnableRefresh(group.enableRefresh) - it.upBookSort(group.getRealBookSort()) - } - } - } - - override fun gotoTop() { - fragmentMap[groupId]?.gotoTop() - } - - private fun selectLastTab() { - val lastPosition = AppConfig.saveTabPosition - if (lastPosition in 0 until bookGroups.size) { - binding.viewPagerBookshelf.post { - binding.viewPagerBookshelf.setCurrentItem(lastPosition, false) - updateToolbarTitle(lastPosition) - } - } else { - AppConfig.saveTabPosition = 0 - updateToolbarTitle(0) - } - } - - private inner class TabFragmentPageAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) { - override fun getItemCount(): Int = bookGroups.size - - override fun createFragment(position: Int): Fragment { - val group = bookGroups[position] - val fragment = BooksFragment(position, group) - fragmentMap[group.groupId] = fragment - return fragment - } - } -} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/books/BookshelfFragment4.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/books/BookshelfFragment4.kt deleted file mode 100644 index 1f5624507..000000000 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/books/BookshelfFragment4.kt +++ /dev/null @@ -1,164 +0,0 @@ -@file:Suppress("DEPRECATION") - -package io.legado.app.ui.main.bookshelf.books - -import android.annotation.SuppressLint -import android.os.Build -import android.os.Bundle -import android.view.View -import androidx.appcompat.widget.SearchView -import androidx.fragment.app.Fragment -import androidx.viewpager2.adapter.FragmentStateAdapter -import com.google.android.material.tabs.TabLayout -import com.google.android.material.tabs.TabLayoutMediator -import io.legado.app.R -import io.legado.app.data.appDb -import io.legado.app.data.entities.Book -import io.legado.app.data.entities.BookGroup -import io.legado.app.databinding.FragmentBookshelf1Binding -import io.legado.app.databinding.FragmentBookshelf4Binding -import io.legado.app.help.config.AppConfig -import io.legado.app.ui.book.group.GroupEditDialog -import io.legado.app.ui.book.search.SearchActivity -import io.legado.app.ui.main.bookshelf.BaseBookshelfFragment -import io.legado.app.utils.showDialogFragment -import io.legado.app.utils.toastOnUi -import io.legado.app.utils.viewbindingdelegate.viewBinding -import kotlin.collections.set - -/** - * 书架界面 - */ -class BookshelfFragment4() : BaseBookshelfFragment(R.layout.fragment_bookshelf4), - TabLayout.OnTabSelectedListener, - SearchView.OnQueryTextListener { - - constructor(position: Int) : this() { - val bundle = Bundle() - bundle.putInt("position", position) - arguments = bundle - } - - private val binding by viewBinding(FragmentBookshelf4Binding::bind) - - private val bookGroups = mutableListOf() - private val fragmentMap = hashMapOf() - override val groupId: Long get() = selectedGroup?.groupId ?: 0 - - private lateinit var adapter: TabFragmentPageAdapter - - override val books: List - get() { - val fragment = fragmentMap[groupId] - return fragment?.getBooks() ?: emptyList() - } - - override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { - setSupportToolbar(binding.topBar) - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.S) - binding.titleBar.fitsSystemWindows = true - initView() - initBookGroupData() - } - - private val selectedGroup: BookGroup? - get() = bookGroups.getOrNull(binding.viewPagerBookshelf.currentItem) - - private fun initView() { - adapter = TabFragmentPageAdapter(this) - binding.viewPagerBookshelf.adapter = adapter - binding.viewPagerBookshelf.offscreenPageLimit = 3 - - TabLayoutMediator(binding.tabLayout, binding.viewPagerBookshelf) { tab, position -> - tab.text = bookGroups[position].groupName - }.attach() - } - - override fun onQueryTextSubmit(query: String?): Boolean { - SearchActivity.start(requireContext(), query) - return false - } - - override fun onQueryTextChange(newText: String?): Boolean { - return false - } - - @SuppressLint("NotifyDataSetChanged") - @Synchronized - override fun upGroup(data: List) { - if (data.isEmpty()) { - appDb.bookGroupDao.enableGroup(BookGroup.IdAll) - } else { - if (data != bookGroups) { - bookGroups.clear() - bookGroups.addAll(data) - adapter.notifyDataSetChanged() - adapter = TabFragmentPageAdapter(this) - binding.viewPagerBookshelf.adapter = adapter - - TabLayoutMediator(binding.tabLayout, binding.viewPagerBookshelf) { tab, position -> - tab.text = bookGroups[position].groupName - }.attach() - - selectLastTab() - - binding.tabLayout.post { - for (i in 0 until bookGroups.size) { - binding.tabLayout.getTabAt(i)?.view?.setOnLongClickListener { - showDialogFragment(GroupEditDialog(bookGroups[i])) - true - } - } - } - } - } - } - - override fun upSort() { - childFragmentManager.fragments.forEach { - if (it is BooksFragment) { - val position = it.position - val group = bookGroups.getOrNull(position) ?: return@forEach - it.setEnableRefresh(group.enableRefresh) - it.upBookSort(group.getRealBookSort()) - } - } - } - - private fun selectLastTab() { - binding.tabLayout.post { - binding.tabLayout.removeOnTabSelectedListener(this) - binding.tabLayout.getTabAt(AppConfig.saveTabPosition)?.select() - binding.tabLayout.addOnTabSelectedListener(this) - } - } - - override fun onTabReselected(tab: TabLayout.Tab) { - selectedGroup?.let { group -> - fragmentMap[group.groupId]?.let { - toastOnUi("${group.groupName}(${it.getBooksCount()})") - } - } - } - - override fun onTabUnselected(tab: TabLayout.Tab) = Unit - - override fun onTabSelected(tab: TabLayout.Tab) { - AppConfig.saveTabPosition = tab.position - } - - override fun gotoTop() { - fragmentMap[groupId]?.gotoTop() - } - - private inner class TabFragmentPageAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) { - override fun getItemCount(): Int = bookGroups.size - - override fun createFragment(position: Int): Fragment { - val group = bookGroups[position] - val fragment = BooksFragment(position, group) - fragmentMap[group.groupId] = fragment - return fragment - } - } -} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/books/styleDefalut/BaseBooksAdapter.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/books/styleDefalut/BaseBooksAdapter.kt deleted file mode 100644 index f24141b75..000000000 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/books/styleDefalut/BaseBooksAdapter.kt +++ /dev/null @@ -1,102 +0,0 @@ -package io.legado.app.ui.main.bookshelf.books.styleDefalut - -import android.content.Context -import android.view.View -import androidx.core.os.bundleOf -import androidx.recyclerview.widget.DiffUtil -import androidx.viewbinding.ViewBinding -import io.legado.app.base.adapter.DiffRecyclerAdapter -import io.legado.app.base.adapter.ItemViewHolder -import io.legado.app.data.entities.Book - -abstract class BaseBooksAdapter(context: Context) : - DiffRecyclerAdapter(context) { - - init { - setHasStableIds(true) - } - override fun getItemId(position: Int): Long { - return getItems()[position].bookUrl.hashCode().toLong() - } - - override val keepScrollPosition = true - - override val diffItemCallback: DiffUtil.ItemCallback = - object : DiffUtil.ItemCallback() { - - override fun areItemsTheSame(oldItem: Book, newItem: Book): Boolean { - return oldItem.bookUrl == newItem.bookUrl - } - - override fun areContentsTheSame(oldItem: Book, newItem: Book): Boolean { - return when { - oldItem.durChapterTime != newItem.durChapterTime -> false - oldItem.name != newItem.name -> false - oldItem.author != newItem.author -> false - oldItem.durChapterTitle != newItem.durChapterTitle -> false - oldItem.latestChapterTitle != newItem.latestChapterTitle -> false - oldItem.lastCheckCount != newItem.lastCheckCount -> false - oldItem.getDisplayCover() != newItem.getDisplayCover() -> false - oldItem.getUnreadChapterNum() != newItem.getUnreadChapterNum() -> false - else -> true - } - } - - override fun getChangePayload(oldItem: Book, newItem: Book): Any? { - val bundle = bundleOf() - if (oldItem.name != newItem.name) { - bundle.putString("name", newItem.name) - } - if (oldItem.author != newItem.author) { - bundle.putString("author", newItem.author) - } - if (oldItem.durChapterTitle != newItem.durChapterTitle) { - bundle.putString("dur", newItem.durChapterTitle) - } - if (oldItem.latestChapterTitle != newItem.latestChapterTitle) { - bundle.putString("last", newItem.latestChapterTitle) - } - if (oldItem.getDisplayCover() != newItem.getDisplayCover()) { - bundle.putString("cover", newItem.getDisplayCover()) - } - if (oldItem.lastCheckCount != newItem.lastCheckCount - || oldItem.durChapterTime != newItem.durChapterTime - || oldItem.getUnreadChapterNum() != newItem.getUnreadChapterNum() - || oldItem.lastCheckCount != newItem.lastCheckCount - ) { - bundle.putBoolean("refresh", true) - } - if (oldItem.latestChapterTime != newItem.latestChapterTime) { - bundle.putBoolean("lastUpdateTime", true) - } - if (bundle.isEmpty) return null - return bundle - } - - } - - override fun onViewRecycled(holder: ItemViewHolder) { - super.onViewRecycled(holder) - holder.itemView.setOnClickListener(null) - holder.itemView.setOnLongClickListener(null) - } - - fun notification(bookUrl: String) { - getItems().forEachIndexed { i, it -> - if (it.bookUrl == bookUrl) { - notifyItemChanged(i, bundleOf("refresh" to true, "lastUpdateTime" to true)) - return - } - } - } - - fun upLastUpdateTime() { - notifyItemRangeChanged(0, itemCount, bundleOf(Pair("lastUpdateTime", null))) - } - - interface CallBack { - fun open(book: Book, sharedView: View) - fun openBookInfo(book: Book, sharedView: View) - fun isUpdate(bookUrl: String): Boolean - } -} diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/books/styleDefalut/BooksAdapterGrid.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/books/styleDefalut/BooksAdapterGrid.kt deleted file mode 100644 index fa27e0a67..000000000 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/books/styleDefalut/BooksAdapterGrid.kt +++ /dev/null @@ -1,93 +0,0 @@ -package io.legado.app.ui.main.bookshelf.books.styleDefalut - -import android.content.Context -import android.os.Bundle -import android.view.ViewGroup -import androidx.core.view.isVisible -import io.legado.app.base.adapter.ItemViewHolder -import io.legado.app.data.entities.Book -import io.legado.app.databinding.ItemBookshelfGridBinding -import io.legado.app.help.book.getBookTypeName -import io.legado.app.help.book.isLocal -import io.legado.app.help.config.AppConfig -import io.legado.app.utils.gone -import io.legado.app.utils.visible - -class BooksAdapterGrid(context: Context, private val callBack: CallBack) : - BaseBooksAdapter(context) { - - override fun getViewBinding(parent: ViewGroup): ItemBookshelfGridBinding { - return ItemBookshelfGridBinding.inflate(inflater, parent, false) - } - - override fun convert( - holder: ItemViewHolder, - binding: ItemBookshelfGridBinding, - item: Book, - payloads: MutableList - ) = binding.run { - if (payloads.isEmpty()) { - cdCover.transitionName = "book_${item.bookUrl}" - tvName.text = item.name - ivCover.load(item.getDisplayCover(), item.name, item.author, false, item.origin) - upRefresh(binding, item) - } else { - for (i in payloads.indices) { - val bundle = payloads[i] as Bundle - bundle.keySet().forEach { - when (it) { - "name" -> tvName.text = item.name - "cover" -> ivCover.load(item.getDisplayCover(), item.name, item.author, false, item.origin) - "refresh" -> upRefresh(binding, item) - } - } - } - } - } - - private fun upRefresh(binding: ItemBookshelfGridBinding, item: Book) { - if (!item.isLocal && callBack.isUpdate(item.bookUrl)) { - binding.cdUnread.gone() - binding.cdTip.visible() - binding.rlLoading.visible() - } else { - binding.rlLoading.gone() - if (AppConfig.showUnread) { - val unreadCount = item.getUnreadChapterNum() - if (unreadCount > 0) { - binding.cdUnread.visible() - binding.tvUnread.text = unreadCount.toString() - if (AppConfig.showUnreadNew) - binding.newChapter.isVisible = item.lastCheckCount > 0 - } else { - binding.cdUnread.gone() - binding.newChapter.gone() - } - } else { - binding.cdUnread.gone() - } - if (AppConfig.showTip){ - binding.cdTip.visible() - binding.tvTip.text = item.getBookTypeName() - } else { - binding.cdTip.gone() - } - } - } - - override fun registerListener(holder: ItemViewHolder, binding: ItemBookshelfGridBinding) { - - binding.cvContent.setOnClickListener { - getItem(holder.layoutPosition)?.let { - callBack.open(it, binding.cdCover) - } - } - - binding.cvContent.setOnLongClickListener { - getItem(holder.layoutPosition)?.let { - callBack.openBookInfo(it, binding.cdCover) - } - true - } - } -} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/books/styleDefalut/BooksAdapterGridCompact.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/books/styleDefalut/BooksAdapterGridCompact.kt deleted file mode 100644 index 9d42e9433..000000000 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/books/styleDefalut/BooksAdapterGridCompact.kt +++ /dev/null @@ -1,103 +0,0 @@ -package io.legado.app.ui.main.bookshelf.books.styleDefalut - -import android.content.Context -import android.os.Bundle -import android.view.View -import android.view.ViewGroup -import androidx.core.view.isVisible -import io.legado.app.base.adapter.ItemViewHolder -import io.legado.app.data.entities.Book -import io.legado.app.databinding.ItemBookshelfGridCompactBinding -import io.legado.app.help.book.getBookTypeName -import io.legado.app.help.book.isLocal -import io.legado.app.help.config.AppConfig -import io.legado.app.utils.gone -import io.legado.app.utils.visible - -class BooksAdapterGridCompact(context: Context, private val callBack: CallBack) : - BaseBooksAdapter(context) { - - override fun getViewBinding(parent: ViewGroup): ItemBookshelfGridCompactBinding { - return ItemBookshelfGridCompactBinding.inflate(inflater, parent, false) - } - - override fun convert( - holder: ItemViewHolder, - binding: ItemBookshelfGridCompactBinding, - item: Book, - payloads: MutableList - ) = binding.run { - if (payloads.isEmpty()) { - cvContent.transitionName = "book_${item.bookUrl}" - tvName.text = item.name - ivCover.load(item.getDisplayCover(), item.name, item.author, false, item.origin) - upRefresh(binding, item) - } else { - for (i in payloads.indices) { - val bundle = payloads[i] as Bundle - bundle.keySet().forEach { - when (it) { - "name" -> tvName.text = item.name - "cover" -> ivCover.load( - item.getDisplayCover(), - item.name, - item.author, - false, - item.origin - ) - - "refresh" -> upRefresh(binding, item) - } - } - } - } - } - - private fun upRefresh(binding: ItemBookshelfGridCompactBinding, item: Book) { - if (!item.isLocal && callBack.isUpdate(item.bookUrl)) { - binding.cdUnread.visibility = View.GONE - binding.rlLoading.visibility = View.VISIBLE - } else { - binding.rlLoading.visibility = View.GONE - if (AppConfig.showUnread) { - val unreadCount = item.getUnreadChapterNum() - if (unreadCount > 0) { - binding.cdUnread.visibility = View.VISIBLE - binding.tvUnread.text = unreadCount.toString() - if (AppConfig.showUnreadNew) - binding.newChapter.isVisible = item.lastCheckCount > 0 - } else { - binding.cdUnread.visibility = View.GONE - binding.newChapter.gone() - } - } else { - binding.cdUnread.visibility = View.GONE - } - if (AppConfig.showTip){ - binding.cdTip.visible() - binding.tvTip.text = item.getBookTypeName() - } else { - binding.cdTip.gone() - } - } - } - - override fun registerListener( - holder: ItemViewHolder, - binding: ItemBookshelfGridCompactBinding - ) { - - binding.cvContent.setOnClickListener { - getItem(holder.layoutPosition)?.let { - callBack.open(it, binding.cvContent) - } - } - - binding.cvContent.setOnLongClickListener { - getItem(holder.layoutPosition)?.let { - callBack.openBookInfo(it, binding.cvContent) - } - true - } - } -} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/books/styleDefalut/BooksAdapterGridCover.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/books/styleDefalut/BooksAdapterGridCover.kt deleted file mode 100644 index fe7eac617..000000000 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/books/styleDefalut/BooksAdapterGridCover.kt +++ /dev/null @@ -1,103 +0,0 @@ -package io.legado.app.ui.main.bookshelf.books.styleDefalut - -import android.content.Context -import android.os.Bundle -import android.view.View -import android.view.ViewGroup -import androidx.core.view.isVisible -import io.legado.app.base.adapter.ItemViewHolder -import io.legado.app.data.entities.Book -import io.legado.app.databinding.ItemBookshelfGridCompactBinding -import io.legado.app.help.book.getBookTypeName -import io.legado.app.help.book.isLocal -import io.legado.app.help.config.AppConfig -import io.legado.app.utils.gone -import io.legado.app.utils.visible - -class BooksAdapterGridCover(context: Context, private val callBack: CallBack) : - BaseBooksAdapter(context) { - - override fun getViewBinding(parent: ViewGroup): ItemBookshelfGridCompactBinding { - return ItemBookshelfGridCompactBinding.inflate(inflater, parent, false) - } - - override fun convert( - holder: ItemViewHolder, - binding: ItemBookshelfGridCompactBinding, - item: Book, - payloads: MutableList - ) = binding.run { - if (payloads.isEmpty()) { - cvContent.transitionName = "book_${item.bookUrl}" - tvName.gone() - ivCover.load(item.getDisplayCover(), item.name, item.author, false, item.origin) - upRefresh(binding, item) - } else { - for (i in payloads.indices) { - val bundle = payloads[i] as Bundle - bundle.keySet().forEach { - when (it) { - "name" -> tvName.gone() - "cover" -> ivCover.load( - item.getDisplayCover(), - item.name, - item.author, - false, - item.origin - ) - - "refresh" -> upRefresh(binding, item) - } - } - } - } - } - - private fun upRefresh(binding: ItemBookshelfGridCompactBinding, item: Book) { - if (!item.isLocal && callBack.isUpdate(item.bookUrl)) { - binding.cdUnread.visibility = View.GONE - binding.rlLoading.visibility = View.VISIBLE - } else { - binding.rlLoading.visibility = View.GONE - if (AppConfig.showUnread) { - val unreadCount = item.getUnreadChapterNum() - if (unreadCount > 0) { - binding.cdUnread.visibility = View.VISIBLE - binding.tvUnread.text = unreadCount.toString() - if (AppConfig.showUnreadNew) - binding.newChapter.isVisible = item.lastCheckCount > 0 - } else { - binding.cdUnread.visibility = View.GONE - binding.newChapter.gone() - } - } else { - binding.cdUnread.visibility = View.GONE - } - if (AppConfig.showTip){ - binding.cdTip.visible() - binding.tvTip.text = item.getBookTypeName() - } else { - binding.cdTip.gone() - } - } - } - - override fun registerListener( - holder: ItemViewHolder, - binding: ItemBookshelfGridCompactBinding - ) { - - binding.cvContent.setOnClickListener { - getItem(holder.layoutPosition)?.let { - callBack.open(it, binding.cvContent) - } - } - - binding.cvContent.setOnLongClickListener { - getItem(holder.layoutPosition)?.let { - callBack.openBookInfo(it, binding.cvContent) - } - true - } - } -} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/books/styleDefalut/BooksAdapterList.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/books/styleDefalut/BooksAdapterList.kt deleted file mode 100644 index fc0de4105..000000000 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/books/styleDefalut/BooksAdapterList.kt +++ /dev/null @@ -1,129 +0,0 @@ -package io.legado.app.ui.main.bookshelf.books.styleDefalut - -import android.content.Context -import android.os.Bundle -import android.view.ViewGroup -import androidx.core.view.isVisible -import androidx.fragment.app.Fragment -import androidx.lifecycle.Lifecycle -import io.legado.app.base.adapter.ItemViewHolder -import io.legado.app.data.entities.Book -import io.legado.app.databinding.ItemBookshelfListBinding -import io.legado.app.help.book.getBookTypeName -import io.legado.app.help.book.isLocal -import io.legado.app.help.config.AppConfig -import io.legado.app.utils.gone -import io.legado.app.utils.toTimeAgo -import io.legado.app.utils.visible -import splitties.views.onLongClick - -class BooksAdapterList( - context: Context, - private val fragment: Fragment, - private val callBack: CallBack, - private val lifecycle: Lifecycle -) : BaseBooksAdapter(context) { - - override fun getViewBinding(parent: ViewGroup): ItemBookshelfListBinding { - return ItemBookshelfListBinding.inflate(inflater, parent, false) - } - - override fun convert( - holder: ItemViewHolder, - binding: ItemBookshelfListBinding, - item: Book, - payloads: MutableList - ) = binding.run { - if (payloads.isEmpty()) { - cdCover.transitionName = "book_${item.bookUrl}" - tvName.text = item.name - tvAuthor.text = item.author - tvRead.text = item.durChapterTitle - tvLast.text = item.latestChapterTitle - ivCover.load(item.getDisplayCover(), item.name, item.author, false, item.origin) - upRefresh(binding, item) - upLastUpdateTime(binding, item) - } else { - for (i in payloads.indices) { - val bundle = payloads[i] as Bundle - bundle.keySet().forEach { - when (it) { - "name" -> tvName.text = item.name - "author" -> tvAuthor.text = item.author - "dur" -> tvRead.text = item.durChapterTitle - "last" -> tvLast.text = item.latestChapterTitle - "cover" -> ivCover.load( - item.getDisplayCover(), - item.name, - item.author, - false, - item.origin, - fragment, - lifecycle - ) - - "refresh" -> upRefresh(binding, item) - "lastUpdateTime" -> upLastUpdateTime(binding, item) - } - } - } - } - } - - private fun upRefresh(binding: ItemBookshelfListBinding, item: Book) { - if (!item.isLocal && callBack.isUpdate(item.bookUrl)) { - binding.cdUnread.gone() - binding.rlLoading.visible() - } else { - binding.rlLoading.gone() - if (AppConfig.showUnread) { - val unreadCount = item.getUnreadChapterNum() - if (unreadCount > 0) { - binding.cdUnread.visible() - binding.tvUnread.text = unreadCount.toString() - if (AppConfig.showUnreadNew) - binding.newChapter.isVisible = item.lastCheckCount > 0 - } else { - binding.cdUnread.gone() - binding.newChapter.gone() - } - } else { - binding.cdUnread.gone() - } - if (AppConfig.showTip){ - binding.cdTip.visible() - binding.tvTip.text = item.getBookTypeName() - } else { - binding.cdTip.gone() - } - } - } - - private fun upLastUpdateTime(binding: ItemBookshelfListBinding, item: Book) { - if (AppConfig.showLastUpdateTime && !item.isLocal) { - val time = item.latestChapterTime.toTimeAgo() - if (binding.tvLastUpdateTime.text != time) { - binding.tvLastUpdateTime.text = time - binding.tvLastUpdateTime.isVisible = true - } - } else { - binding.tvLastUpdateTime.isVisible = false - } - } - - override fun registerListener(holder: ItemViewHolder, binding: ItemBookshelfListBinding) { - holder.itemView.apply { - binding.cvContent.setOnClickListener { - getItem(holder.layoutPosition)?.let { - callBack.open(it, binding.cdCover) - } - } - - binding.cvContent.onLongClick { - getItem(holder.layoutPosition)?.let { - callBack.openBookInfo(it, binding.cdCover) - } - } - } - } -} diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/books/styleDefalut/BooksAdapterListCompact.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/books/styleDefalut/BooksAdapterListCompact.kt deleted file mode 100644 index 66b651f06..000000000 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/books/styleDefalut/BooksAdapterListCompact.kt +++ /dev/null @@ -1,133 +0,0 @@ -package io.legado.app.ui.main.bookshelf.books.styleDefalut - -import android.content.Context -import android.os.Bundle -import android.view.ViewGroup -import androidx.core.view.isVisible -import androidx.fragment.app.Fragment -import androidx.lifecycle.Lifecycle -import io.legado.app.R -import io.legado.app.base.adapter.ItemViewHolder -import io.legado.app.data.entities.Book -import io.legado.app.databinding.ItemBookshelfListCompactBinding -import io.legado.app.help.book.getBookTypeName -import io.legado.app.help.book.isLocal -import io.legado.app.help.config.AppConfig -import io.legado.app.utils.gone -import io.legado.app.utils.toTimeAgo -import io.legado.app.utils.visible -import splitties.views.onLongClick - -class BooksAdapterListCompact( - context: Context, - private val fragment: Fragment, - private val callBack: CallBack, - private val lifecycle: Lifecycle -) : BaseBooksAdapter(context) { - - override fun getViewBinding(parent: ViewGroup): ItemBookshelfListCompactBinding { - return ItemBookshelfListCompactBinding.inflate(inflater, parent, false) - } - - override fun convert( - holder: ItemViewHolder, - binding: ItemBookshelfListCompactBinding, - item: Book, - payloads: MutableList - ) = binding.run { - if (payloads.isEmpty()) { - cdCover.transitionName = "book_${item.bookUrl}" - tvName.text = item.name - tvAuthor.text = - context.getString( - R.string.author_read, - item.author, - (item.totalChapterNum - item.durChapterIndex).coerceAtLeast(0) - ) - tvLast.text = item.latestChapterTitle - ivCover.load(item.getDisplayCover(), item.name, item.author, false, item.origin) - upRefresh(binding, item) - upLastUpdateTime(binding, item) - } else { - for (i in payloads.indices) { - val bundle = payloads[i] as Bundle - bundle.keySet().forEach { - when (it) { - "name" -> tvName.text = item.name - "author" -> tvAuthor.text = item.author - "last" -> tvLast.text = item.latestChapterTitle - "cover" -> ivCover.load( - item.getDisplayCover(), - item.name, - item.author, - false, - item.origin, - fragment, - lifecycle - ) - - "refresh" -> upRefresh(binding, item) - "lastUpdateTime" -> upLastUpdateTime(binding, item) - } - } - } - } - } - - private fun upRefresh(binding: ItemBookshelfListCompactBinding, item: Book) { - if (!item.isLocal && callBack.isUpdate(item.bookUrl)) { - binding.cdUnread.gone() - binding.rlLoading.visible() - } else { - binding.rlLoading.gone() - if (AppConfig.showUnread) { - val unreadCount = item.getUnreadChapterNum() - if (unreadCount > 0) { - binding.cdUnread.visible() - binding.tvUnread.text = unreadCount.toString() - if (AppConfig.showUnreadNew) - binding.newChapter.isVisible = item.lastCheckCount > 0 - } else { - binding.cdUnread.gone() - binding.newChapter.gone() - } - } else { - binding.cdUnread.gone() - } - if (AppConfig.showTip){ - binding.cdTip.visible() - binding.tvTip.text = item.getBookTypeName() - } else { - binding.cdTip.gone() - } - } - } - - private fun upLastUpdateTime(binding: ItemBookshelfListCompactBinding, item: Book) { - if (AppConfig.showLastUpdateTime && !item.isLocal) { - val time = item.latestChapterTime.toTimeAgo() - if (binding.tvLastUpdateTime.text != time) { - binding.tvLastUpdateTime.text = time - binding.tvLastUpdateTime.isVisible = true - } - } else { - binding.tvLastUpdateTime.isVisible = false - } - } - - override fun registerListener(holder: ItemViewHolder, binding: ItemBookshelfListCompactBinding) { - holder.itemView.apply { - binding.cvContent.setOnClickListener { - getItem(holder.layoutPosition)?.let { - callBack.open(it, binding.cdCover) - } - } - - binding.cvContent.onLongClick { - getItem(holder.layoutPosition)?.let { - callBack.openBookInfo(it, binding.cdCover) - } - } - } - } -} diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/books/styleFold/BaseBooksAdapter.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/books/styleFold/BaseBooksAdapter.kt deleted file mode 100644 index ef6bcbd12..000000000 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/books/styleFold/BaseBooksAdapter.kt +++ /dev/null @@ -1,146 +0,0 @@ -package io.legado.app.ui.main.bookshelf.books.styleFold - -import android.content.Context -import android.view.LayoutInflater -import android.view.View -import androidx.core.os.bundleOf -import androidx.recyclerview.widget.AsyncListDiffer -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.RecyclerView -import io.legado.app.data.entities.Book -import io.legado.app.data.entities.BookGroup - -abstract class BaseBooksAdapter( - val context: Context, - val callBack: CallBack -) : RecyclerView.Adapter() { - - protected val inflater: LayoutInflater = LayoutInflater.from(context) - - private val diffItemCallback = object : DiffUtil.ItemCallback() { - - override fun areItemsTheSame(oldItem: Any, newItem: Any): Boolean { - return when { - oldItem is Book && newItem is Book -> { - oldItem.bookUrl == newItem.bookUrl - } - - oldItem is BookGroup && newItem is BookGroup -> { - oldItem.groupId == newItem.groupId - } - - else -> false - } - } - - override fun areContentsTheSame(oldItem: Any, newItem: Any): Boolean { - return when { - oldItem is Book && newItem is Book -> { - oldItem.durChapterTime == newItem.durChapterTime && - oldItem.name == newItem.name && - oldItem.author == newItem.author && - oldItem.durChapterTitle == newItem.durChapterTitle && - oldItem.latestChapterTitle == newItem.latestChapterTitle && - oldItem.lastCheckCount == newItem.lastCheckCount && - oldItem.getDisplayCover() == newItem.getDisplayCover() && - oldItem.getUnreadChapterNum() == newItem.getUnreadChapterNum() - } - - oldItem is BookGroup && newItem is BookGroup -> false - - else -> false - } - } - - override fun getChangePayload(oldItem: Any, newItem: Any): Any? { - val bundle = bundleOf() - when { - oldItem is Book && newItem is Book -> { - if (oldItem.name != newItem.name) { - bundle.putString("name", newItem.name) - } - if (oldItem.author != newItem.author) { - bundle.putString("author", newItem.author) - } - if (oldItem.durChapterTitle != newItem.durChapterTitle) { - bundle.putString("dur", newItem.durChapterTitle) - } - if (oldItem.latestChapterTitle != newItem.latestChapterTitle) { - bundle.putString("last", newItem.latestChapterTitle) - } - if (oldItem.getDisplayCover() != newItem.getDisplayCover()) { - bundle.putString("cover", newItem.getDisplayCover()) - } - if (oldItem.lastCheckCount != newItem.lastCheckCount - || oldItem.durChapterTime != newItem.durChapterTime - || oldItem.getUnreadChapterNum() != newItem.getUnreadChapterNum() - || oldItem.lastCheckCount != newItem.lastCheckCount - ) { - bundle.putBoolean("refresh", true) - } - } - - oldItem is BookGroup && newItem is BookGroup -> { - if (oldItem.groupName != newItem.groupName) { - bundle.putString("groupName", newItem.groupName) - } - if (oldItem.cover != newItem.cover) { - bundle.putString("cover", newItem.cover) - } - } - } - if (bundle.isEmpty) return null - return bundle - } - } - - private val asyncListDiffer by lazy { - AsyncListDiffer(this, diffItemCallback) - } - - fun updateItems() { - asyncListDiffer.submitList(callBack.getItems()) - } - - fun notification(bookUrl: String) { - for (i in 0 until itemCount) { - getItem(i).let { - if (it is Book && it.bookUrl == bookUrl) { - notifyItemChanged(i, bundleOf(Pair("refresh", null))) - return - } - } - } - } - - fun getItems() = asyncListDiffer.currentList - - fun getItem(position: Int) = getItems().getOrNull(position) - - internal var allBooks: List = emptyList() - - fun setAllBooks(all: List) { - this.allBooks = all - } - - override fun getItemCount(): Int { - return getItems().size - } - - override fun getItemViewType(position: Int): Int { - if (getItem(position) is BookGroup) { - return 1 - } - return 0 - } - - final override fun onBindViewHolder(holder: VH, position: Int) {} - - - interface CallBack { - fun onItemClick(item: Any, sharedView: View) - fun onItemLongClick(item: Any, sharedView: View) - fun isUpdate(bookUrl: String): Boolean - fun getItems(): List - } -} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/books/styleFold/BooksAdapterGrid.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/books/styleFold/BooksAdapterGrid.kt deleted file mode 100644 index 9fdedbb29..000000000 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/books/styleFold/BooksAdapterGrid.kt +++ /dev/null @@ -1,195 +0,0 @@ -package io.legado.app.ui.main.bookshelf.books.styleFold - -import android.content.Context -import android.os.Bundle -import android.view.View -import android.view.ViewGroup -import androidx.core.content.ContextCompat -import androidx.core.view.isVisible -import androidx.recyclerview.widget.RecyclerView -import io.legado.app.R -import io.legado.app.data.entities.Book -import io.legado.app.data.entities.BookGroup -import io.legado.app.databinding.ItemBookshelfGridBinding -import io.legado.app.databinding.ItemBookshelfGridGroupBinding -import io.legado.app.help.book.getBookTypeName -import io.legado.app.help.book.isLocal -import io.legado.app.help.config.AppConfig -import io.legado.app.utils.gone -import io.legado.app.utils.visible -import splitties.views.onLongClick - -@Suppress("UNUSED_PARAMETER") -class BooksAdapterGrid(context: Context, callBack: CallBack) : - BaseBooksAdapter(context, callBack) { - - override fun onCreateViewHolder( - parent: ViewGroup, - viewType: Int - ): RecyclerView.ViewHolder { - return when (viewType) { - 1 -> GroupViewHolder(ItemBookshelfGridGroupBinding.inflate(inflater, parent, false)) - else -> BookViewHolder(ItemBookshelfGridBinding.inflate(inflater, parent, false)) - } - } - - override fun onBindViewHolder( - holder: RecyclerView.ViewHolder, - position: Int, - payloads: MutableList - ) { - - when (holder) { - is BookViewHolder -> (getItem(position) as? Book)?.let { - holder.registerListener(it) - holder.onBind(it, payloads) - } - - is GroupViewHolder -> (getItem(position) as? BookGroup)?.let { - holder.registerListener(it) - holder.onBind(it, payloads) - } - } - } - - private fun getBooksForGroup(group: BookGroup): List { - return if (group.groupId == -1L) { - allBooks.take(4) - } else { - allBooks.filter { (it.group and group.groupId) != 0L }.take(4) - } - } - - inner class BookViewHolder(val binding: ItemBookshelfGridBinding) : - RecyclerView.ViewHolder(binding.root) { - - fun onBind(item: Book) = binding.run { - cvContent.transitionName = "book_${item.bookUrl}" - tvName.text = item.name - ivCover.load(item.getDisplayCover(), item.name, item.author, false, item.origin) - upRefresh(this, item) - } - - fun onBind(item: Book, payloads: MutableList) = binding.run { - if (payloads.isEmpty()) { - onBind(item) - } else { - for (i in payloads.indices) { - val bundle = payloads[i] as Bundle - bundle.keySet().forEach { - when (it) { - "name" -> tvName.text = item.name - "cover" -> ivCover.load( - item.getDisplayCover(), - item.name, - item.author, - false, - item.origin - ) - - "refresh" -> upRefresh(this, item) - } - } - } - } - } - - fun registerListener(item: Any) { - binding.cvContent.setOnClickListener { - callBack.onItemClick(item, binding.cvContent) - } - binding.cvContent.onLongClick { - callBack.onItemLongClick(item, binding.cvContent) - } - } - - private fun upRefresh(binding: ItemBookshelfGridBinding, item: Book) { - if (!item.isLocal && callBack.isUpdate(item.bookUrl)) { - binding.cdUnread.visibility = View.GONE - binding.rlLoading.visibility = View.VISIBLE - } else { - binding.rlLoading.visibility = View.GONE - if (AppConfig.showUnread) { - val unreadCount = item.getUnreadChapterNum() - if (unreadCount > 0) { - binding.cdUnread.visibility = View.VISIBLE - binding.tvUnread.text = unreadCount.toString() - if (AppConfig.showUnreadNew) - binding.newChapter.isVisible = item.lastCheckCount > 0 - } else { - binding.cdUnread.visibility = View.GONE - } - } else { - binding.cdUnread.visibility = View.GONE - } - if (AppConfig.showTip){ - binding.cdTip.visible() - binding.tvTip.text = item.getBookTypeName() - } else { - binding.cdTip.gone() - } - } - } - - - } - - inner class GroupViewHolder(val binding: ItemBookshelfGridGroupBinding) : - RecyclerView.ViewHolder(binding.root) { - - fun onBind(item: BookGroup) = binding.run { - tvName.text = item.groupName - loadGroupCover(item) - cdUnread.gone() - } - - fun onBind(item: BookGroup, payloads: MutableList) = binding.run { - if (payloads.isEmpty()) { - onBind(item) - } else { - for (i in payloads.indices) { - val bundle = payloads[i] as Bundle - bundle.keySet().forEach { - when (it) { - "groupName" -> tvName.text = item.groupName - "cover" -> loadGroupCover(item) - } - } - } - } - } - - fun registerListener(item: Any) { - binding.cvContent.setOnClickListener { - callBack.onItemClick(item, binding.cdCover) - } - binding.cvContent.onLongClick { - callBack.onItemLongClick(item, binding.cdCover) - } - } - - private fun loadGroupCover(item: BookGroup) = binding.run { - - binding.ivCover.setImageDrawable( - ContextCompat.getDrawable(binding.root.context, R.drawable.bg_surface_variant) - ) - binding.ivCover1.setImageDrawable(null) - binding.ivCover2.setImageDrawable(null) - binding.ivCover3.setImageDrawable(null) - binding.ivCover4.setImageDrawable(null) - if (!item.cover.isNullOrEmpty()) { - ivCover.load(item.cover) - } else { - - val books = getBooksForGroup(item) - - if (books.size > 0) binding.ivCover1.load(books[0].getDisplayCover()) - if (books.size > 1) binding.ivCover2.load(books[1].getDisplayCover()) - if (books.size > 2) binding.ivCover3.load(books[2].getDisplayCover()) - if (books.size > 3) binding.ivCover4.load(books[3].getDisplayCover()) - } - } - - } - -} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/books/styleFold/BooksAdapterGridCompact.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/books/styleFold/BooksAdapterGridCompact.kt deleted file mode 100644 index 41419292b..000000000 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/books/styleFold/BooksAdapterGridCompact.kt +++ /dev/null @@ -1,202 +0,0 @@ -package io.legado.app.ui.main.bookshelf.books.styleFold - -import android.content.Context -import android.os.Bundle -import android.view.View -import android.view.ViewGroup -import androidx.core.content.ContextCompat -import androidx.core.view.isVisible -import androidx.recyclerview.widget.RecyclerView -import io.legado.app.R -import io.legado.app.data.entities.Book -import io.legado.app.data.entities.BookGroup -import io.legado.app.databinding.ItemBookshelfGridCompactBinding -import io.legado.app.databinding.ItemBookshelfGridCompactGroupBinding -import io.legado.app.help.book.getBookTypeName -import io.legado.app.help.book.isLocal -import io.legado.app.help.config.AppConfig -import io.legado.app.utils.gone -import io.legado.app.utils.visible -import splitties.views.onLongClick - -@Suppress("UNUSED_PARAMETER") -class BooksAdapterGridCompact(context: Context, callBack: CallBack) : - BaseBooksAdapter(context, callBack) { - - override fun onCreateViewHolder( - parent: ViewGroup, - viewType: Int - ): RecyclerView.ViewHolder { - return when (viewType) { - 1 -> GroupViewHolder( - ItemBookshelfGridCompactGroupBinding.inflate( - inflater, - parent, - false - ) - ) - - else -> BookViewHolder(ItemBookshelfGridCompactBinding.inflate(inflater, parent, false)) - } - } - - override fun onBindViewHolder( - holder: RecyclerView.ViewHolder, - position: Int, - payloads: MutableList - ) { - - when (holder) { - is BookViewHolder -> (getItem(position) as? Book)?.let { - holder.registerListener(it) - holder.onBind(it, payloads) - } - - is GroupViewHolder -> (getItem(position) as? BookGroup)?.let { - holder.registerListener(it) - holder.onBind(it, payloads) - } - } - } - - private fun getBooksForGroup(group: BookGroup): List { - return if (group.groupId == -1L) { - allBooks.take(4) - } else { - allBooks.filter { (it.group and group.groupId) != 0L }.take(4) - } - } - - inner class BookViewHolder(val binding: ItemBookshelfGridCompactBinding) : - RecyclerView.ViewHolder(binding.root) { - - fun onBind(item: Book) = binding.run { - cvContent.transitionName = "book_${item.bookUrl}" - tvName.text = item.name - ivCover.load(item.getDisplayCover(), item.name, item.author, false, item.origin) - upRefresh(this, item) - } - - fun onBind(item: Book, payloads: MutableList) = binding.run { - if (payloads.isEmpty()) { - onBind(item) - } else { - for (i in payloads.indices) { - val bundle = payloads[i] as Bundle - bundle.keySet().forEach { - when (it) { - "name" -> tvName.text = item.name - "cover" -> ivCover.load( - item.getDisplayCover(), - item.name, - item.author, - false, - item.origin - ) - - "refresh" -> upRefresh(this, item) - } - } - } - } - } - - fun registerListener(item: Any) { - binding.cvContent.setOnClickListener { - callBack.onItemClick(item, binding.cvContent) - } - binding.cvContent.onLongClick { - callBack.onItemLongClick(item, binding.cvContent) - } - } - - private fun upRefresh(binding: ItemBookshelfGridCompactBinding, item: Book) { - if (!item.isLocal && callBack.isUpdate(item.bookUrl)) { - binding.cdUnread.visibility = View.GONE - binding.rlLoading.visibility = View.VISIBLE - } else { - binding.rlLoading.visibility = View.GONE - if (AppConfig.showUnread) { - val unreadCount = item.getUnreadChapterNum() - if (unreadCount > 0) { - binding.cdUnread.visibility = View.VISIBLE - binding.tvUnread.text = unreadCount.toString() - if (AppConfig.showUnreadNew) - binding.newChapter.isVisible = item.lastCheckCount > 0 - } else { - binding.cdUnread.visibility = View.GONE - } - } else { - binding.cdUnread.visibility = View.GONE - } - if (AppConfig.showTip){ - binding.cdTip.visible() - binding.tvTip.text = item.getBookTypeName() - } else { - binding.cdTip.gone() - } - } - } - - - } - - inner class GroupViewHolder(val binding: ItemBookshelfGridCompactGroupBinding) : - RecyclerView.ViewHolder(binding.root) { - - fun onBind(item: BookGroup) = binding.run { - tvName.text = item.groupName - loadGroupCover(item) - cdUnread.gone() - } - - fun onBind(item: BookGroup, payloads: MutableList) = binding.run { - if (payloads.isEmpty()) { - onBind(item) - } else { - for (i in payloads.indices) { - val bundle = payloads[i] as Bundle - bundle.keySet().forEach { - when (it) { - "groupName" -> tvName.text = item.groupName - "cover" -> loadGroupCover(item) - } - } - } - } - } - - fun registerListener(item: Any) { - binding.cvContent.setOnClickListener { - callBack.onItemClick(item, binding.cvContent) - } - binding.cvContent.onLongClick { - callBack.onItemLongClick(item, binding.cvContent) - } - } - - private fun loadGroupCover(item: BookGroup) = binding.run { - - binding.ivCover.setImageDrawable( - ContextCompat.getDrawable(binding.root.context, R.drawable.bg_surface_variant) - ) - binding.ivCover1.setImageDrawable(null) - binding.ivCover2.setImageDrawable(null) - binding.ivCover3.setImageDrawable(null) - binding.ivCover4.setImageDrawable(null) - if (!item.cover.isNullOrEmpty()) { - ivCover.load(item.cover) - } else { - - val books = getBooksForGroup(item) - - if (books.size > 0) binding.ivCover1.load(books[0].getDisplayCover()) - if (books.size > 1) binding.ivCover2.load(books[1].getDisplayCover()) - if (books.size > 2) binding.ivCover3.load(books[2].getDisplayCover()) - if (books.size > 3) binding.ivCover4.load(books[3].getDisplayCover()) - } - } - - } - -} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/books/styleFold/BooksAdapterGridCover.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/books/styleFold/BooksAdapterGridCover.kt deleted file mode 100644 index 1cd492cce..000000000 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/books/styleFold/BooksAdapterGridCover.kt +++ /dev/null @@ -1,202 +0,0 @@ -package io.legado.app.ui.main.bookshelf.books.styleFold - -import android.content.Context -import android.os.Bundle -import android.view.View -import android.view.ViewGroup -import androidx.core.content.ContextCompat -import androidx.core.view.isVisible -import androidx.recyclerview.widget.RecyclerView -import io.legado.app.R -import io.legado.app.data.entities.Book -import io.legado.app.data.entities.BookGroup -import io.legado.app.databinding.ItemBookshelfGridCompactBinding -import io.legado.app.databinding.ItemBookshelfGridCompactGroupBinding -import io.legado.app.help.book.getBookTypeName -import io.legado.app.help.book.isLocal -import io.legado.app.help.config.AppConfig -import io.legado.app.utils.gone -import io.legado.app.utils.visible -import splitties.views.onLongClick - -@Suppress("UNUSED_PARAMETER") -class BooksAdapterGridCover(context: Context, callBack: CallBack) : - BaseBooksAdapter(context, callBack) { - - override fun onCreateViewHolder( - parent: ViewGroup, - viewType: Int - ): RecyclerView.ViewHolder { - return when (viewType) { - 1 -> GroupViewHolder( - ItemBookshelfGridCompactGroupBinding.inflate( - inflater, - parent, - false - ) - ) - - else -> BookViewHolder(ItemBookshelfGridCompactBinding.inflate(inflater, parent, false)) - } - } - - override fun onBindViewHolder( - holder: RecyclerView.ViewHolder, - position: Int, - payloads: MutableList - ) { - - when (holder) { - is BookViewHolder -> (getItem(position) as? Book)?.let { - holder.registerListener(it) - holder.onBind(it, payloads) - } - - is GroupViewHolder -> (getItem(position) as? BookGroup)?.let { - holder.registerListener(it) - holder.onBind(it, payloads) - } - } - } - - private fun getBooksForGroup(group: BookGroup): List { - return if (group.groupId == -1L) { - allBooks.take(4) - } else { - allBooks.filter { (it.group and group.groupId) != 0L }.take(4) - } - } - - inner class BookViewHolder(val binding: ItemBookshelfGridCompactBinding) : - RecyclerView.ViewHolder(binding.root) { - - fun onBind(item: Book) = binding.run { - cvContent.transitionName = "book_${item.bookUrl}" - tvName.gone() - ivCover.load(item.getDisplayCover(), item.name, item.author, false, item.origin) - upRefresh(this, item) - } - - fun onBind(item: Book, payloads: MutableList) = binding.run { - if (payloads.isEmpty()) { - onBind(item) - } else { - for (i in payloads.indices) { - val bundle = payloads[i] as Bundle - bundle.keySet().forEach { - when (it) { - "name" -> tvName.gone() - "cover" -> ivCover.load( - item.getDisplayCover(), - item.name, - item.author, - false, - item.origin - ) - - "refresh" -> upRefresh(this, item) - } - } - } - } - } - - fun registerListener(item: Any) { - binding.cvContent.setOnClickListener { - callBack.onItemClick(item, binding.cvContent) - } - binding.cvContent.onLongClick { - callBack.onItemLongClick(item, binding.cvContent) - } - } - - private fun upRefresh(binding: ItemBookshelfGridCompactBinding, item: Book) { - if (!item.isLocal && callBack.isUpdate(item.bookUrl)) { - binding.cdUnread.visibility = View.GONE - binding.rlLoading.visibility = View.VISIBLE - } else { - binding.rlLoading.visibility = View.GONE - if (AppConfig.showUnread) { - val unreadCount = item.getUnreadChapterNum() - if (unreadCount > 0) { - binding.cdUnread.visibility = View.VISIBLE - binding.tvUnread.text = unreadCount.toString() - if (AppConfig.showUnreadNew) - binding.newChapter.isVisible = item.lastCheckCount > 0 - } else { - binding.cdUnread.visibility = View.GONE - } - } else { - binding.cdUnread.visibility = View.GONE - } - if (AppConfig.showTip){ - binding.cdTip.visible() - binding.tvTip.text = item.getBookTypeName() - } else { - binding.cdTip.gone() - } - } - } - - - } - - inner class GroupViewHolder(val binding: ItemBookshelfGridCompactGroupBinding) : - RecyclerView.ViewHolder(binding.root) { - - fun onBind(item: BookGroup) = binding.run { - tvName.gone() - loadGroupCover(item) - cdUnread.gone() - } - - fun onBind(item: BookGroup, payloads: MutableList) = binding.run { - if (payloads.isEmpty()) { - onBind(item) - } else { - for (i in payloads.indices) { - val bundle = payloads[i] as Bundle - bundle.keySet().forEach { - when (it) { - "groupName" -> tvName.gone() - "cover" -> loadGroupCover(item) - } - } - } - } - } - - fun registerListener(item: Any) { - binding.cvContent.setOnClickListener { - callBack.onItemClick(item, binding.cvContent) - } - binding.cvContent.onLongClick { - callBack.onItemLongClick(item, binding.cvContent) - } - } - - private fun loadGroupCover(item: BookGroup) = binding.run { - - binding.ivCover.setImageDrawable( - ContextCompat.getDrawable(binding.root.context, R.drawable.bg_surface_variant) - ) - binding.ivCover1.setImageDrawable(null) - binding.ivCover2.setImageDrawable(null) - binding.ivCover3.setImageDrawable(null) - binding.ivCover4.setImageDrawable(null) - if (!item.cover.isNullOrEmpty()) { - ivCover.load(item.cover) - } else { - - val books = getBooksForGroup(item) - - if (books.size > 0) binding.ivCover1.load(books[0].getDisplayCover()) - if (books.size > 1) binding.ivCover2.load(books[1].getDisplayCover()) - if (books.size > 2) binding.ivCover3.load(books[2].getDisplayCover()) - if (books.size > 3) binding.ivCover4.load(books[3].getDisplayCover()) - } - } - - } - -} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/books/styleFold/BooksAdapterList.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/books/styleFold/BooksAdapterList.kt deleted file mode 100644 index 5a24ad01d..000000000 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/books/styleFold/BooksAdapterList.kt +++ /dev/null @@ -1,215 +0,0 @@ -package io.legado.app.ui.main.bookshelf.books.styleFold - -import android.content.Context -import android.os.Bundle -import android.view.ViewGroup -import androidx.core.content.ContextCompat -import androidx.core.view.isVisible -import androidx.recyclerview.widget.RecyclerView -import io.legado.app.R -import io.legado.app.data.entities.Book -import io.legado.app.data.entities.BookGroup -import io.legado.app.databinding.ItemBookshelfListBinding -import io.legado.app.databinding.ItemBookshelfListGroupBinding -import io.legado.app.help.book.getBookTypeName -import io.legado.app.help.book.isLocal -import io.legado.app.help.config.AppConfig -import io.legado.app.utils.gone -import io.legado.app.utils.toTimeAgo -import io.legado.app.utils.visible -import splitties.views.onLongClick - -@Suppress("UNUSED_PARAMETER") -class BooksAdapterList(context: Context, callBack: CallBack) : - BaseBooksAdapter(context, callBack) { - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - return when (viewType) { - 1 -> GroupViewHolder(ItemBookshelfListGroupBinding.inflate(inflater, parent, false)) - else -> BookViewHolder(ItemBookshelfListBinding.inflate(inflater, parent, false)) - } - } - - override fun onBindViewHolder( - holder: RecyclerView.ViewHolder, - position: Int, - payloads: MutableList - ) { - when (holder) { - is BookViewHolder -> (getItem(position) as? Book)?.let { - holder.registerListener(it) - holder.onBind(it, payloads) - } - - is GroupViewHolder -> (getItem(position) as? BookGroup)?.let { - holder.registerListener(it) - holder.onBind(it, payloads) - } - } - } - - private fun getBooksForGroup(group: BookGroup): List { - return if (group.groupId == -1L) { - allBooks.take(4) - } else { - allBooks.filter { (it.group and group.groupId) != 0L }.take(4) - } - } - - inner class BookViewHolder(val binding: ItemBookshelfListBinding) : - RecyclerView.ViewHolder(binding.root) { - - fun onBind(item: Book) = binding.run { - cdCover.transitionName = "book_${item.bookUrl}" - tvName.text = item.name - tvAuthor.text = item.author - tvRead.text = item.durChapterTitle - tvLast.text = item.latestChapterTitle - ivCover.load(item.getDisplayCover(), item.name, item.author, false, item.origin) - cdUnread.visible() -// ivAuthor.visible() -// ivLast.visible() -// ivRead.visible() - upRefresh(this, item) - upLastUpdateTime(binding, item) - } - - fun onBind(item: Book, payloads: MutableList) = binding.run { - if (payloads.isEmpty()) { - onBind(item) - } else { - for (i in payloads.indices) { - val bundle = payloads[i] as Bundle - bundle.keySet().forEach { - when (it) { - "name" -> tvName.text = item.name - "author" -> tvAuthor.text = item.author - "dur" -> tvRead.text = item.durChapterTitle - "last" -> tvLast.text = item.latestChapterTitle - "cover" -> ivCover.load( - item.getDisplayCover(), - item.name, - item.author, - false, - item.origin - ) - - "refresh" -> upRefresh(this, item) - "lastUpdateTime" -> upLastUpdateTime(binding, item) - } - } - } - } - } - - private fun upLastUpdateTime(binding: ItemBookshelfListBinding, item: Book) { - if (AppConfig.showLastUpdateTime && !item.isLocal) { - val time = item.latestChapterTime.toTimeAgo() - if (binding.tvLastUpdateTime.text != time) { - binding.tvLastUpdateTime.text = time - binding.tvLastUpdateTime.visible() - } - } else { - binding.tvLastUpdateTime.gone() - } - } - - fun registerListener(item: Any) { - binding.cvContent.setOnClickListener { - callBack.onItemClick(item, binding.cdCover) - } - binding.cvContent.onLongClick { - callBack.onItemLongClick(item, binding.cdCover) - } - } - - private fun upRefresh(binding: ItemBookshelfListBinding, item: Book) { - if (!item.isLocal && callBack.isUpdate(item.bookUrl)) { - binding.cdUnread.gone() - binding.rlLoading.visible() - } else { - binding.rlLoading.gone() - if (AppConfig.showUnread) { - val unreadCount = item.getUnreadChapterNum() - if (unreadCount > 0) { - binding.cdUnread.visible() - binding.tvUnread.text = unreadCount.toString() - if (AppConfig.showUnreadNew) - binding.newChapter.isVisible = item.lastCheckCount > 0 - } else { - binding.cdUnread.gone() - } - } else { - binding.cdUnread.gone() - } - if (AppConfig.showTip){ - binding.cdTip.visible() - binding.tvTip.text = item.getBookTypeName() - } else { - binding.cdTip.gone() - } - } - } - - } - - inner class GroupViewHolder(val binding: ItemBookshelfListGroupBinding) : - RecyclerView.ViewHolder(binding.root) { - - fun onBind(item: BookGroup) = binding.run { - tvName.text = item.groupName - loadGroupCover(item) - cdUnread.gone() - } - - - fun onBind(item: BookGroup, payloads: MutableList) = binding.run { - if (payloads.isEmpty()) { - onBind(item) - } else { - for (i in payloads.indices) { - val bundle = payloads[i] as Bundle - bundle.keySet().forEach { - when (it) { - "groupName" -> tvName.text = item.groupName - "cover" -> loadGroupCover(item) - } - } - } - } - } - - fun registerListener(item: Any) { - binding.cvContent.setOnClickListener { - callBack.onItemClick(item, binding.ivCover) - } - binding.cvContent.onLongClick { - callBack.onItemLongClick(item, binding.ivCover) - } - } - - private fun loadGroupCover(item: BookGroup) = binding.run { - - binding.ivCover.setImageDrawable( - ContextCompat.getDrawable(binding.root.context, R.drawable.bg_surface_variant) - ) - binding.ivCover1.setImageDrawable(null) - binding.ivCover2.setImageDrawable(null) - binding.ivCover3.setImageDrawable(null) - binding.ivCover4.setImageDrawable(null) - if (!item.cover.isNullOrEmpty()) { - ivCover.load(item.cover) - } else { - - val books = getBooksForGroup(item) - - if (books.size > 0) binding.ivCover1.load(books[0].getDisplayCover()) - if (books.size > 1) binding.ivCover2.load(books[1].getDisplayCover()) - if (books.size > 2) binding.ivCover3.load(books[2].getDisplayCover()) - if (books.size > 3) binding.ivCover4.load(books[3].getDisplayCover()) - } - } - - } - -} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/books/styleFold/BooksAdapterListCompact.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/books/styleFold/BooksAdapterListCompact.kt deleted file mode 100644 index 7def24064..000000000 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/books/styleFold/BooksAdapterListCompact.kt +++ /dev/null @@ -1,209 +0,0 @@ -package io.legado.app.ui.main.bookshelf.books.styleFold - -import android.content.Context -import android.os.Bundle -import android.view.ViewGroup -import androidx.core.content.ContextCompat -import androidx.core.view.isVisible -import androidx.recyclerview.widget.RecyclerView -import io.legado.app.R -import io.legado.app.data.entities.Book -import io.legado.app.data.entities.BookGroup -import io.legado.app.databinding.ItemBookshelfListCompactBinding -import io.legado.app.databinding.ItemBookshelfListCompactGroupBinding -import io.legado.app.help.book.getBookTypeName -import io.legado.app.help.book.isLocal -import io.legado.app.help.config.AppConfig -import io.legado.app.utils.gone -import io.legado.app.utils.toTimeAgo -import io.legado.app.utils.visible -import splitties.views.onLongClick - -@Suppress("UNUSED_PARAMETER") -class BooksAdapterListCompact(context: Context, callBack: CallBack) : - BaseBooksAdapter(context, callBack) { - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - return when (viewType) { - 1 -> GroupViewHolder(ItemBookshelfListCompactGroupBinding.inflate(inflater, parent, false)) - else -> BookViewHolder(ItemBookshelfListCompactBinding.inflate(inflater, parent, false)) - } - } - - override fun onBindViewHolder( - holder: RecyclerView.ViewHolder, - position: Int, - payloads: MutableList - ) { - when (holder) { - is BookViewHolder -> (getItem(position) as? Book)?.let { - holder.registerListener(it) - holder.onBind(it, payloads) - } - - is GroupViewHolder -> (getItem(position) as? BookGroup)?.let { - holder.registerListener(it) - holder.onBind(it, payloads) - } - } - } - - private fun getBooksForGroup(group: BookGroup): List { - return if (group.groupId == -1L) { - allBooks.take(4) - } else { - allBooks.filter { (it.group and group.groupId) != 0L }.take(4) - } - } - - inner class BookViewHolder(val binding: ItemBookshelfListCompactBinding) : - RecyclerView.ViewHolder(binding.root) { - - fun onBind(item: Book) = binding.run { - cdCover.transitionName = "book_${item.bookUrl}" - tvName.text = item.name - tvAuthor.text = item.author - tvLast.text = item.latestChapterTitle - ivCover.load(item.getDisplayCover(), item.name, item.author, false, item.origin) - cdUnread.visible() -// ivAuthor.visible() -// ivLast.visible() -// ivRead.visible() - upRefresh(this, item) - upLastUpdateTime(binding, item) - } - - fun onBind(item: Book, payloads: MutableList) = binding.run { - if (payloads.isEmpty()) { - onBind(item) - } else { - for (i in payloads.indices) { - val bundle = payloads[i] as Bundle - bundle.keySet().forEach { - when (it) { - "name" -> tvName.text = item.name - "author" -> tvAuthor.text = item.author - "last" -> tvLast.text = item.latestChapterTitle - "cover" -> ivCover.load( - item.getDisplayCover(), - item.name, - item.author, - false, - item.origin - ) - - "refresh" -> upRefresh(this, item) - "lastUpdateTime" -> upLastUpdateTime(binding, item) - } - } - } - } - } - - private fun upLastUpdateTime(binding: ItemBookshelfListCompactBinding, item: Book) { - if (AppConfig.showLastUpdateTime && !item.isLocal) { - val time = item.latestChapterTime.toTimeAgo() - if (binding.tvLastUpdateTime.text != time) { - binding.tvLastUpdateTime.text = time - binding.tvLastUpdateTime.visible() - } - } else { - binding.tvLastUpdateTime.gone() - } - } - - fun registerListener(item: Any) { - binding.cvContent.setOnClickListener { - callBack.onItemClick(item, binding.cdCover) - } - binding.cvContent.onLongClick { - callBack.onItemLongClick(item, binding.cdCover) - } - } - - private fun upRefresh(binding: ItemBookshelfListCompactBinding, item: Book) { - if (!item.isLocal && callBack.isUpdate(item.bookUrl)) { - binding.cdUnread.gone() - binding.rlLoading.visible() - } else { - binding.rlLoading.gone() - if (AppConfig.showUnread) { - val unreadCount = item.getUnreadChapterNum() - if (unreadCount > 0) { - binding.cdUnread.visible() - binding.tvUnread.text = unreadCount.toString() - if (AppConfig.showUnreadNew) - binding.newChapter.isVisible = item.lastCheckCount > 0 - } else { - binding.cdUnread.gone() - } - } else { - binding.cdUnread.gone() - } - if (AppConfig.showTip){ - binding.cdTip.visible() - binding.tvTip.text = item.getBookTypeName() - } else { - binding.cdTip.gone() - } - } - } - - } - - inner class GroupViewHolder(val binding: ItemBookshelfListCompactGroupBinding) : - RecyclerView.ViewHolder(binding.root) { - - fun onBind(item: BookGroup) = binding.run { - tvName.text = item.groupName - loadGroupCover(item) - cdUnread.gone() - } - - - fun onBind(item: BookGroup, payloads: MutableList) = binding.run { - if (payloads.isEmpty()) { - onBind(item) - } else { - for (i in payloads.indices) { - val bundle = payloads[i] as Bundle - bundle.keySet().forEach { - when (it) { - "groupName" -> tvName.text = item.groupName - "cover" -> loadGroupCover(item) - } - } - } - } - } - - fun registerListener(item: Any) { - binding.cvContent.setOnClickListener { - callBack.onItemClick(item, binding.ivCover) - } - binding.cvContent.onLongClick { - callBack.onItemLongClick(item, binding.ivCover) - } - } - - private fun loadGroupCover(item: BookGroup) = binding.run { - - binding.ivCover.setImageDrawable( - ContextCompat.getDrawable(binding.root.context, R.drawable.bg_surface_variant) - ) - binding.ivCover1.setImageDrawable(null) - binding.ivCover2.setImageDrawable(null) - if (!item.cover.isNullOrEmpty()) { - ivCover.load(item.cover) - } else { - - val books = getBooksForGroup(item) - - if (books.size > 0) binding.ivCover1.load(books[0].getDisplayCover()) - if (books.size > 1) binding.ivCover2.load(books[1].getDisplayCover()) - } - } - - } - -} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/main/explore/ExploreAdapter.kt b/app/src/main/java/io/legado/app/ui/main/explore/ExploreAdapter.kt deleted file mode 100644 index 9f6823e8a..000000000 --- a/app/src/main/java/io/legado/app/ui/main/explore/ExploreAdapter.kt +++ /dev/null @@ -1,225 +0,0 @@ -package io.legado.app.ui.main.explore - -import android.content.Context -import android.view.View -import android.view.ViewGroup -import android.widget.PopupMenu -import android.widget.TextView -import androidx.core.view.children -import com.google.android.flexbox.FlexboxLayout -import com.google.android.material.card.MaterialCardView -import io.legado.app.R -import io.legado.app.base.adapter.ItemViewHolder -import io.legado.app.base.adapter.RecyclerAdapter -import io.legado.app.data.entities.BookSourcePart -import io.legado.app.data.entities.rule.ExploreKind -import io.legado.app.databinding.ItemFilletTextBinding -import io.legado.app.databinding.ItemFindBookBinding -import io.legado.app.help.coroutine.Coroutine -import io.legado.app.help.source.clearExploreKindsCache -import io.legado.app.help.source.exploreKinds -import io.legado.app.ui.login.SourceLoginActivity -import io.legado.app.ui.widget.dialog.TextDialog -import io.legado.app.utils.activity -import io.legado.app.utils.gone -import io.legado.app.utils.removeLastElement -import io.legado.app.utils.showDialogFragment -import io.legado.app.utils.startActivity -import io.legado.app.utils.visible -import kotlinx.coroutines.CoroutineScope -import splitties.views.onLongClick - -class ExploreAdapter( - context: Context, - private val callBack: CallBack -) : RecyclerAdapter(context) { - - private val recycler = arrayListOf() - private var expandedId: String? = null - private var scrollToId: String? = null - - init { - setHasStableIds(true) - } - - override fun getItemId(position: Int): Long { - return getItem(position)?.bookSourceUrl.hashCode().toLong() - } - - override fun getViewBinding(parent: ViewGroup): ItemFindBookBinding { - return ItemFindBookBinding.inflate(inflater, parent, false) - } - - override fun convert( - holder: ItemViewHolder, - binding: ItemFindBookBinding, - item: BookSourcePart, - payloads: MutableList - ) { - binding.run { - if (payloads.isEmpty()) { - tvName.text = item.bookSourceName - } - - val isExpanded = (item.bookSourceUrl == expandedId) - - if (isExpanded) { - ivStatus.isChecked = true - rotateLoading.visible() - if (scrollToId == item.bookSourceUrl) { - val pos = holder.bindingAdapterPosition - if (pos >= 0) callBack.scrollTo(pos) - scrollToId = null - } - - Coroutine.async(callBack.scope) { - item.exploreKinds() - }.onSuccess { kindList -> - upKindList(flexbox, item.bookSourceUrl, kindList) - }.onFinally { - rotateLoading.gone() - } - - } else { - ivStatus.isChecked = false - recyclerFlexbox(flexbox) - flexbox.gone() - } - } - } - - private fun upKindList(flexbox: FlexboxLayout, sourceUrl: String, kinds: List) { - if (kinds.isNotEmpty()) kotlin.runCatching { - recyclerFlexbox(flexbox) - flexbox.visible() - kinds.forEach { kind -> - val cardView = getFlexboxChild(flexbox) - val textView = cardView.findViewById(R.id.text_view) - flexbox.addView(cardView) - - textView.text = kind.title - kind.style().apply(cardView) - - if (kind.url.isNullOrBlank()) { - cardView.setOnClickListener(null) - } else { - cardView.setOnClickListener { - if (kind.title.startsWith("ERROR:")) { - it.activity?.showDialogFragment(TextDialog("ERROR", kind.url)) - } else { - callBack.openExplore(sourceUrl, kind.title, kind.url) - } - } - } - } - } - } - - @Synchronized - private fun getFlexboxChild(flexbox: FlexboxLayout): MaterialCardView { - val cardView = if (recycler.isEmpty()) { - ItemFilletTextBinding.inflate(inflater, flexbox, false).root - } else { - recycler.removeLastElement() as MaterialCardView - } - val lp = FlexboxLayout.LayoutParams( - ViewGroup.LayoutParams.WRAP_CONTENT, - ViewGroup.LayoutParams.WRAP_CONTENT - ) - cardView.layoutParams = lp - cardView.forceLayout() - val tv = cardView.findViewById(R.id.text_view) - tv.text = "" - tv.requestLayout() - return cardView - } - - @Synchronized - private fun recyclerFlexbox(flexbox: FlexboxLayout) { - recycler.addAll(flexbox.children) - flexbox.removeAllViews() - } - - override fun registerListener(holder: ItemViewHolder, binding: ItemFindBookBinding) { - binding.apply { - llTitle.setOnClickListener { - toggleExpandState(holder) - } - ivStatus.setOnClickListener { - toggleExpandState(holder) - } - llTitle.onLongClick { - showMenu(ivStatus, holder.bindingAdapterPosition) - } - } - } - - private fun toggleExpandState(holder: ItemViewHolder) { - val item = getItem(holder.bindingAdapterPosition) ?: return - val oldId = expandedId - expandedId = if (expandedId == item.bookSourceUrl) null else item.bookSourceUrl - notifyItemChangedById(oldId) - notifyItemChangedById(expandedId) - - if (expandedId != null) { - scrollToId = expandedId - callBack.scrollTo(holder.bindingAdapterPosition) - } - } - - fun compressExplore(): Boolean { - return if (expandedId == null) { - false - } else { - val oldId = expandedId - expandedId = null - notifyItemChangedById(oldId) - true - } - } - private fun notifyItemChangedById(id: String?) { - id ?: return - for (pos in 0 until itemCount) { - val item = getItem(pos) ?: continue - if (item.bookSourceUrl == id) { - notifyItemChanged(pos) - break - } - } - } - private fun showMenu(view: View, position: Int) { - val source = getItem(position) ?: return - val popupMenu = PopupMenu(context, view) - popupMenu.inflate(R.menu.explore_item) - popupMenu.menu.findItem(R.id.menu_login).isVisible = source.hasLoginUrl - popupMenu.setOnMenuItemClickListener { - when (it.itemId) { - R.id.menu_edit -> callBack.editSource(source.bookSourceUrl) - R.id.menu_top -> callBack.toTop(source) - R.id.menu_search -> callBack.searchBook(source) - R.id.menu_login -> context.startActivity { - putExtra("type", "bookSource") - putExtra("key", source.bookSourceUrl) - } - R.id.menu_refresh -> Coroutine.async(callBack.scope) { - source.clearExploreKindsCache() - }.onSuccess { - notifyItemChanged(position) - } - R.id.menu_del -> callBack.deleteSource(source) - } - true - } - popupMenu.show() - } - - interface CallBack { - val scope: CoroutineScope - fun scrollTo(pos: Int) - fun openExplore(sourceUrl: String, title: String, exploreUrl: String?) - fun editSource(sourceUrl: String) - fun toTop(source: BookSourcePart) - fun deleteSource(source: BookSourcePart) - fun searchBook(bookSource: BookSourcePart) - } -} diff --git a/app/src/main/java/io/legado/app/ui/main/explore/ExploreDiffItemCallBack.kt b/app/src/main/java/io/legado/app/ui/main/explore/ExploreDiffItemCallBack.kt deleted file mode 100644 index 164da8bc0..000000000 --- a/app/src/main/java/io/legado/app/ui/main/explore/ExploreDiffItemCallBack.kt +++ /dev/null @@ -1,17 +0,0 @@ -package io.legado.app.ui.main.explore - -import androidx.recyclerview.widget.DiffUtil -import io.legado.app.data.entities.BookSourcePart - - -class ExploreDiffItemCallBack : DiffUtil.ItemCallback() { - - override fun areItemsTheSame(oldItem: BookSourcePart, newItem: BookSourcePart): Boolean { - return oldItem == newItem - } - - override fun areContentsTheSame(oldItem: BookSourcePart, newItem: BookSourcePart): Boolean { - return oldItem.bookSourceName == newItem.bookSourceName - } - -} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/main/explore/ExploreFragment.kt b/app/src/main/java/io/legado/app/ui/main/explore/ExploreFragment.kt deleted file mode 100644 index a02a9bc96..000000000 --- a/app/src/main/java/io/legado/app/ui/main/explore/ExploreFragment.kt +++ /dev/null @@ -1,48 +0,0 @@ -package io.legado.app.ui.main.explore - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.platform.ViewCompositionStrategy -import androidx.fragment.app.Fragment -import io.legado.app.ui.main.MainFragmentInterface -import io.legado.app.ui.theme.AppTheme - -/** - * 发现界面 - */ -class ExploreFragment() : Fragment(), MainFragmentInterface { - - constructor(position: Int) : this() { - arguments = Bundle().apply { - putInt("position", position) - } - } - - override val position: Int - get() = arguments?.getInt("position") ?: 0 - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - return ComposeView(requireContext()).apply { - setViewCompositionStrategy( - ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed - ) - setContent { - AppTheme { - ExploreScreen() - } - } - } - } - - fun compressExplore() { - // Compose version handled by ViewModel state - } - -} diff --git a/app/src/main/java/io/legado/app/ui/main/explore/ExploreScreen.kt b/app/src/main/java/io/legado/app/ui/main/explore/ExploreScreen.kt index 13fb62f72..c6c654770 100644 --- a/app/src/main/java/io/legado/app/ui/main/explore/ExploreScreen.kt +++ b/app/src/main/java/io/legado/app/ui/main/explore/ExploreScreen.kt @@ -397,56 +397,54 @@ fun ExploreSourceHeader( tint = MaterialTheme.colorScheme.onSurfaceVariant ) } + RoundDropdownMenu(expanded = showMenu, onDismissRequest = { showMenu = false }) { + PillHeaderDivider(title = item.bookSourceName) + RoundDropdownMenuItem( + leadingIcon = { MenuItemIcon(Icons.Default.VerticalAlignTop) }, + text = { Text(stringResource(R.string.to_top)) }, + onClick = { onTop(); showMenu = false } + ) + RoundDropdownMenuItem( + leadingIcon = { MenuItemIcon(Icons.Default.Edit) }, + text = { Text(stringResource(R.string.edit)) }, + onClick = { onEdit(); showMenu = false } + ) + RoundDropdownMenuItem( + leadingIcon = { MenuItemIcon(Icons.Default.Search) }, + text = { Text(stringResource(R.string.search)) }, + onClick = { onSearch(); showMenu = false } + ) + if (item.hasLoginUrl) { + RoundDropdownMenuItem( + leadingIcon = { MenuItemIcon(Icons.AutoMirrored.Filled.Login) }, + text = { Text(stringResource(R.string.login)) }, + onClick = { onLogin(); showMenu = false } + ) + } + RoundDropdownMenuItem( + leadingIcon = { MenuItemIcon(Icons.Default.Refresh) }, + text = { Text(stringResource(R.string.refresh)) }, + onClick = { onRefresh(); showMenu = false } + ) + RoundDropdownMenuItem( + leadingIcon = { + MenuItemIcon( + Icons.Default.Delete, + tint = MaterialTheme.colorScheme.error + ) + }, + text = { + Text( + stringResource(R.string.delete), + color = MaterialTheme.colorScheme.error + ) + }, + onClick = { onDelete(); showMenu = false } + ) + } } ) } - - RoundDropdownMenu(expanded = showMenu, onDismissRequest = { showMenu = false }) { - PillHeaderDivider(title = item.bookSourceName) - RoundDropdownMenuItem( - leadingIcon = { MenuItemIcon(Icons.Default.VerticalAlignTop) }, - text = { Text(stringResource(R.string.to_top)) }, - onClick = { onTop(); showMenu = false } - ) - RoundDropdownMenuItem( - leadingIcon = { MenuItemIcon(Icons.Default.Edit) }, - text = { Text(stringResource(R.string.edit)) }, - onClick = { onEdit(); showMenu = false } - ) - RoundDropdownMenuItem( - leadingIcon = { MenuItemIcon(Icons.Default.Search) }, - text = { Text(stringResource(R.string.search)) }, - onClick = { onSearch(); showMenu = false } - ) - if (item.hasLoginUrl) { - RoundDropdownMenuItem( - leadingIcon = { MenuItemIcon(Icons.AutoMirrored.Filled.Login) }, - text = { Text(stringResource(R.string.login)) }, - onClick = { onLogin(); showMenu = false } - ) - } - RoundDropdownMenuItem( - leadingIcon = { MenuItemIcon(Icons.Default.Refresh) }, - text = { Text(stringResource(R.string.refresh)) }, - onClick = { onRefresh(); showMenu = false } - ) - RoundDropdownMenuItem( - leadingIcon = { - MenuItemIcon( - Icons.Default.Delete, - tint = MaterialTheme.colorScheme.error - ) - }, - text = { - Text( - stringResource(R.string.delete), - color = MaterialTheme.colorScheme.error - ) - }, - onClick = { onDelete(); showMenu = false } - ) - } - } @Composable @@ -493,8 +491,7 @@ fun ExploreKindItem( OutlinedCard( shape = shape, colors = CardDefaults.outlinedCardColors( - containerColor = Color.Transparent, - contentColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f) + contentColor = MaterialTheme.colorScheme.primary ), modifier = modifier ) { diff --git a/app/src/main/java/io/legado/app/ui/main/explore/ExploreViewModel.kt b/app/src/main/java/io/legado/app/ui/main/explore/ExploreViewModel.kt index 35218b34c..ede0162ab 100644 --- a/app/src/main/java/io/legado/app/ui/main/explore/ExploreViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/main/explore/ExploreViewModel.kt @@ -9,7 +9,7 @@ import io.legado.app.data.entities.rule.ExploreKind import io.legado.app.help.source.SourceHelp import io.legado.app.help.source.clearExploreKindsCache import io.legado.app.help.source.exploreKinds -import io.legado.app.ui.widget.components.rules.ListUiState +import io.legado.app.ui.widget.components.list.ListUiState import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow diff --git a/app/src/main/java/io/legado/app/ui/theme/HazeStyle.kt b/app/src/main/java/io/legado/app/ui/theme/HazeStyle.kt index 2b9ad664d..be22e3380 100644 --- a/app/src/main/java/io/legado/app/ui/theme/HazeStyle.kt +++ b/app/src/main/java/io/legado/app/ui/theme/HazeStyle.kt @@ -47,4 +47,18 @@ fun Modifier.responsiveHazeEffect( ) } } -} \ No newline at end of file +} + +/** + * 仅判断 enableBlur 的简单 HazeEffect + */ +@OptIn(ExperimentalHazeMaterialsApi::class) +@Composable +fun Modifier.regularHazeEffect(state: HazeState): Modifier { + if (!ThemeConfig.enableBlur) return this + + return this.hazeEffect( + state = state, + style = HazeMaterials.ultraThin() + ) +} diff --git a/app/src/main/java/io/legado/app/ui/widget/components/GlassDefaults.kt b/app/src/main/java/io/legado/app/ui/widget/components/GlassDefaults.kt new file mode 100644 index 000000000..d83c13419 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/components/GlassDefaults.kt @@ -0,0 +1,27 @@ +package io.legado.app.ui.widget.components + +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import io.legado.app.ui.config.themeConfig.ThemeConfig + +object GlassDefaults { + + /** + * 统一的玻璃颜色处理方法 + * @param noBlurColor 未开启模糊时使用的颜色 + * @param blurAlpha 开启模糊时应用的透明度 + */ + @Composable + fun glassColor(noBlurColor: Color, blurAlpha: Float): Color { + return if (ThemeConfig.enableBlur) { + noBlurColor.copy(alpha = blurAlpha) + } else { + noBlurColor + } + } + + // 默认的玻璃效果透明度值,方便全局修改 + val DefaultBlurAlpha = 0.36f + val ThickBlurAlpha = 0.72f + val TransparentAlpha = 0f +} diff --git a/app/src/main/java/io/legado/app/ui/widget/components/cover/BookCover.kt b/app/src/main/java/io/legado/app/ui/widget/components/cover/BookCover.kt new file mode 100644 index 000000000..0be5391c7 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/components/cover/BookCover.kt @@ -0,0 +1,292 @@ +package io.legado.app.ui.widget.components.cover + +import android.graphics.Paint +import android.graphics.Typeface +import android.text.Layout +import android.text.StaticLayout +import android.text.TextPaint +import android.text.TextUtils +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +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.draw.shadow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.drawIntoCanvas +import androidx.compose.ui.graphics.nativeCanvas +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import androidx.core.graphics.withSave +import coil.compose.AsyncImage +import coil.request.ImageRequest +import io.legado.app.help.config.AppConfig +import io.legado.app.model.BookCover +import io.legado.app.ui.config.coverConfig.CoverConfig +import org.koin.compose.koinInject + +@Composable +fun BookCover( + name: String?, + author: String?, + path: String?, + modifier: Modifier = Modifier.width(64.dp), + badgeContent: (@Composable RowScope.() -> Unit)? = null, + sourceOrigin: String? = null, + onLoadFinish: (() -> Unit)? = null +) { + val context = LocalContext.current + val isNight = isSystemInDarkTheme() + + val useDefault = AppConfig.useDefaultCover + val finalPath = if (useDefault) null else path + + // 为每个 BookCover 组件实例基于其书名或路径生成一个随机封面 + // 这样书架上的每个默认封面都会不同,但对于同一本书又是固定的 + val randomDefault = remember(name, author, path) { + BookCover.getRandomDefaultDrawable(seed = name ?: author ?: path ?: "") + } + + Box( + modifier = modifier + .aspectRatio(5f / 7f) + .then( + if (CoverConfig.coverShowShadow) { + Modifier.shadow(4.dp, RoundedCornerShape(4.dp)) + } else Modifier + ) + .clip(RoundedCornerShape(4.dp)) + .background(MaterialTheme.colorScheme.surfaceContainerLow) + .then( + if (CoverConfig.coverShowStroke) { + Modifier.border( + 0.5.dp, + MaterialTheme.colorScheme.outlineVariant, + RoundedCornerShape(4.dp) + ) + } else Modifier + ) + ) { + // 1. 封面底图 + AsyncImage( + model = ImageRequest.Builder(context) + .data(finalPath ?: randomDefault) + .placeholder(randomDefault) + .error(randomDefault) + .crossfade(true) + .setParameter("sourceOrigin", sourceOrigin) + .setParameter("loadOnlyWifi", CoverConfig.loadCoverOnlyWifi) + .build(), + contentDescription = null, + imageLoader = koinInject(), + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize(), + onSuccess = { onLoadFinish?.invoke() }, + onError = { onLoadFinish?.invoke() } + ) + + // 2. 文字叠加层(始终尝试渲染,由 CoverConfig 内部控制具体显示逻辑) + CoverTextOverlay( + name = name, + author = author, + isNight = isNight + ) + + // 3. 角标 (Badge) + if (badgeContent != null) { + Surface( + modifier = Modifier + .align(Alignment.TopEnd) + .padding(2.dp), + shape = RoundedCornerShape(2.dp), + color = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.9f), + contentColor = MaterialTheme.colorScheme.onSecondaryContainer, + ) { + Row( + modifier = Modifier.padding(horizontal = 4.dp, vertical = 1.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + content = badgeContent + ) + } + } + } +} + +@Composable +private fun CoverTextOverlay( + name: String?, + author: String?, + isNight: Boolean +) { + val showName = if (isNight) CoverConfig.coverShowNameN else CoverConfig.coverShowName + val showAuthor = + (if (isNight) CoverConfig.coverShowAuthorN else CoverConfig.coverShowAuthor) && showName + + // 如果都不显示,直接返回 + if (!showName && !showAuthor) return + + val secondaryColor = MaterialTheme.colorScheme.secondary.toArgb() + val textColor = if (CoverConfig.coverDefaultColor) { + secondaryColor + } else { + if (isNight) CoverConfig.coverTextColorN else CoverConfig.coverTextColor + } + val shadowColor = if (isNight) CoverConfig.coverShadowColorN else CoverConfig.coverShadowColor + val isHorizontal = CoverConfig.coverInfoOrientation == "1" + + Canvas(modifier = Modifier.fillMaxSize()) { + val viewWidth = size.width + val viewHeight = size.height + + drawIntoCanvas { canvas -> + val nativeCanvas = canvas.nativeCanvas + + // 绘制书名 + if (showName && !name.isNullOrBlank()) { + val paint = Paint().apply { + isAntiAlias = true + textAlign = Paint.Align.CENTER + typeface = Typeface.DEFAULT_BOLD + textSize = viewWidth / 8f + color = textColor + if (CoverConfig.coverShowShadow) { + setShadowLayer(4f, 2f, 2f, shadowColor) + } + } + + if (isHorizontal) { + val maxWidth = (viewWidth * 0.8f).toInt() + val textPaint = TextPaint(paint).apply { + textAlign = Paint.Align.LEFT + } + + val layout = StaticLayout.Builder + .obtain(name, 0, name.length, textPaint, maxWidth) + .setAlignment(Layout.Alignment.ALIGN_CENTER) + .setMaxLines(3) + .setEllipsize(TextUtils.TruncateAt.END) + .build() + + nativeCanvas.withSave { + val textX = (viewWidth - maxWidth) / 2f + val textY = viewHeight * 0.08f + + translate(textX, textY) + + if (CoverConfig.coverShowStroke) { + textPaint.style = Paint.Style.STROKE + textPaint.strokeWidth = textPaint.textSize / 12 + val originalColor = textPaint.color + textPaint.color = Color.White.toArgb() + textPaint.clearShadowLayer() + layout.draw(this) + + // 还原 + textPaint.style = Paint.Style.FILL + textPaint.color = originalColor + if (CoverConfig.coverShowShadow) { + textPaint.setShadowLayer(4f, 2f, 2f, shadowColor) + } + } + layout.draw(this) + } + } else { + var startX = viewWidth * 0.16f + var startY = viewHeight * 0.16f + val fm = paint.fontMetrics + val charHeight = fm.bottom - fm.top + + name.forEach { char -> + if (CoverConfig.coverShowStroke) { + val strokePaint = Paint(paint).apply { + color = Color.White.toArgb() + style = Paint.Style.STROKE + strokeWidth = paint.textSize / 10 + clearShadowLayer() + } + nativeCanvas.drawText(char.toString(), startX, startY, strokePaint) + } + nativeCanvas.drawText(char.toString(), startX, startY, paint) + startY += charHeight + if (startY > viewHeight * 0.8f) { + startX += paint.textSize * 1.2f + startY = viewHeight * 0.2f + } + } + } + } + + // 绘制作者 + if (showAuthor && !author.isNullOrBlank()) { + val paint = Paint().apply { + isAntiAlias = true + textAlign = Paint.Align.CENTER + textSize = viewWidth / 12f + color = textColor + if (CoverConfig.coverShowShadow) { + setShadowLayer(4f, 1f, 1f, shadowColor) + } + } + + if (isHorizontal) { + val authorText = TextUtils.ellipsize( + author, + TextPaint(paint), + viewWidth * 0.9f, + TextUtils.TruncateAt.END + ) + + if (CoverConfig.coverShowStroke) { + val strokePaint = Paint(paint).apply { + color = Color.White.toArgb() + style = Paint.Style.STROKE + strokeWidth = paint.textSize / 10 + clearShadowLayer() + } + nativeCanvas.drawText( + authorText.toString(), + viewWidth / 2, + viewHeight * 0.75f, + strokePaint + ) + } + nativeCanvas.drawText( + authorText.toString(), + viewWidth / 2, + viewHeight * 0.75f, + paint + ) + } else { + val startX = viewWidth * 0.84f + val fm = paint.fontMetrics + val charHeight = fm.bottom - fm.top + var startY = viewHeight * 0.16f - (author.length * charHeight) + startY = startY.coerceAtLeast(viewHeight * 0.2f) + + author.forEach { char -> + nativeCanvas.drawText(char.toString(), startX, startY, paint) + startY += charHeight + } + } + } + } + } +} diff --git a/app/src/main/java/io/legado/app/ui/widget/components/cover/BookCoverWithProgress.kt b/app/src/main/java/io/legado/app/ui/widget/components/cover/BookCoverWithProgress.kt new file mode 100644 index 000000000..2a4c65a4b --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/components/cover/BookCoverWithProgress.kt @@ -0,0 +1,51 @@ +package io.legado.app.ui.widget.components.cover + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.unit.dp + +@Composable +fun BookCoverWithProgress( + name: String?, + author: String?, + path: String?, + modifier: Modifier = Modifier, + isUpdating: Boolean = false, + badgeContent: (@Composable RowScope.() -> Unit)? = null, + sourceOrigin: String? = null, + onLoadFinish: (() -> Unit)? = null +) { + Box(modifier = modifier) { + BookCover( + name = name, + author = author, + path = path, + modifier = Modifier.fillMaxWidth(), + badgeContent = badgeContent, + sourceOrigin = sourceOrigin, + onLoadFinish = onLoadFinish + ) + + if (isUpdating) { + LinearProgressIndicator( + modifier = Modifier + .align(Alignment.BottomCenter) + .fillMaxWidth() + .padding(horizontal = 4.dp, vertical = 6.dp) + .height(3.dp), + color = MaterialTheme.colorScheme.primary, + trackColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.2f), + strokeCap = StrokeCap.Round + ) + } + } +} diff --git a/app/src/main/java/io/legado/app/ui/widget/components/Cover.kt b/app/src/main/java/io/legado/app/ui/widget/components/cover/Cover.kt similarity index 98% rename from app/src/main/java/io/legado/app/ui/widget/components/Cover.kt rename to app/src/main/java/io/legado/app/ui/widget/components/cover/Cover.kt index ac512309a..7b2d9cf32 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/Cover.kt +++ b/app/src/main/java/io/legado/app/ui/widget/components/cover/Cover.kt @@ -1,4 +1,4 @@ -package io.legado.app.ui.widget.components +package io.legado.app.ui.widget.components.cover import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement diff --git a/app/src/main/java/io/legado/app/ui/widget/components/dialog/ColorPickerSheet.kt b/app/src/main/java/io/legado/app/ui/widget/components/dialog/ColorPickerSheet.kt new file mode 100644 index 000000000..54ec573f7 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/components/dialog/ColorPickerSheet.kt @@ -0,0 +1,198 @@ +package io.legado.app.ui.widget.components.dialog + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +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.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +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.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.github.skydoves.colorpicker.compose.AlphaSlider +import com.github.skydoves.colorpicker.compose.BrightnessSlider +import com.github.skydoves.colorpicker.compose.HsvColorPicker +import com.github.skydoves.colorpicker.compose.rememberColorPickerController +import io.legado.app.R +import io.legado.app.ui.widget.components.modalBottomSheet.GlassModalBottomSheet + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ColorPickerSheet( + initialColor: Int, + onDismissRequest: () -> Unit, + onColorSelected: (Int) -> Unit +) { + val controller = rememberColorPickerController() + var currentColor by remember { mutableStateOf(Color(initialColor)) } + + val presets = remember { + listOf( + Color(0xFFF44336), + Color(0xFFE91E63), + Color(0xFF9C27B0), + Color(0xFF673AB7), + Color(0xFF3F51B5), + Color(0xFF2196F3), + Color(0xFF03A9F4), + Color(0xFF00BCD4), + Color(0xFF009688), + Color(0xFF4CAF50), + Color(0xFF8BC34A), + Color(0xFFCDDC39), + Color(0xFFFFEB3B), + Color(0xFFFFC107), + Color(0xFFFF9800), + Color(0xFFFF5722), + Color(0xFF795548), + Color(0xFF9E9E9E), + Color(0xFF607D8B), + Color(0xFF000000), + Color(0xFFFFFFFF) + ) + } + + GlassModalBottomSheet(onDismissRequest = onDismissRequest) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "颜色选择", + style = MaterialTheme.typography.titleLarge, + modifier = Modifier.padding(bottom = 16.dp) + ) + + HsvColorPicker( + modifier = Modifier + .fillMaxWidth() + .height(250.dp) + .padding(10.dp), + controller = controller, + initialColor = Color(initialColor), + onColorChanged = { envelope -> + currentColor = envelope.color + } + ) + + Spacer(modifier = Modifier.height(16.dp)) + + AlphaSlider( + modifier = Modifier + .fillMaxWidth() + .padding(10.dp) + .height(35.dp), + controller = controller, + ) + + BrightnessSlider( + modifier = Modifier + .fillMaxWidth() + .padding(10.dp) + .height(35.dp), + controller = controller, + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 10.dp) + ) { + Box( + modifier = Modifier + .size(40.dp) + .clip(RoundedCornerShape(8.dp)) + .background(currentColor) + .border( + 1.dp, + MaterialTheme.colorScheme.outlineVariant, + RoundedCornerShape(8.dp) + ) + ) + Spacer(modifier = Modifier.width(12.dp)) + Text( + text = "#${Integer.toHexString(currentColor.toArgb()).uppercase()}", + style = MaterialTheme.typography.bodyLarge + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + LazyVerticalGrid( + columns = GridCells.Adaptive(36.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier + .heightIn(max = 80.dp) + .padding(horizontal = 10.dp) + ) { + items(presets) { color -> + Box( + modifier = Modifier + .size(36.dp) + .clip(CircleShape) + .background(color) + .border( + if (color.toArgb() == currentColor.toArgb()) 2.dp else 0.dp, + MaterialTheme.colorScheme.primary, + CircleShape + ) + .clickable { + controller.selectByColor(color, true) + } + ) + } + } + + Spacer(modifier = Modifier.height(24.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End + ) { + TextButton(onClick = onDismissRequest) { + Text(stringResource(R.string.cancel)) + } + Spacer(modifier = Modifier.width(8.dp)) + Button(onClick = { + onColorSelected(currentColor.toArgb()) + onDismissRequest() + }) { + Text(stringResource(R.string.ok)) + } + } + } + } +} diff --git a/app/src/main/java/io/legado/app/ui/widget/components/exportComponents/FilePickerSheet.kt b/app/src/main/java/io/legado/app/ui/widget/components/filePicker/FilePickerSheet.kt similarity index 92% rename from app/src/main/java/io/legado/app/ui/widget/components/exportComponents/FilePickerSheet.kt rename to app/src/main/java/io/legado/app/ui/widget/components/filePicker/FilePickerSheet.kt index 0c0f2c19e..912d9961a 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/exportComponents/FilePickerSheet.kt +++ b/app/src/main/java/io/legado/app/ui/widget/components/filePicker/FilePickerSheet.kt @@ -1,4 +1,4 @@ -package io.legado.app.ui.widget.components.exportComponents +package io.legado.app.ui.widget.components.filePicker import android.webkit.MimeTypeMap import androidx.compose.foundation.layout.Arrangement @@ -41,6 +41,7 @@ fun FilePickerSheet( title: String = stringResource(R.string.select_operation), onSelectSysDir: (() -> Unit)? = null, onSelectSysFile: ((Array) -> Unit)? = null, + onSelectSysFiles: ((Array) -> Unit)? = null, onManualInput: (() -> Unit)? = null, onUpload: (() -> Unit)? = null, allowExtensions: Array? = null, @@ -84,6 +85,14 @@ fun FilePickerSheet( ) } + onSelectSysFiles?.let { + FilePickerOptionCard( + icon = Icons.AutoMirrored.Filled.InsertDriveFile, + text = "系统多选", + onClick = { it(typesOfExtensions(allowExtensions)) } + ) + } + onManualInput?.let { FilePickerOptionCard( icon = Icons.Default.EditNote, @@ -160,4 +169,4 @@ private fun typesOfExtensions(allowExtensions: Array?): Array { } } return types.toTypedArray() -} \ No newline at end of file +} diff --git a/app/src/main/java/io/legado/app/ui/widget/components/list/ListScaffold.kt b/app/src/main/java/io/legado/app/ui/widget/components/list/ListScaffold.kt index 442437053..60b1e4ce7 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/list/ListScaffold.kt +++ b/app/src/main/java/io/legado/app/ui/widget/components/list/ListScaffold.kt @@ -42,7 +42,6 @@ import io.legado.app.ui.theme.responsiveHazeEffect import io.legado.app.ui.theme.responsiveHazeSource import io.legado.app.ui.widget.components.SelectionActions import io.legado.app.ui.widget.components.SelectionBottomBar -import io.legado.app.ui.widget.components.rules.ListUiState import io.legado.app.ui.widget.components.topbar.DynamicTopAppBar @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) diff --git a/app/src/main/java/io/legado/app/ui/widget/components/rules/ListUiState.kt b/app/src/main/java/io/legado/app/ui/widget/components/list/ListUiState.kt similarity index 87% rename from app/src/main/java/io/legado/app/ui/widget/components/rules/ListUiState.kt rename to app/src/main/java/io/legado/app/ui/widget/components/list/ListUiState.kt index 1d70c1f99..959e28040 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/rules/ListUiState.kt +++ b/app/src/main/java/io/legado/app/ui/widget/components/list/ListUiState.kt @@ -1,4 +1,4 @@ -package io.legado.app.ui.widget.components.rules +package io.legado.app.ui.widget.components.list interface ListUiState { val items: List diff --git a/app/src/main/java/io/legado/app/ui/widget/components/settingItem/ClickableSettingItem.kt b/app/src/main/java/io/legado/app/ui/widget/components/settingItem/ClickableSettingItem.kt index ae8c7f4b0..cb77972a6 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/settingItem/ClickableSettingItem.kt +++ b/app/src/main/java/io/legado/app/ui/widget/components/settingItem/ClickableSettingItem.kt @@ -15,6 +15,8 @@ fun ClickableSettingItem( description: String? = null, option: String? = null, imageVector: ImageVector? = null, + onLongClick: (() -> Unit)? = null, + trailingContent: (@Composable () -> Unit)? = null, onClick: () -> Unit ) { SettingItem( @@ -23,13 +25,14 @@ fun ClickableSettingItem( description = description, option = option, imageVector = imageVector, - trailingContent = { + trailingContent = trailingContent ?: { Icon( imageVector = Icons.Default.ChevronRight, contentDescription = null, tint = MaterialTheme.colorScheme.onSurfaceVariant ) }, - onClick = onClick + onClick = onClick, + onLongClick = onLongClick ) -} \ No newline at end of file +} diff --git a/app/src/main/java/io/legado/app/ui/widget/components/settingItem/CompactSettingItems.kt b/app/src/main/java/io/legado/app/ui/widget/components/settingItem/CompactSettingItems.kt new file mode 100644 index 000000000..e4f75a8dc --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/components/settingItem/CompactSettingItems.kt @@ -0,0 +1,153 @@ +package io.legado.app.ui.widget.components.settingItem + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.Remove +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Slider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +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.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.unit.dp +import io.legado.app.ui.widget.components.button.SmallOutlinedIconButton +import io.legado.app.ui.widget.components.card.TextCard +import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem + +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +fun CompactDropdownSettingItem( + title: String, + selectedValue: String, + displayEntries: Array, + entryValues: Array, + description: String? = null, + imageVector: ImageVector? = null, + color: Color? = MaterialTheme.colorScheme.surface, + shape: Shape = MaterialTheme.shapes.small, + onValueChange: (String) -> Unit +) { + val currentEntry = displayEntries.getOrNull(entryValues.indexOf(selectedValue)) ?: selectedValue + + SettingItem( + title = title, + description = description, + imageVector = imageVector, + color = color, + shape = shape, + trailingContent = { + TextCard( + cornerRadius = 8.dp, + horizontalPadding = 8.dp, + verticalPadding = 4.dp, + text = currentEntry, + backgroundColor = MaterialTheme.colorScheme.surfaceContainer, + contentColor = MaterialTheme.colorScheme.onSurface + ) + }, + dropdownMenu = { onDismiss -> + displayEntries.forEachIndexed { index, display -> + RoundDropdownMenuItem( + text = { Text(display) }, + onClick = { + onValueChange(entryValues[index]) + onDismiss() + }, + trailingIcon = if (selectedValue == entryValues[index]) { + { + Icon( + Icons.Default.Check, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + } + } else null + ) + } + } + ) +} + +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +fun CompactSliderSettingItem( + title: String, + value: Float, + valueRange: ClosedFloatingPointRange, + steps: Int = 0, + description: String? = null, + imageVector: ImageVector? = null, + color: Color? = MaterialTheme.colorScheme.surface, + shape: Shape = MaterialTheme.shapes.small, + onValueChange: (Float) -> Unit +) { + var expanded by remember { mutableStateOf(false) } + + var sliderValue by remember(value) { mutableFloatStateOf(value) } + + SettingItem( + title = title, + description = description, + imageVector = imageVector, + color = color, + shape = shape, + expanded = expanded, + onExpandChange = { expanded = it }, + trailingContent = { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + SmallOutlinedIconButton( + onClick = { + val newValue = (value.toInt() - 1).toFloat().coerceIn(valueRange) + onValueChange(newValue) + }, + icon = Icons.Default.Remove + ) + TextCard( + cornerRadius = 8.dp, + horizontalPadding = 8.dp, + verticalPadding = 4.dp, + text = value.toInt().toString(), + backgroundColor = MaterialTheme.colorScheme.surfaceContainer, + contentColor = MaterialTheme.colorScheme.onSurface + ) + SmallOutlinedIconButton( + onClick = { + val newValue = (value.toInt() + 1).toFloat().coerceIn(valueRange) + onValueChange(newValue) + }, + icon = Icons.Default.Add + ) + } + }, + expandContent = { + Slider( + value = sliderValue, + onValueChange = { + sliderValue = it + onValueChange(it) + }, + valueRange = valueRange, + steps = steps, + modifier = Modifier.fillMaxWidth() + ) + } + ) +} diff --git a/app/src/main/res/values-zh-rCN/arrays.xml b/app/src/main/res/values-zh-rCN/arrays.xml index 56e98b34b..ea66ae3e0 100644 --- a/app/src/main/res/values-zh-rCN/arrays.xml +++ b/app/src/main/res/values-zh-rCN/arrays.xml @@ -9,9 +9,8 @@ 标签 - 文件夹 隐藏标签 - 材料标签 + 文件夹 @@ -132,6 +131,14 @@ @string/bookshelf_px_5 + + @string/list + @string/compact_list + @string/grid + @string/compact_grid + @string/cover_grid + + 动态取色 草野 diff --git a/app/src/main/res/values-zh-rHK/arrays.xml b/app/src/main/res/values-zh-rHK/arrays.xml index d237b5748..52c99d234 100644 --- a/app/src/main/res/values-zh-rHK/arrays.xml +++ b/app/src/main/res/values-zh-rHK/arrays.xml @@ -10,9 +10,8 @@ 標籤 - 文件夾 隐藏標籤 - 材料標籤 + 文件夾 diff --git a/app/src/main/res/values-zh-rTW/arrays.xml b/app/src/main/res/values-zh-rTW/arrays.xml index 569ffb6ac..b5dfccd8d 100644 --- a/app/src/main/res/values-zh-rTW/arrays.xml +++ b/app/src/main/res/values-zh-rTW/arrays.xml @@ -9,9 +9,8 @@ 標籤 - 資料夾 隐藏標籤 - 材料標籤 + 資料夾 diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 0d7ad3f28..dca8e6961 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -14,9 +14,8 @@ Tab - Folder Hide Tab - Material Tab + Folder @@ -279,6 +278,14 @@ @string/bookshelf_px_5 + + @string/list + @string/compact_list + @string/grid + @string/compact_grid + @string/cover_grid + + Auto Selected diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6ba5b5cb5..4f18269db 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -20,6 +20,7 @@ ksp = "2.3.4" agp = "8.13.2" appcompat = "1.7.1" colorpicker = "1.1.0" +colorpicker-compose = "1.1.3" commonsText = "1.15.0" constraintlayout = "2.2.1" core = "1.17.0" @@ -209,6 +210,7 @@ androidx-recyclerview = { module = "androidx.recyclerview:recyclerview", version androidx-viewpager2 = { module = "androidx.viewpager2:viewpager2", version.ref = "viewpager2" } androidx-webkit = { module = "androidx.webkit:webkit", version.ref = "webkit" } colorpicker = { module = "com.jaredrummler:colorpicker", version.ref = "colorpicker" } +colorpicker-compose = { module = "com.github.skydoves:colorpicker-compose", version.ref = "colorpicker-compose" } commons-text = { module = "org.apache.commons:commons-text", version.ref = "commonsText" } desugar = { group = "com.android.tools", name = "desugar_jdk_libs_nio", version.ref = "desugar" }