Sfoglia il codice sorgente

[新增] 重写了主页面和书架界面、封面设置界面,现在支持底栏模糊,更多的封面样式、封面阴影、随机默认封面等功能(在高级 - 测试 - 封面设置中应用)。

HapeLee 7 mesi fa
parent
commit
6922fdeed9
68 ha cambiato i file con 4059 aggiunte e 4653 eliminazioni
  1. 2 0
      app/build.gradle.kts
  2. 4 1
      app/src/main/java/io/legado/app/constant/EventBus.kt
  3. 6 0
      app/src/main/java/io/legado/app/constant/PreferKey.kt
  4. 7 2
      app/src/main/java/io/legado/app/data/entities/Book.kt
  5. 8 0
      app/src/main/java/io/legado/app/di/appModule.kt
  6. 3 0
      app/src/main/java/io/legado/app/help/book/BookExtensions.kt
  7. 27 8
      app/src/main/java/io/legado/app/model/BookCover.kt
  8. 9 5
      app/src/main/java/io/legado/app/model/webBook/WebBook.kt
  9. 5 4
      app/src/main/java/io/legado/app/ui/book/explore/ExploreShowScreen.kt
  10. 249 0
      app/src/main/java/io/legado/app/ui/book/group/GroupEditSheet.kt
  11. 16 2
      app/src/main/java/io/legado/app/ui/config/TestConfigActivity.kt
  12. 120 0
      app/src/main/java/io/legado/app/ui/config/bookshelfConfig/BookshelfConfig.kt
  13. 87 0
      app/src/main/java/io/legado/app/ui/config/coverConfig/CoverConfig.kt
  14. 285 0
      app/src/main/java/io/legado/app/ui/config/coverConfig/CoverConfigScreen.kt
  15. 139 0
      app/src/main/java/io/legado/app/ui/config/coverConfig/CoverConfigViewModel.kt
  16. 154 0
      app/src/main/java/io/legado/app/ui/config/coverConfig/CoverManageSheet.kt
  17. 126 0
      app/src/main/java/io/legado/app/ui/config/coverConfig/CoverRuleConfigSheet.kt
  18. 19 0
      app/src/main/java/io/legado/app/ui/config/mainConfig/MainConfig.kt
  19. 21 419
      app/src/main/java/io/legado/app/ui/main/MainActivity.kt
  20. 53 0
      app/src/main/java/io/legado/app/ui/main/MainDestination.kt
  21. 207 0
      app/src/main/java/io/legado/app/ui/main/MainScreen.kt
  22. 56 230
      app/src/main/java/io/legado/app/ui/main/MainViewModel.kt
  23. 0 179
      app/src/main/java/io/legado/app/ui/main/bookshelf/BaseBookshelfFragment.kt
  24. 325 0
      app/src/main/java/io/legado/app/ui/main/bookshelf/BookItem.kt
  25. 0 228
      app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfConfigBottomSheet.kt
  26. 228 0
      app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfConfigSheet.kt
  27. 534 0
      app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt
  28. 26 0
      app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfUiState.kt
  29. 380 4
      app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt
  30. 139 0
      app/src/main/java/io/legado/app/ui/main/bookshelf/GroupManageSheet.kt
  31. 0 375
      app/src/main/java/io/legado/app/ui/main/bookshelf/books/BooksFragment.kt
  32. 0 229
      app/src/main/java/io/legado/app/ui/main/bookshelf/books/BookshelfFragment1.kt
  33. 0 467
      app/src/main/java/io/legado/app/ui/main/bookshelf/books/BookshelfFragment2.kt
  34. 0 147
      app/src/main/java/io/legado/app/ui/main/bookshelf/books/BookshelfFragment3.kt
  35. 0 164
      app/src/main/java/io/legado/app/ui/main/bookshelf/books/BookshelfFragment4.kt
  36. 0 102
      app/src/main/java/io/legado/app/ui/main/bookshelf/books/styleDefalut/BaseBooksAdapter.kt
  37. 0 93
      app/src/main/java/io/legado/app/ui/main/bookshelf/books/styleDefalut/BooksAdapterGrid.kt
  38. 0 103
      app/src/main/java/io/legado/app/ui/main/bookshelf/books/styleDefalut/BooksAdapterGridCompact.kt
  39. 0 103
      app/src/main/java/io/legado/app/ui/main/bookshelf/books/styleDefalut/BooksAdapterGridCover.kt
  40. 0 129
      app/src/main/java/io/legado/app/ui/main/bookshelf/books/styleDefalut/BooksAdapterList.kt
  41. 0 133
      app/src/main/java/io/legado/app/ui/main/bookshelf/books/styleDefalut/BooksAdapterListCompact.kt
  42. 0 146
      app/src/main/java/io/legado/app/ui/main/bookshelf/books/styleFold/BaseBooksAdapter.kt
  43. 0 195
      app/src/main/java/io/legado/app/ui/main/bookshelf/books/styleFold/BooksAdapterGrid.kt
  44. 0 202
      app/src/main/java/io/legado/app/ui/main/bookshelf/books/styleFold/BooksAdapterGridCompact.kt
  45. 0 202
      app/src/main/java/io/legado/app/ui/main/bookshelf/books/styleFold/BooksAdapterGridCover.kt
  46. 0 215
      app/src/main/java/io/legado/app/ui/main/bookshelf/books/styleFold/BooksAdapterList.kt
  47. 0 209
      app/src/main/java/io/legado/app/ui/main/bookshelf/books/styleFold/BooksAdapterListCompact.kt
  48. 0 225
      app/src/main/java/io/legado/app/ui/main/explore/ExploreAdapter.kt
  49. 0 17
      app/src/main/java/io/legado/app/ui/main/explore/ExploreDiffItemCallBack.kt
  50. 0 48
      app/src/main/java/io/legado/app/ui/main/explore/ExploreFragment.kt
  51. 46 49
      app/src/main/java/io/legado/app/ui/main/explore/ExploreScreen.kt
  52. 1 1
      app/src/main/java/io/legado/app/ui/main/explore/ExploreViewModel.kt
  53. 15 1
      app/src/main/java/io/legado/app/ui/theme/HazeStyle.kt
  54. 27 0
      app/src/main/java/io/legado/app/ui/widget/components/GlassDefaults.kt
  55. 292 0
      app/src/main/java/io/legado/app/ui/widget/components/cover/BookCover.kt
  56. 51 0
      app/src/main/java/io/legado/app/ui/widget/components/cover/BookCoverWithProgress.kt
  57. 1 1
      app/src/main/java/io/legado/app/ui/widget/components/cover/Cover.kt
  58. 198 0
      app/src/main/java/io/legado/app/ui/widget/components/dialog/ColorPickerSheet.kt
  59. 11 2
      app/src/main/java/io/legado/app/ui/widget/components/filePicker/FilePickerSheet.kt
  60. 0 1
      app/src/main/java/io/legado/app/ui/widget/components/list/ListScaffold.kt
  61. 1 1
      app/src/main/java/io/legado/app/ui/widget/components/list/ListUiState.kt
  62. 6 3
      app/src/main/java/io/legado/app/ui/widget/components/settingItem/ClickableSettingItem.kt
  63. 153 0
      app/src/main/java/io/legado/app/ui/widget/components/settingItem/CompactSettingItems.kt
  64. 9 2
      app/src/main/res/values-zh-rCN/arrays.xml
  65. 1 2
      app/src/main/res/values-zh-rHK/arrays.xml
  66. 1 2
      app/src/main/res/values-zh-rTW/arrays.xml
  67. 9 2
      app/src/main/res/values/arrays.xml
  68. 2 0
      gradle/libs.versions.toml

+ 2 - 0
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)

+ 4 - 1
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"
-}
+
+    const val UP_BOOKSHELF_COUNT = "upBookshelfCount"
+    const val UP_ALL_BOOK_TOC = "upAllBookToc"
+}

+ 6 - 0
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"
 

+ 7 - 2
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<ReadConfig>(json).getOrNull()
     }
-}
+}

+ 8 - 0
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)

+ 3 - 0
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 {

+ 27 - 8
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<Drawable> {
+        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<Drawable> {
-        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 {
         }
     }
 
-}
+}

+ 9 - 5
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<List<BookChapter>> {
-        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 {
         }
     }
 
-}
+}

+ 5 - 4
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(

+ 249 - 0
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))
+                }
+            }
+        }
+    }
+}

+ 16 - 2
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<ReadConfigRoute> {
                 ReadConfigScreen(onBackClick = { navController.popBackStack() })
             }
+
+            composable<CoverConfigRoute> {
+                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
+                    )
                 }
             }
         }

+ 120 - 0
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)
+
+}

+ 87 - 0
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
+    )
+}

+ 285 - 0
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<String?>(null) }
+    var showColorPickerByField by remember { mutableStateOf<String?>(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()
+            }
+        )
+    }
+}

+ 139 - 0
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<Uri>) {
+        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()
+    }
+}

+ 154 - 0
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")
+        )
+    }
+}

+ 126 - 0
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))
+                }
+            }
+        }
+    }
+}

+ 19 - 0
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)
+}

+ 21 - 419
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<ActivityMainBinding, MainViewModel>(),
-    BottomNavigationView.OnNavigationItemSelectedListener,
-    BottomNavigationView.OnNavigationItemReselectedListener {
+open class MainActivity : BaseComposeActivity() {
 
-    override val binding by viewBinding(ActivityMainBinding::inflate)
-    override val viewModel by viewModels<MainViewModel>()
-    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<Int, Fragment>()
-    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<MainViewModel>()
 
     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<ActivityMainBinding, MainViewModel>(),
             //自动更新书籍
             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
-    }
-
-    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()
-                    }
-                }
-            }
+    @Composable
+    override fun Content() {
+        val orientation = resources.configuration.orientation
+        val smallestWidthDp = resources.configuration.smallestScreenWidthDp
+        val tabletInterface = AppConfig.tabletInterface
 
-            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<ActivityMainBinding, MainViewModel>(),
         }
     }
 
-    private fun initView() = binding.run {
-        viewPagerMain.offscreenPageLimit = 3
-        viewPagerMain.adapter = adapter
-        viewPagerMain.registerOnPageChangeCallback(PageChangeCallback())
-    }
-
     /**
      * 版本更新日志
      */
@@ -275,10 +144,6 @@ open class MainActivity : VMBaseActivity<ActivityMainBinding, MainViewModel>(),
         }
     }
 
-    /**
-     * 设置本地密码
-     */
-
     private fun notifyAppCrash() {
         if (!LocalConfig.appCrash || BuildConfig.DEBUG) {
             return
@@ -316,7 +181,6 @@ open class MainActivity : VMBaseActivity<ActivityMainBinding, MainViewModel>(),
 
     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<ActivityMainBinding, MainViewModel>(),
         }
     }
 
-    /**
-     * 如果重启太快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<Boolean>(EventBus.NOTIFY_MAIN) {
-            binding.apply {
-                upBottomMenu()
-                updateBookshelfIcon(true)
-                if (it) {
-                    viewPagerMain.setCurrentItem(bottomMenuCount - 1, false)
-                }
-            }
-        }
-        observeEvent<String>(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<ExtendedFloatingActionButton>(R.id.nav_fab)
-        val button =
-            binding.navigationRailView.headerView!!.findViewById<ImageView>(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<SearchActivity>()
-        }
-
-        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()

+ 53 - 0
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)
+    }
+}

+ 207 - 0
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<SearchActivity>() },
+                        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<ReadBookActivity> {
+                                    putExtra("bookUrl", book.bookUrl)
+                                }
+                            },
+                            onBookLongClick = { book ->
+                                context.startActivity<BookInfoActivity> {
+                                    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)
+    }
+}

+ 56 - 230
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
 
-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<String>()
-    private val onUpTocBooks = ConcurrentHashMap.newKeySet<String>()
-    val onUpBooksLiveData = MutableLiveData<Int>()
-    private var upTocJob: Job? = null
-    private var cacheBookJob: Job? = null
-    private val eventListenerSource = ConcurrentHashMap<BookSource, Boolean>()
-    val booksListRecycledViewPool = RecycledViewPool().apply {
-        setMaxRecycledViews(0, 30)
-    }
-    val booksGridRecycledViewPool = RecycledViewPool().apply {
-        setMaxRecycledViews(0, 100)
-    }
-
-    init {
-        deleteNotShelfBook()
-    }
-
-    override fun onCleared() {
-        super.onCleared()
-        upTocPool.close()
-    }
+data class MainUiState(
+    val upBooksCount: Int = 0
+)
 
-    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<Book>) {
-        execute(context = upTocPool) {
-            books.filter {
-                !it.isLocal && it.canUpdate
-            }.let {
-                addToWaitUp(it)
-            }
-        }
-    }
+class MainViewModel(application: Application) : BaseViewModel(application) {
 
-    @Synchronized
-    private fun addToWaitUp(books: List<Book>) {
-        books.forEach { book ->
-            if (!waitUpTocBooks.contains(book.bookUrl) && !onUpTocBooks.contains(book.bookUrl)) {
-                waitUpTocBooks.add(book.bookUrl)
-            }
-        }
-        if (upTocJob == null) {
-            startUpTocJob()
-        }
-    }
+    private val upBooksCountFlow = MutableStateFlow(0)
 
-    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()
-        }
-    }
+    val state: StateFlow<MainUiState> = upBooksCountFlow
+        .map { count -> MainUiState(count) }
+        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), MainUiState())
 
-    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)
+    init {
+        viewModelScope.launch {
+            FlowEventBus.with<Int>(EventBus.UP_BOOKSHELF_COUNT).collect {
+                upBooksCountFlow.value = it
             }
         }
+        deleteNotShelfBook()
     }
 
-    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)
-        }
+    fun upAllBookToc() {
+        FlowEventBus.post(EventBus.UP_ALL_BOOK_TOC, Unit)
     }
 
     fun postLoad() {
@@ -270,4 +66,34 @@ class MainViewModel(application: Application) : BaseViewModel(application) {
         }
     }
 
-}
+    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
+        }
+    }
+
+}

+ 0 - 179
app/src/main/java/io/legado/app/ui/main/bookshelf/BaseBookshelfFragment.kt

@@ -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<BookshelfViewModel>(layoutId),
-    MainFragmentInterface {
-
-    override val position: Int? get() = arguments?.getInt("position")
-
-    val activityViewModel by activityViewModels<MainViewModel>()
-    override val viewModel by viewModels<BookshelfViewModel>()
-
-    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<Book>
-    private var groupsLiveData: LiveData<List<BookGroup>>? = 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<RemoteBookActivity>()
-            R.id.menu_search -> startActivity<SearchActivity>()
-            R.id.menu_update_toc -> activityViewModel.upToc(books)
-            R.id.menu_bookshelf_layout -> showDialogFragment<BookshelfConfigBottomSheet>()
-            R.id.menu_group_manage -> showDialogFragment<GroupManageDialog>()
-            R.id.menu_add_local -> startActivity<ImportBookActivity>()
-            R.id.menu_add_url -> showAddBookByUrlAlert()
-            R.id.menu_bookshelf_manage -> startActivity<BookshelfManageActivity> {
-                putExtra("groupId", groupId)
-            }
-
-            R.id.menu_download -> startActivity<CacheActivity> {
-                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<AppLogDialog>()
-        }
-    }
-
-    protected fun initBookGroupData() {
-        groupsLiveData?.removeObservers(viewLifecycleOwner)
-        groupsLiveData = appDb.bookGroupDao.show.apply {
-            observe(viewLifecycleOwner) {
-                upGroup(it)
-            }
-        }
-    }
-
-    abstract fun upGroup(data: List<BookGroup>)
-
-    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")
-                }
-            }
-        }
-    }
-
-}

+ 325 - 0
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<Book>,
+    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<Book>,
+    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<Book>,
+    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
+    )
+}

+ 0 - 228
app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfConfigBottomSheet.kt

@@ -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<MainViewModel>()
-
-    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<Chip>(binding.chipGroupStyle.checkedChipId)
-        val isGroupStyle0 = binding.chipGroupStyle.indexOfChild(checkedStyleChip) == 0
-        binding.swShowBookshelfTabMenu.isEnabled = isGroupStyle0
-    }
-}

+ 228 - 0
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() }
+            )
+        }
+    }
+}

+ 534 - 0
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<SearchActivity>() },
+        onSearchQueryChange = { viewModel.setSearchKey(it) },
+        topBarActions = { },
+        dropDownMenuContent = { dismiss ->
+            RoundDropdownMenuItem(
+                text = { Text(stringResource(R.string.add_remote_book)) },
+                onClick = { context.startActivity<RemoteBookActivity>(); dismiss() },
+                leadingIcon = { Icon(Icons.Default.Wifi, null) }
+            )
+            RoundDropdownMenuItem(
+                text = { Text(stringResource(R.string.book_local)) },
+                onClick = { context.startActivity<ImportBookActivity>(); 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<BookshelfManageActivity> {
+                        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<CacheActivity> {
+                        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
+    )
+}

+ 26 - 0
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<Book> = emptyList(),
+    override val selectedIds: Set<Any> = emptySet(),
+    override val searchKey: String = "",
+    override val isSearch: Boolean = false,
+    override val isLoading: Boolean = false,
+    val groups: List<BookGroup> = emptyList(),
+    val groupPreviews: Map<Long, List<Book>> = 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<String> = emptySet()
+) : ListUiState<Book>

+ 380 - 4
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<String?>(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<String>()
+    private val onUpTocBooks = ConcurrentHashMap.newKeySet<String>()
+    private val updatingBooksFlow = MutableStateFlow<Set<String>>(emptySet())
+    private var upTocJob: Job? = null
+    private var cacheBookJob: Job? = null
+    private val eventListenerSource = ConcurrentHashMap<BookSource, Boolean>()
+
+    val scrollTrigger = MutableSharedFlow<Unit>(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<String>
+    )
+
+    val uiState: StateFlow<BookshelfUiState> = 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<Long, List<Book>>()
+        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<Unit>(EventBus.UP_ALL_BOOK_TOC).collect {
+                upAllBookToc()
+            }
+        }
+        viewModelScope.launch {
+            FlowEventBus.with<Unit>(EventBus.BOOKSHELF_REFRESH).collect {
+                refresh()
+            }
+        }
+
+        if (BookshelfConfig.autoRefreshBook) {
+            upAllBookToc()
+        }
+    }
+
+    override fun onCleared() {
+        super.onCleared()
+        upTocPool.close()
+    }
+
+    private fun sortBooks(list: List<Book>, groupId: Long): List<Book> {
+        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<Book>) {
+        execute(context = upTocPool) {
+            books.filter {
+                !it.isLocal && it.canUpdate
+            }.let {
+                addToWaitUp(it)
+            }
+        }
+    }
+
+    @Synchronized
+    private fun addToWaitUp(books: List<Book>) {
+        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<BookSourcePart> 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)
         }
     }

+ 139 - 0
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<BookGroup?>(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
+                )
+            }
+        }
+    )
+}

+ 0 - 375
app/src/main/java/io/legado/app/ui/main/bookshelf/books/BooksFragment.kt

@@ -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<MainViewModel>()
-
-    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<Book> {
-        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<BookInfoActivity> {
-                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<String>(EventBus.UP_BOOKSHELF) {
-            booksAdapter.notification(it)
-        }
-        observeEvent<String>(EventBus.BOOKSHELF_REFRESH) {
-            booksAdapter.notifyDataSetChanged()
-            startLastUpdateTimeJob()
-            upFastScrollerBar()
-        }
-    }
-}

+ 0 - 229
app/src/main/java/io/legado/app/ui/main/bookshelf/books/BookshelfFragment1.kt

@@ -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<BookGroup>()
-    private val fragmentMap = hashMapOf<Long, BooksFragment>()
-    override val groupId: Long get() = selectedGroup?.groupId ?: 0
-
-    private lateinit var adapter: TabFragmentPageAdapter
-    private var tabFadeListener: AppBarLayout.OnOffsetChangedListener? = null
-
-    override val books: List<Book>
-        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<BookGroup>) {
-        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
-        }
-    }
-}

+ 0 - 467
app/src/main/java/io/legado/app/ui/main/bookshelf/books/BookshelfFragment2.kt

@@ -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<BookGroup> = emptyList()
-    private var booksFlowJob: Job? = null
-    override var groupId = BookGroup.Companion.IdRoot
-    private var allBooks: List<Book> = emptyList()
-    override var books: List<Book> = 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<Book> {
-        return getItems() as List<Book>
-    }
-
-    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<BookGroup>) {
-        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<BookInfoActivity> {
-                    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<Any> {
-        return if (groupId == BookGroup.Companion.IdRoot) {
-            bookGroups + books
-        } else {
-            books
-        }
-    }
-
-
-    @SuppressLint("NotifyDataSetChanged")
-    override fun observeLiveBus() {
-        super.observeLiveBus()
-        observeEvent<String>(EventBus.UP_BOOKSHELF) {
-            booksAdapter.notification(it)
-        }
-        observeEvent<String>(EventBus.BOOKSHELF_REFRESH) {
-            booksAdapter.notifyDataSetChanged()
-        }
-    }
-}

+ 0 - 147
app/src/main/java/io/legado/app/ui/main/bookshelf/books/BookshelfFragment3.kt

@@ -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<BookGroup>()
-    private val fragmentMap = hashMapOf<Long, BooksFragment>()
-    override val groupId: Long get() = selectedGroup?.groupId ?: 0
-
-    private lateinit var adapter: TabFragmentPageAdapter
-
-    override val books: List<Book>
-        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<BookGroup>) {
-        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
-        }
-    }
-}

+ 0 - 164
app/src/main/java/io/legado/app/ui/main/bookshelf/books/BookshelfFragment4.kt

@@ -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<BookGroup>()
-    private val fragmentMap = hashMapOf<Long, BooksFragment>()
-    override val groupId: Long get() = selectedGroup?.groupId ?: 0
-
-    private lateinit var adapter: TabFragmentPageAdapter
-
-    override val books: List<Book>
-        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<BookGroup>) {
-        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
-        }
-    }
-}

+ 0 - 102
app/src/main/java/io/legado/app/ui/main/bookshelf/books/styleDefalut/BaseBooksAdapter.kt

@@ -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<VB : ViewBinding>(context: Context) :
-    DiffRecyclerAdapter<Book, VB>(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<Book> =
-        object : DiffUtil.ItemCallback<Book>() {
-
-            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
-    }
-}

+ 0 - 93
app/src/main/java/io/legado/app/ui/main/bookshelf/books/styleDefalut/BooksAdapterGrid.kt

@@ -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<ItemBookshelfGridBinding>(context) {
-
-    override fun getViewBinding(parent: ViewGroup): ItemBookshelfGridBinding {
-        return ItemBookshelfGridBinding.inflate(inflater, parent, false)
-    }
-
-    override fun convert(
-        holder: ItemViewHolder,
-        binding: ItemBookshelfGridBinding,
-        item: Book,
-        payloads: MutableList<Any>
-    ) = 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
-        }
-    }
-}

+ 0 - 103
app/src/main/java/io/legado/app/ui/main/bookshelf/books/styleDefalut/BooksAdapterGridCompact.kt

@@ -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<ItemBookshelfGridCompactBinding>(context) {
-
-    override fun getViewBinding(parent: ViewGroup): ItemBookshelfGridCompactBinding {
-        return ItemBookshelfGridCompactBinding.inflate(inflater, parent, false)
-    }
-
-    override fun convert(
-        holder: ItemViewHolder,
-        binding: ItemBookshelfGridCompactBinding,
-        item: Book,
-        payloads: MutableList<Any>
-    ) = 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
-        }
-    }
-}

+ 0 - 103
app/src/main/java/io/legado/app/ui/main/bookshelf/books/styleDefalut/BooksAdapterGridCover.kt

@@ -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<ItemBookshelfGridCompactBinding>(context) {
-
-    override fun getViewBinding(parent: ViewGroup): ItemBookshelfGridCompactBinding {
-        return ItemBookshelfGridCompactBinding.inflate(inflater, parent, false)
-    }
-
-    override fun convert(
-        holder: ItemViewHolder,
-        binding: ItemBookshelfGridCompactBinding,
-        item: Book,
-        payloads: MutableList<Any>
-    ) = 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
-        }
-    }
-}

+ 0 - 129
app/src/main/java/io/legado/app/ui/main/bookshelf/books/styleDefalut/BooksAdapterList.kt

@@ -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<ItemBookshelfListBinding>(context) {
-
-    override fun getViewBinding(parent: ViewGroup): ItemBookshelfListBinding {
-        return ItemBookshelfListBinding.inflate(inflater, parent, false)
-    }
-
-    override fun convert(
-        holder: ItemViewHolder,
-        binding: ItemBookshelfListBinding,
-        item: Book,
-        payloads: MutableList<Any>
-    ) = 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)
-                }
-            }
-        }
-    }
-}

+ 0 - 133
app/src/main/java/io/legado/app/ui/main/bookshelf/books/styleDefalut/BooksAdapterListCompact.kt

@@ -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<ItemBookshelfListCompactBinding>(context) {
-
-    override fun getViewBinding(parent: ViewGroup): ItemBookshelfListCompactBinding {
-        return ItemBookshelfListCompactBinding.inflate(inflater, parent, false)
-    }
-
-    override fun convert(
-        holder: ItemViewHolder,
-        binding: ItemBookshelfListCompactBinding,
-        item: Book,
-        payloads: MutableList<Any>
-    ) = 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)
-                }
-            }
-        }
-    }
-}

+ 0 - 146
app/src/main/java/io/legado/app/ui/main/bookshelf/books/styleFold/BaseBooksAdapter.kt

@@ -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<VH : RecyclerView.ViewHolder>(
-    val context: Context,
-    val callBack: CallBack
-) : RecyclerView.Adapter<VH>() {
-
-    protected val inflater: LayoutInflater = LayoutInflater.from(context)
-
-    private val diffItemCallback = object : DiffUtil.ItemCallback<Any>() {
-
-        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<Book> = emptyList()
-
-    fun setAllBooks(all: List<Book>) {
-        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<Any>
-    }
-}

+ 0 - 195
app/src/main/java/io/legado/app/ui/main/bookshelf/books/styleFold/BooksAdapterGrid.kt

@@ -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<RecyclerView.ViewHolder>(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<Any>
-    ) {
-
-        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<Book> {
-        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<Any>) = 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<Any>) = 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())
-            }
-        }
-
-    }
-
-}

+ 0 - 202
app/src/main/java/io/legado/app/ui/main/bookshelf/books/styleFold/BooksAdapterGridCompact.kt

@@ -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<RecyclerView.ViewHolder>(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<Any>
-    ) {
-
-        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<Book> {
-        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<Any>) = 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<Any>) = 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())
-            }
-        }
-
-    }
-
-}

+ 0 - 202
app/src/main/java/io/legado/app/ui/main/bookshelf/books/styleFold/BooksAdapterGridCover.kt

@@ -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<RecyclerView.ViewHolder>(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<Any>
-    ) {
-
-        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<Book> {
-        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<Any>) = 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<Any>) = 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())
-            }
-        }
-
-    }
-
-}

+ 0 - 215
app/src/main/java/io/legado/app/ui/main/bookshelf/books/styleFold/BooksAdapterList.kt

@@ -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<RecyclerView.ViewHolder>(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<Any>
-    ) {
-        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<Book> {
-        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<Any>) = 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<Any>) = 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())
-            }
-        }
-
-    }
-
-}

+ 0 - 209
app/src/main/java/io/legado/app/ui/main/bookshelf/books/styleFold/BooksAdapterListCompact.kt

@@ -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<RecyclerView.ViewHolder>(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<Any>
-    ) {
-        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<Book> {
-        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<Any>) = 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<Any>) = 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())
-            }
-        }
-
-    }
-
-}

+ 0 - 225
app/src/main/java/io/legado/app/ui/main/explore/ExploreAdapter.kt

@@ -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<BookSourcePart, ItemFindBookBinding>(context) {
-
-    private val recycler = arrayListOf<View>()
-    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<Any>
-    ) {
-        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<ExploreKind>) {
-        if (kinds.isNotEmpty()) kotlin.runCatching {
-            recyclerFlexbox(flexbox)
-            flexbox.visible()
-            kinds.forEach { kind ->
-                val cardView = getFlexboxChild(flexbox)
-                val textView = cardView.findViewById<TextView>(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<TextView>(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<SourceLoginActivity> {
-                    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)
-    }
-}

+ 0 - 17
app/src/main/java/io/legado/app/ui/main/explore/ExploreDiffItemCallBack.kt

@@ -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<BookSourcePart>() {
-
-    override fun areItemsTheSame(oldItem: BookSourcePart, newItem: BookSourcePart): Boolean {
-        return oldItem == newItem
-    }
-
-    override fun areContentsTheSame(oldItem: BookSourcePart, newItem: BookSourcePart): Boolean {
-        return oldItem.bookSourceName == newItem.bookSourceName
-    }
-
-}

+ 0 - 48
app/src/main/java/io/legado/app/ui/main/explore/ExploreFragment.kt

@@ -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
-    }
-
-}

+ 46 - 49
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
             ) {

+ 1 - 1
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

+ 15 - 1
app/src/main/java/io/legado/app/ui/theme/HazeStyle.kt

@@ -47,4 +47,18 @@ fun Modifier.responsiveHazeEffect(
             )
         }
     }
-}
+}
+
+/**
+ * 仅判断 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()
+    )
+}

+ 27 - 0
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
+}

+ 292 - 0
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
+                    }
+                }
+            }
+        }
+    }
+}

+ 51 - 0
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
+            )
+        }
+    }
+}

+ 1 - 1
app/src/main/java/io/legado/app/ui/widget/components/Cover.kt → 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

+ 198 - 0
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))
+                }
+            }
+        }
+    }
+}

+ 11 - 2
app/src/main/java/io/legado/app/ui/widget/components/exportComponents/FilePickerSheet.kt → 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<String>) -> Unit)? = null,
+    onSelectSysFiles: ((Array<String>) -> Unit)? = null,
     onManualInput: (() -> Unit)? = null,
     onUpload: (() -> Unit)? = null,
     allowExtensions: Array<String>? = 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<String>?): Array<String> {
         }
     }
     return types.toTypedArray()
-}
+}

+ 0 - 1
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)

+ 1 - 1
app/src/main/java/io/legado/app/ui/widget/components/rules/ListUiState.kt → 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<T> {
     val items: List<T>

+ 6 - 3
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
     )
-}
+}

+ 153 - 0
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<String>,
+    entryValues: Array<String>,
+    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<Float>,
+    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()
+            )
+        }
+    )
+}

+ 9 - 2
app/src/main/res/values-zh-rCN/arrays.xml

@@ -9,9 +9,8 @@
 
     <string-array name="group_style">
         <item>标签</item>
-        <item>文件夹</item>
         <item>隐藏标签</item>
-        <item>材料标签</item>
+        <item>文件夹</item>
     </string-array>
 
     <string-array name="text_suffix">
@@ -132,6 +131,14 @@
         <item>@string/bookshelf_px_5</item>
     </string-array>
 
+    <string-array name="bookshelf_layout">
+        <item>@string/list</item>
+        <item>@string/compact_list</item>
+        <item>@string/grid</item>
+        <item>@string/compact_grid</item>
+        <item>@string/cover_grid</item>
+    </string-array>
+
     <string-array name="themes_item">
         <item>动态取色</item>
         <item>草野</item>

+ 1 - 2
app/src/main/res/values-zh-rHK/arrays.xml

@@ -10,9 +10,8 @@
 
     <string-array name="group_style">
         <item>標籤</item>
-        <item>文件夾</item>
         <item>隐藏標籤</item>
-        <item>材料標籤</item>
+        <item>文件夾</item>
     </string-array>
 
     <string-array name="theme_mode">

+ 1 - 2
app/src/main/res/values-zh-rTW/arrays.xml

@@ -9,9 +9,8 @@
 
     <string-array name="group_style">
         <item>標籤</item>
-        <item>資料夾</item>
         <item>隐藏標籤</item>
-        <item>材料標籤</item>
+        <item>資料夾</item>
     </string-array>
 
     <string-array name="text_suffix">

+ 9 - 2
app/src/main/res/values/arrays.xml

@@ -14,9 +14,8 @@
 
 	<string-array name="group_style">
 		<item>Tab</item>
-		<item>Folder</item>
 		<item>Hide Tab</item>
-		<item>Material Tab</item>
+		<item>Folder</item>
 	</string-array>
 
 	<string-array name="book_sort">
@@ -279,6 +278,14 @@
 		<item>@string/bookshelf_px_5</item>
 	</string-array>
 
+	<string-array name="bookshelf_layout">
+		<item>@string/list</item>
+		<item>@string/compact_list</item>
+		<item>@string/grid</item>
+		<item>@string/compact_grid</item>
+		<item>@string/cover_grid</item>
+	</string-array>
+
     <string-array name="label_vis_mode" >
 		<item>Auto</item>
 		<item>Selected</item>

+ 2 - 0
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" }