HapeLee 6 месяцев назад
Родитель
Сommit
b3a294ff1e
22 измененных файлов с 497 добавлено и 492 удалено
  1. 6 1
      .claude/settings.local.json
  2. 0 6
      app/src/main/AndroidManifest.xml
  3. 3 0
      app/src/main/java/io/legado/app/constant/PreferKey.kt
  4. 4 0
      app/src/main/java/io/legado/app/di/appModule.kt
  5. 0 144
      app/src/main/java/io/legado/app/help/config/PersonalizationThemeConfig.kt
  6. 0 11
      app/src/main/java/io/legado/app/help/storage/Restore.kt
  7. 14 5
      app/src/main/java/io/legado/app/ui/book/readRecord/ReadRecordScreen.kt
  8. 2 14
      app/src/main/java/io/legado/app/ui/config/customTheme/CustomThemeScreen.kt
  9. 4 5
      app/src/main/java/io/legado/app/ui/config/themeConfig/NavIconManageSheet.kt
  10. 82 10
      app/src/main/java/io/legado/app/ui/main/MainActivity.kt
  11. 5 2
      app/src/main/java/io/legado/app/ui/main/MainViewModel.kt
  12. 336 286
      app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt
  13. 1 2
      app/src/main/java/io/legado/app/ui/main/my/MyScreen.kt
  14. 1 0
      app/src/main/java/io/legado/app/ui/main/my/MyViewModel.kt
  15. 1 1
      app/src/main/java/io/legado/app/ui/widget/components/card/GlassCard.kt
  16. 2 1
      app/src/main/java/io/legado/app/ui/widget/components/settingItem/CompactSettingItems.kt
  17. 6 1
      app/src/main/res/values-zh-rCN/strings.xml
  18. 6 1
      app/src/main/res/values-zh-rHK/strings.xml
  19. 6 1
      app/src/main/res/values-zh-rTW/strings.xml
  20. 6 0
      app/src/main/res/values/array_values.xml
  21. 6 0
      app/src/main/res/values/arrays.xml
  22. 6 1
      app/src/main/res/values/strings.xml

+ 6 - 1
.claude/settings.local.json

@@ -50,7 +50,12 @@
       "Bash(npx skills *)",
       "Bash(npx *)",
       "PowerShell(.\\\\gradlew.bat :app:compileAppDebugKotlin 2>&1 | Select-Object -Last 20)",
-      "Bash(cmd /c \"gradlew.bat :app:compileAppDebugKotlin\")"
+      "Bash(cmd /c \"gradlew.bat :app:compileAppDebugKotlin\")",
+      "Bash(cmd /c \"gradlew.bat :app:compileAppDebugKotlin 2>&1\")",
+      "PowerShell(.\\\\gradlew.bat :app:compileAppDebugKotlin 2>&1 | Select-Object -Last 40)",
+      "PowerShell(.\\\\gradlew.bat :app:compileAppDebugKotlin 2>&1 | Select-Object -Last 100)",
+      "PowerShell(.\\\\gradlew.bat :app:compileAppDebugKotlin 2>&1 | Select-Object -Last 50)",
+      "Bash(python3 *)"
     ]
   }
 }

+ 0 - 6
app/src/main/AndroidManifest.xml

@@ -291,12 +291,6 @@
             android:configChanges="orientation|screenSize"
             android:hardwareAccelerated="true"
             android:theme="@style/AppTheme.Transparent" />
-        <!-- 阅读记录 -->
-        <activity
-            android:name=".ui.book.readRecord.ReadRecordActivity"
-            android:enableOnBackInvokedCallback="true"
-            android:configChanges="orientation|screenSize"
-            android:hardwareAccelerated="true" />
         <!-- 字典管理 -->
         <activity
             android:name=".ui.dict.rule.DictRuleActivity"

+ 3 - 0
app/src/main/java/io/legado/app/constant/PreferKey.kt

@@ -257,6 +257,7 @@ object PreferKey {
     const val ignoreBatteryPermission = "ignoreBatteryPermission"
     const val paddingDisplayCutouts = "paddingDisplayCutouts"
     const val autoCheckNewBackup = "autoCheckNewBackup"
+    const val backupSyncMode = "backupSyncMode"
     const val delayBookLoadEnable = "delayBookLoadEnable"
     const val sharedElementEnterTransitionEnable = "sharedElementEnterTransitionEnable"
     const val bookshelfLayoutGridLandscape = "bookshelfLayoutGridLandscape"
@@ -301,6 +302,8 @@ object PreferKey {
     const val showTip = "showTip"
     const val showBookCount = "showBookCount"
     const val showBookIntro = "showBookIntro"
+    const val bookshelfShowIntro = "bookshelfShowIntro"
+    const val bookshelfShowTag = "bookshelfShowTag"
     const val bookshelfIntroMaxLines = "bookshelfIntroMaxLines"
     const val sliderVibrator = "sliderVibrator"
     const val selectVibrator = "selectVibrator"

+ 4 - 0
app/src/main/java/io/legado/app/di/appModule.kt

@@ -48,6 +48,7 @@ import io.legado.app.domain.usecase.GetReadingProgressUseCase
 import io.legado.app.domain.usecase.RemoveBookGroupAssignmentUseCase
 import io.legado.app.ui.widget.components.explore.ExploreKindUiUseCase
 import io.legado.app.domain.usecase.ResolveBookShelfStateUseCase
+import io.legado.app.domain.usecase.readRecord.GetReadRecordOverviewUseCase
 import io.legado.app.domain.usecase.SearchBooksUseCase
 import io.legado.app.domain.usecase.ShrinkDatabaseUseCase
 import io.legado.app.domain.usecase.UpdateBooksGroupUseCase
@@ -73,6 +74,7 @@ import io.legado.app.ui.book.info.BookInfoViewModel
 import io.legado.app.ui.book.manga.ReadMangaViewModel
 import io.legado.app.ui.book.read.ReadBookViewModel
 import io.legado.app.ui.book.readRecord.ReadRecordViewModel
+import io.legado.app.ui.book.readRecord.ReadRecordOverviewViewModel
 import io.legado.app.ui.book.search.SearchViewModel
 import io.legado.app.ui.book.searchContent.SearchContentViewModel
 import io.legado.app.ui.book.toc.TocViewModel
@@ -128,6 +130,7 @@ val appModule = module {
     singleOf(::UpdateBooksGroupUseCase)
     singleOf(::UploadReadingProgressUseCase)
     singleOf(::ResolveBookShelfStateUseCase)
+    factory { GetReadRecordOverviewUseCase() }
     singleOf(::ShrinkDatabaseUseCase)
     singleOf(::WebDavBackupUseCase)
     singleOf(::BookshelfManageScreenConfig)
@@ -174,6 +177,7 @@ val appModule = module {
     viewModelOf(::RssArticlesViewModel)
     viewModelOf(::ReadRssViewModel)
     viewModelOf(::ReadRecordViewModel)
+    viewModelOf(::ReadRecordOverviewViewModel)
     viewModelOf(::ExploreShowViewModel)
     viewModelOf(::MyViewModel)
     viewModelOf(::BookshelfViewModel)

+ 0 - 144
app/src/main/java/io/legado/app/help/config/PersonalizationThemeConfig.kt

@@ -1,144 +0,0 @@
-package io.legado.app.help.config
-
-import com.google.gson.Gson
-import com.google.gson.GsonBuilder
-import com.google.gson.JsonDeserializationContext
-import com.google.gson.JsonDeserializer
-import com.google.gson.JsonElement
-import com.google.gson.JsonObject
-import com.google.gson.JsonSerializationContext
-import com.google.gson.JsonSerializer
-import io.legado.app.utils.FileUtils
-import io.legado.app.utils.getString
-import splitties.init.appCtx
-import java.io.File
-import java.lang.reflect.Type
-
-object PersonalizationThemeConfig {
-    const val configFileName = "personalizationThemeConfig.json"
-    val configFilePath = FileUtils.getPath(appCtx.filesDir, configFileName)
-
-    private val THEME_GSON: Gson by lazy {
-        GsonBuilder()
-            .registerTypeAdapter(Config::class.java, ConfigSerializer())
-            .setPrettyPrinting()
-            .disableHtmlEscaping()
-            .create()
-    }
-
-    private val _configList: ArrayList<Config> by lazy {
-        ArrayList(getConfigs() ?: emptyList())
-    }
-
-    val configList: List<Config> get() = _configList
-
-    fun save() {
-        val json = THEME_GSON.toJson(_configList)
-        FileUtils.delete(configFilePath)
-        FileUtils.createFileIfNotExist(configFilePath).writeText(json)
-    }
-
-    fun toJson(config: Config): String {
-        return THEME_GSON.toJson(config)
-    }
-
-    fun toJson(): String {
-        return THEME_GSON.toJson(_configList)
-    }
-
-    fun fromJson(json: String): Boolean {
-        return kotlin.runCatching {
-            val configs = THEME_GSON.fromJson(json, Array<Config>::class.java)
-            _configList.clear()
-            _configList.addAll(configs)
-            save()
-            true
-        }.getOrDefault(false)
-    }
-
-    fun delConfig(index: Int) {
-        if (index in _configList.indices) {
-            _configList.removeAt(index)
-            save()
-        }
-    }
-
-    fun addConfig(json: String): Boolean {
-        return kotlin.runCatching {
-            val config = THEME_GSON.fromJson(json, Config::class.java)
-            addConfig(config)
-            true
-        }.getOrDefault(false)
-    }
-
-    fun addConfig(newConfig: Config) {
-        _configList.forEachIndexed { index, config ->
-            if (newConfig.themeName == config.themeName) {
-                _configList[index] = newConfig
-                save()
-                return
-            }
-        }
-        _configList.add(newConfig)
-        save()
-    }
-
-    private fun getConfigs(): List<Config>? {
-        val configFile = File(configFilePath)
-        if (configFile.exists()) {
-            kotlin.runCatching {
-                val json = configFile.readText()
-                return THEME_GSON.fromJson(json, Array<Config>::class.java).toList()
-            }
-        }
-        return null
-    }
-
-    fun applyConfig(config: Config) {
-        config.appearance.applyToThemeConfig()
-    }
-
-    fun savePersonalizationTheme(name: String): Config {
-        val config = Config(
-            themeName = name,
-            appearance = ThemeAppearanceConfig.fromCurrent()
-        )
-        addConfig(config)
-        return config
-    }
-
-    data class Config(
-        var themeName: String = "",
-        var appearance: ThemeAppearanceConfig = ThemeAppearanceConfig()
-    ) {
-        val themeColor: Int get() = appearance.themeColor
-    }
-}
-
-class ConfigSerializer : JsonSerializer<PersonalizationThemeConfig.Config>,
-    JsonDeserializer<PersonalizationThemeConfig.Config> {
-
-    override fun serialize(
-        src: PersonalizationThemeConfig.Config,
-        typeOfSrc: Type,
-        context: JsonSerializationContext
-    ): JsonElement {
-        val obj = JsonObject()
-        obj.addProperty("themeName", src.themeName)
-        ThemeAppearanceJson.writeTo(obj, src.appearance)
-        return obj
-    }
-
-    override fun deserialize(
-        json: JsonElement,
-        typeOfT: Type,
-        context: JsonDeserializationContext
-    ): PersonalizationThemeConfig.Config {
-        val obj = json.asJsonObject
-        return PersonalizationThemeConfig.Config(
-            themeName = obj.getString("themeName", "") ?: "",
-            appearance = ThemeAppearanceJson.readFrom(obj)
-        )
-    }
-
-}

+ 0 - 11
app/src/main/java/io/legado/app/help/storage/Restore.kt

@@ -34,7 +34,6 @@ import io.legado.app.help.book.isLocal
 import io.legado.app.help.book.upType
 import io.legado.app.help.config.LocalConfig
 import io.legado.app.help.config.OldThemeConfig
-import io.legado.app.help.config.PersonalizationThemeConfig
 import io.legado.app.help.config.ReadBookConfig
 import io.legado.app.model.BookCover
 import io.legado.app.model.localBook.LocalBook
@@ -284,16 +283,6 @@ object Restore {
         }?.onFailure {
             AppLog.put("恢复封面规则出错\n${it.localizedMessage}", it)
         }
-        if (!BackupConfig.ignorePersonalizationThemeConfig) {
-            File(path, PersonalizationThemeConfig.configFileName).takeIf {
-                it.exists()
-            }?.runCatching {
-                val json = readText()
-                PersonalizationThemeConfig.fromJson(json)
-            }?.onFailure {
-                AppLog.put("恢复个性化主题出错\n${it.localizedMessage}", it)
-            }
-        }
         if (!BackupConfig.ignoreReadConfig) {
             //恢复阅读界面配置
             File(path, ReadBookConfig.configFileName).takeIf {

+ 14 - 5
app/src/main/java/io/legado/app/ui/book/readRecord/ReadRecordScreen.kt

@@ -68,6 +68,8 @@ import androidx.compose.ui.zIndex
 import cn.hutool.core.date.DateUtil
 import io.legado.app.data.entities.readRecord.ReadRecord
 import io.legado.app.data.entities.readRecord.ReadRecordDetail
+import io.legado.app.data.entities.readRecord.ReadRecordSession
+import io.legado.app.ui.book.readRecord.component.*
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.theme.adaptiveContentPaddingOnlyVertical
 import io.legado.app.ui.theme.adaptiveHorizontalPadding
@@ -110,12 +112,18 @@ import java.time.LocalDate
 import java.time.format.DateTimeFormatter
 import java.util.Date
 
+data class TimelineItem(
+    val session: ReadRecordSession,
+    val showHeader: Boolean
+)
+
 @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
 @Composable
 fun ReadRecordScreen(
     viewModel: ReadRecordViewModel = koinViewModel(),
     onBackClick: () -> Unit,
-    onBookClick: (String, String) -> Unit
+    onBookClick: (String, String) -> Unit,
+    onSummaryClick: () -> Unit
 ) {
     val snackbarHostState = remember { SnackbarHostState() }
     val scope = rememberCoroutineScope()
@@ -283,7 +291,7 @@ fun ReadRecordScreen(
                             )
                         ) {
                             item(key = "summary_card") {
-                                SummarySection(state, viewModel)
+                                SummarySection(state, viewModel, onSummaryClick)
                             }
                             renderListByMode(
                                 displayMode = displayMode,
@@ -446,7 +454,8 @@ fun ReadRecordScreen(
 @Composable
 fun SummarySection(
     state: ReadRecordUiState,
-    viewModel: ReadRecordViewModel
+    viewModel: ReadRecordViewModel,
+    onSummaryClick: () -> Unit
 ) {
     val selectedDate = state.selectedDate
 
@@ -464,7 +473,7 @@ fun SummarySection(
                 totalTimeMillis = dailyTime,
                 bookNamesForCover = distinctBooks.take(3),
                 viewModel = viewModel,
-                onClick = { }
+                onClick = onSummaryClick
             )
         }
     } else {
@@ -478,7 +487,7 @@ fun SummarySection(
                 totalTimeMillis = totalTime,
                 bookNamesForCover = state.latestRecords.take(5).map { it.bookName to it.bookAuthor },
                 viewModel = viewModel,
-                onClick = {  }
+                onClick = onSummaryClick
             )
         }
     }

+ 2 - 14
app/src/main/java/io/legado/app/ui/config/customTheme/CustomThemeScreen.kt

@@ -44,8 +44,7 @@ import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
 @OptIn(ExperimentalMaterial3Api::class)
 @Composable
 fun CustomThemeScreen(
-    onBackClick: () -> Unit,
-    onNavigateToThemePack: () -> Unit
+    onBackClick: () -> Unit
 ) {
     val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
     var showColorPicker by remember { mutableStateOf(false) }
@@ -73,7 +72,7 @@ fun CustomThemeScreen(
         modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
         topBar = {
             GlassMediumFlexibleTopAppBar(
-                title = stringResource(R.string.custom_theme),
+                title = "自定义颜色",
                 scrollBehavior = scrollBehavior,
                 navigationIcon = {
                     TopBarNavigationButton(onClick = onBackClick)
@@ -299,17 +298,6 @@ fun CustomThemeScreen(
                 }
             }
 
-            // Theme Pack entry
-            item {
-                SplicedColumnGroup {
-                    ClickableSettingItem(
-                        title = stringResource(R.string.theme_pack),
-                        description = stringResource(R.string.theme_pack_s),
-                        onClick = onNavigateToThemePack
-                    )
-                }
-            }
-
         }
 
         // Deep personalization color picker

+ 4 - 5
app/src/main/java/io/legado/app/ui/config/themeConfig/NavIconManageSheet.kt

@@ -26,7 +26,6 @@ import androidx.compose.ui.layout.ContentScale
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.unit.dp
 import coil.compose.AsyncImage
-import io.legado.app.ui.config.mainConfig.MainConfig
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.widget.components.button.SmallTonalIconButton
 import io.legado.app.ui.widget.components.card.NormalCard
@@ -52,10 +51,10 @@ fun NavIconManageSheet(
     var activeDest by remember { mutableStateOf<String?>(null) }
 
     val destinations = listOf(
-        NavIconDestination("bookshelf", "书架", MainConfig.navIconBookshelf) { MainConfig.navIconBookshelf = it },
-        NavIconDestination("explore", "发现", MainConfig.navIconExplore) { MainConfig.navIconExplore = it },
-        NavIconDestination("rss", "订阅", MainConfig.navIconRss) { MainConfig.navIconRss = it },
-        NavIconDestination("my", "我的", MainConfig.navIconMy) { MainConfig.navIconMy = it },
+        NavIconDestination("bookshelf", "书架", ThemeConfig.navIconBookshelf) { ThemeConfig.navIconBookshelf = it },
+        NavIconDestination("explore", "发现", ThemeConfig.navIconExplore) { ThemeConfig.navIconExplore = it },
+        NavIconDestination("rss", "订阅", ThemeConfig.navIconRss) { ThemeConfig.navIconRss = it },
+        NavIconDestination("my", "我的", ThemeConfig.navIconMy) { ThemeConfig.navIconMy = it },
     )
 
     val selectImage = rememberLauncherForActivityResult(

+ 82 - 10
app/src/main/java/io/legado/app/ui/main/MainActivity.kt

@@ -52,6 +52,9 @@ import io.legado.app.ui.book.import.remote.RemoteBookScreen
 import io.legado.app.ui.book.search.SearchIntent
 import io.legado.app.ui.book.search.SearchScreen
 import io.legado.app.ui.book.search.SearchViewModel
+import io.legado.app.ui.book.readRecord.ReadRecordOverviewScreen
+import io.legado.app.ui.book.readRecord.ReadRecordScreen
+import io.legado.app.ui.book.readRecord.ReadRecordViewModel
 import io.legado.app.ui.book.source.manage.BookSourceActivity
 import io.legado.app.ui.book.manage.BookshelfManageRouteScreen
 import io.legado.app.ui.book.read.ReadBookActivity
@@ -59,13 +62,13 @@ import io.legado.app.ui.config.ConfigNavScreen
 import io.legado.app.ui.config.ConfigTag
 import io.legado.app.ui.config.backupConfig.BackupConfigScreen
 import io.legado.app.ui.config.coverConfig.CoverConfigScreen
-import io.legado.app.ui.config.mainConfig.MainConfig
+import io.legado.app.ui.config.themeConfig.ThemeConfig
 import io.legado.app.ui.config.otherConfig.OtherConfigScreen
 import io.legado.app.ui.config.personalizationConfig.FontSelectScreen
 import io.legado.app.ui.config.customTheme.CustomThemeScreen
 import io.legado.app.ui.config.readConfig.ReadConfigScreen
 import io.legado.app.ui.config.themeConfig.ThemeConfigScreen
-import io.legado.app.ui.config.themePack.ThemePackScreen
+import io.legado.app.ui.config.themeManage.ThemeManageScreen
 import io.legado.app.ui.rss.article.MainRouteRssSort
 import io.legado.app.ui.rss.article.RssSortRouteScreen
 import io.legado.app.ui.rss.read.MainRouteRssRead
@@ -76,6 +79,7 @@ import io.legado.app.ui.widget.dialog.VariableDialog
 import io.legado.app.utils.getPrefBoolean
 import io.legado.app.utils.showDialogFragment
 import io.legado.app.utils.startActivity
+import io.legado.app.utils.startActivityForBook
 import kotlinx.coroutines.Dispatchers.IO
 import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.launch
@@ -110,6 +114,8 @@ open class MainActivity : BaseComposeActivity(), VariableDialog.Callback {
         private const val ROUTE_EXPLORE_SHOW = "explore/show"
         private const val ROUTE_RSS_SORT = "rss/sort"
         private const val ROUTE_RSS_READ = "rss/read"
+        private const val ROUTE_READ_RECORD = "read_record"
+        private const val ROUTE_READ_RECORD_OVERVIEW = "read_record_overview"
         private const val EXTRA_CACHE_GROUP_ID = "extra_cache_group_id"
         private const val EXTRA_SEARCH_KEY = "extra_search_key"
         private const val EXTRA_SEARCH_SCOPE = "extra_search_scope"
@@ -286,7 +292,7 @@ open class MainActivity : BaseComposeActivity(), VariableDialog.Callback {
     private data object MainRouteSettingsCustomTheme : MainRoute
 
     @Serializable
-    private data object MainRouteSettingsThemePack : MainRoute
+    private data object MainRouteSettingsThemeManage : MainRoute
 
     @Serializable
     private data object MainRouteImportLocal : MainRoute
@@ -294,6 +300,12 @@ open class MainActivity : BaseComposeActivity(), VariableDialog.Callback {
     @Serializable
     private data object MainRouteImportRemote : MainRoute
 
+    @Serializable
+    private data object MainRouteReadRecord : MainRoute
+
+    @Serializable
+    private data object MainRouteReadRecordOverview : MainRoute
+
     @Serializable
     private data class MainRouteCache(val groupId: Long) : MainRoute
 
@@ -358,7 +370,7 @@ open class MainActivity : BaseComposeActivity(), VariableDialog.Callback {
     override fun Content() {
         val orientation = resources.configuration.orientation
         val smallestWidthDp = resources.configuration.smallestScreenWidthDp
-        val tabletInterface = MainConfig.tabletInterface
+        val tabletInterface = ThemeConfig.tabletInterface
 
         val useRail = when (tabletInterface) {
             "always" -> true
@@ -524,6 +536,9 @@ open class MainActivity : BaseComposeActivity(), VariableDialog.Callback {
                                 )
                             )
                         },
+                        onNavigateToReadRecord = {
+                            navigateToRoute(backStack, MainRouteReadRecord)
+                        },
                         sharedTransitionScope = this@SharedTransitionLayout,
                         animatedVisibilityScope = LocalNavAnimatedContentScope.current,
                     )
@@ -555,7 +570,8 @@ open class MainActivity : BaseComposeActivity(), VariableDialog.Callback {
                 entry<MainRouteSettingsTheme> {
                     ThemeConfigScreen(
                         onBackClick = { navigateBack(backStack) },
-                        onNavigateToCustomTheme = { backStack.add(MainRouteSettingsCustomTheme) }
+                        onNavigateToCustomTheme = { backStack.add(MainRouteSettingsCustomTheme) },
+                        onNavigateToThemeManage = { backStack.add(MainRouteSettingsThemeManage) }
                     )
                 }
 
@@ -565,13 +581,12 @@ open class MainActivity : BaseComposeActivity(), VariableDialog.Callback {
 
                 entry<MainRouteSettingsCustomTheme> {
                     CustomThemeScreen(
-                        onBackClick = { navigateBack(backStack) },
-                        onNavigateToThemePack = { backStack.add(MainRouteSettingsThemePack) }
+                        onBackClick = { navigateBack(backStack) }
                     )
                 }
 
-                entry<MainRouteSettingsThemePack> {
-                    ThemePackScreen(onBackClick = { navigateBack(backStack) })
+                entry<MainRouteSettingsThemeManage> {
+                    ThemeManageScreen(onBackClick = { navigateBack(backStack) })
                 }
 
                 entry<MainRouteImportLocal> {
@@ -684,6 +699,43 @@ open class MainActivity : BaseComposeActivity(), VariableDialog.Callback {
                     )
                 }
 
+                entry<MainRouteReadRecord> {
+                    ReadRecordScreen(
+                        onBackClick = { navigateBack(backStack) },
+                        onBookClick = { name, author ->
+                            lifecycleScope.launch {
+                                val book = withContext(IO) {
+                                    io.legado.app.data.appDb.bookDao.getBook(name, author)
+                                }
+                                if (book != null) this@MainActivity.startActivityForBook(book)
+                                else {
+                                    navigateToRoute(backStack, MainRouteSearch(key = name))
+                                }
+                            }
+                        },
+                        onSummaryClick = {
+                            navigateToRoute(backStack, MainRouteReadRecordOverview)
+                        }
+                    )
+                }
+
+                entry<MainRouteReadRecordOverview> {
+                    ReadRecordOverviewScreen(
+                        onBackClick = { navigateBack(backStack) },
+                        onBookClick = { name, author ->
+                            lifecycleScope.launch {
+                                val book = withContext(IO) {
+                                    io.legado.app.data.appDb.bookDao.getBook(name, author)
+                                }
+                                if (book != null) this@MainActivity.startActivityForBook(book)
+                                else {
+                                    navigateToRoute(backStack, MainRouteSearch(key = name))
+                                }
+                            }
+                        }
+                    )
+                }
+
                 entry<MainRouteBookInfo>(
                     metadata = NavDisplay.transitionSpec {
                         val from = initialState.key
@@ -780,7 +832,7 @@ open class MainActivity : BaseComposeActivity(), VariableDialog.Callback {
             MainRouteSettingsTheme,
             MainRouteSettingsBackup,
             MainRouteSettingsCustomTheme,
-            MainRouteSettingsThemePack -> {
+            MainRouteSettingsThemeManage -> {
                 backStack.clear()
                 backStack.add(MainRouteHome)
                 backStack.add(MainRouteSettings)
@@ -853,6 +905,26 @@ open class MainActivity : BaseComposeActivity(), VariableDialog.Callback {
                     backStack.add(route)
                 }
             }
+
+            MainRouteReadRecord -> {
+                if (currentRoute == MainRouteHome) {
+                    backStack.add(route)
+                } else {
+                    backStack.clear()
+                    backStack.add(MainRouteHome)
+                    backStack.add(route)
+                }
+            }
+
+            MainRouteReadRecordOverview -> {
+                if (currentRoute == MainRouteHome || currentRoute == MainRouteReadRecord) {
+                    backStack.add(route)
+                } else {
+                    backStack.clear()
+                    backStack.add(MainRouteHome)
+                    backStack.add(route)
+                }
+            }
         }
     }
 

+ 5 - 2
app/src/main/java/io/legado/app/ui/main/MainViewModel.kt

@@ -7,7 +7,7 @@ import io.legado.app.constant.PreferKey
 import io.legado.app.constant.EventBus
 import io.legado.app.domain.usecase.AppStartupMaintenanceUseCase
 import io.legado.app.domain.usecase.WebDavBackupUseCase
-import io.legado.app.ui.config.mainConfig.MainConfig
+import io.legado.app.ui.config.themeConfig.ThemeConfig
 import io.legado.app.ui.main.my.PrefClickEvent
 import io.legado.app.utils.defaultSharedPreferences
 import io.legado.app.utils.eventBus.FlowEventBus
@@ -87,7 +87,7 @@ class MainViewModel(
     fun setNavExtended(expanded: Boolean) {
         if (_uiState.value.navExtended == expanded) return
         _uiState.update { it.copy(navExtended = expanded) }
-        MainConfig.navExtended = expanded
+        ThemeConfig.navExtended = expanded
     }
 
     fun onPrefClickEvent(event: PrefClickEvent) {
@@ -112,6 +112,8 @@ class MainViewModel(
 
             PrefClickEvent.ExitApp -> _effects.tryEmit(MainEffect.ExitApp)
 
+            PrefClickEvent.OpenReadRecord -> _effects.tryEmit(MainEffect.NavigateToReadRecord)
+
             else -> Unit
         }
     }
@@ -128,6 +130,7 @@ sealed interface MainEffect {
     ) : MainEffect
 
     data object ExitApp : MainEffect
+    data object NavigateToReadRecord : MainEffect
 }
 
 data class MainUiState(

+ 336 - 286
app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt

@@ -10,6 +10,10 @@ import androidx.activity.result.contract.ActivityResultContracts
 import androidx.compose.animation.AnimatedContent
 import androidx.compose.animation.AnimatedVisibility
 import androidx.compose.animation.AnimatedVisibilityScope
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.slideInVertically
+import androidx.compose.animation.slideOutVertically
 import androidx.compose.animation.ExperimentalSharedTransitionApi
 import androidx.compose.animation.SharedTransitionScope
 import androidx.compose.foundation.clickable
@@ -52,6 +56,8 @@ import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
 import androidx.compose.material3.Icon
 import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.FloatingToolbarDefaults.ScreenOffset
+import androidx.compose.material3.SnackbarHost
 import androidx.compose.material3.SnackbarHostState
 import androidx.compose.material3.SnackbarResult
 import androidx.compose.material3.Surface
@@ -68,6 +74,8 @@ import androidx.compose.runtime.rememberUpdatedState
 import androidx.compose.runtime.snapshotFlow
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.zIndex
 import androidx.compose.ui.hapticfeedback.HapticFeedbackType
 import androidx.compose.ui.platform.ClipEntry
 import androidx.compose.ui.platform.LocalClipboard
@@ -91,8 +99,10 @@ import io.legado.app.ui.theme.adaptiveContentPaddingBookshelf
 import io.legado.app.ui.theme.adaptiveHorizontalPadding
 import io.legado.app.ui.theme.adaptiveHorizontalPaddingTab
 import io.legado.app.ui.widget.components.ActionItem
+import io.legado.app.ui.widget.components.AppScaffold
 import io.legado.app.ui.widget.components.EmptyMessage
 import io.legado.app.ui.widget.components.SelectionActions
+import io.legado.app.ui.widget.components.SelectionBottomBar
 import io.legado.app.ui.widget.components.button.SmallOutlinedIconToggleButton
 import io.legado.app.ui.widget.components.topbar.TopBarActionButton
 import io.legado.app.ui.widget.components.alert.AppAlertDialog
@@ -103,8 +113,9 @@ import io.legado.app.ui.widget.components.filePicker.FilePickerSheet
 import io.legado.app.ui.widget.components.icon.AppIcons
 import io.legado.app.ui.widget.components.importComponents.SourceInputDialog
 import io.legado.app.ui.widget.components.lazylist.FastScrollLazyVerticalGrid
-import io.legado.app.ui.widget.components.list.ListScaffold
 import io.legado.app.ui.widget.components.list.TopFloatingStickyItem
+import io.legado.app.ui.widget.components.topbar.DynamicTopAppBar
+import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
 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.tabRow.AppTabRow
@@ -186,14 +197,29 @@ fun BookshelfScreen(
     )
 
     if (uiState.groups.isEmpty()) {
-        ListScaffold(
-            title = uiState.title.ifEmpty { stringResource(R.string.bookshelf) },
-            subtitle = uiState.subtitle,
-            state = uiState,
-            showSearchAction = true,
-            onSearchToggle = { viewModel.setSearchMode(it) },
-            onSearchQueryChange = { viewModel.setSearchKey(it) },
-            snackbarHostState = snackbarHostState
+        val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
+        AppScaffold(
+            modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
+            snackbarHost = {
+                SnackbarHost(
+                    hostState = snackbarHostState,
+                    modifier = Modifier
+                        .padding(bottom = 72.dp)
+                )
+            },
+            topBar = {
+                DynamicTopAppBar(
+                    title = uiState.title.ifEmpty { stringResource(R.string.bookshelf) },
+                    subtitle = uiState.subtitle,
+                    state = uiState,
+                    scrollBehavior = scrollBehavior,
+                    showSearchAction = true,
+                    onSearchToggle = { viewModel.setSearchMode(it) },
+                    onSearchQueryChange = { viewModel.setSearchKey(it) },
+                    onClearSelection = { viewModel.clearSelection() },
+                    searchPlaceholder = "搜索...",
+                )
+            }
         ) { paddingValues ->
             EmptyMessage(
                 modifier = Modifier
@@ -321,280 +347,235 @@ fun BookshelfScreen(
         }
     }
 
-    ListScaffold(
-        title = uiState.title.ifEmpty { stringResource(R.string.bookshelf) },
-        subtitle = uiState.subtitle,
-        state = uiState,
-        showSearchAction = true,
-        onSearchToggle = { active ->
-            if (BookshelfConfig.bookshelfSearchActionDirectToSearch) {
-                onNavigateToSearch(uiState.searchKey.trim())
-            } else {
-                viewModel.setSearchMode(active)
-                if (!active && uiState.selectedGroupId != currentTabGroupId) {
-                    viewModel.changeGroup(currentTabGroupId)
-                }
-            }
-        },
-        onSearchQueryChange = { viewModel.setSearchKey(it) },
-        onSearchSubmit = { rawQuery ->
-            rawQuery.trim()
-                .takeIf { it.isNotEmpty() }
-                ?.let(onNavigateToSearch)
-        },
-        searchTrailingIcon = {
-            if (uiState.searchKey.isNotEmpty()) {
-                TopBarActionButton(
-                    onClick = { viewModel.setSearchKey("") },
-                    imageVector = AppIcons.Close,
-                    contentDescription = stringResource(R.string.clear)
-                )
-            }
-        },
-        topBarActions = {
-            AnimatedVisibility(visible = isEditMode) {
-                TopBarActionButton(
-                    onClick = { viewModel.selectAllVisible() },
-                    imageVector = Icons.Default.SelectAll,
-                    contentDescription = stringResource(R.string.select_all)
-                )
-            }
-            AnimatedVisibility(visible = isEditMode) {
-                TopBarActionButton(
-                    onClick = { viewModel.invertVisibleSelection() },
-                    imageVector = Icons.Default.Refresh,
-                    contentDescription = stringResource(R.string.revert_selection)
-                )
-            }
-            AnimatedVisibility(visible = isEditMode) {
-                TopBarActionButton(
-                    onClick = {
-                        if (selectedBookUrls.isNotEmpty()) {
-                            viewModel.showOverlay(BookshelfOverlay.BatchDownloadConfirmDialog)
-                        }
-                    },
-                    imageVector = Icons.Default.Download,
-                    contentDescription = stringResource(R.string.action_download)
-                )
-            }
-            AnimatedVisibility(visible = isEditMode) {
-                TopBarActionButton(
-                    onClick = {
-                        if (selectedBookUrls.isNotEmpty()) {
-                            viewModel.showOverlay(BookshelfOverlay.GroupSelectSheet)
-                        }
-                    },
-                    imageVector = Icons.Default.Bookmarks,
-                    contentDescription = stringResource(R.string.move_to_group)
-                )
-            }
+    val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
+
+    AppScaffold(
+        modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
+        snackbarHost = {
+            SnackbarHost(
+                hostState = snackbarHostState,
+                modifier = Modifier
+                    .padding(bottom = 72.dp)
+            )
         },
-        dropDownMenuContent = if (!isEditMode) {
-            { dismiss ->
-                RoundDropdownMenuItem(
-                    text = stringResource(R.string.add_remote_book),
-                    onClick = { onNavigateToRemoteImport(); dismiss() },
-                    leadingIcon = { Icon(Icons.Default.Wifi, null) }
-                )
-                RoundDropdownMenuItem(
-                    text = stringResource(R.string.book_local),
-                    onClick = { onNavigateToLocalImport(); dismiss() },
-                    leadingIcon = { Icon(Icons.Default.Save, null) }
-                )
-                RoundDropdownMenuItem(
-                    text = stringResource(R.string.update_toc),
-                    onClick = { viewModel.upToc(uiState.items); dismiss() },
-                    leadingIcon = { Icon(Icons.Default.Refresh, null) }
-                )
-                RoundDropdownMenuItem(
-                    text = stringResource(R.string.layout_setting),
-                    onClick = {
-                        viewModel.showOverlay(BookshelfOverlay.ConfigSheet)
-                        dismiss()
-                    },
-                    leadingIcon = { Icon(Icons.Default.GridView, null) }
-                )
-                RoundDropdownMenuItem(
-                    text = stringResource(R.string.group_manage),
-                    onClick = {
-                        viewModel.showOverlay(BookshelfOverlay.GroupManageSheet)
-                        dismiss()
-                    },
-                    leadingIcon = { Icon(Icons.Default.Edit, null) }
-                )
-                RoundDropdownMenuItem(
-                    text = stringResource(R.string.add_url),
-                    onClick = {
-                        viewModel.showOverlay(BookshelfOverlay.AddUrlDialog)
-                        dismiss()
-                    },
-                    leadingIcon = { Icon(Icons.Default.Link, null) }
-                )
-                RoundDropdownMenuItem(
-                    text = stringResource(R.string.edit),
-                    onClick = {
-                        toggleEditMode()
-                        dismiss()
-                    },
-                    leadingIcon = { Icon(Icons.Default.Edit, null) }
-                )
-                RoundDropdownMenuItem(
-                    text = stringResource(R.string.bookshelf_management),
-                    onClick = {
-                        val groupId =
-                            uiState.groups.getOrNull(uiState.selectedGroupIndex)?.groupId ?: -1L
-                        onNavigateToCache(groupId)
-                        dismiss()
-                    },
-                    leadingIcon = { Icon(Icons.Default.Bookmarks, null) }
-                )
-                RoundDropdownMenuItem(
-                    text = stringResource(R.string.export_bookshelf),
-                    onClick = {
-                        viewModel.showOverlay(BookshelfOverlay.ExportSheet)
-                        dismiss()
-                    },
-                    leadingIcon = { Icon(Icons.Default.UploadFile, null) }
-                )
-                RoundDropdownMenuItem(
-                    text = stringResource(R.string.import_bookshelf),
-                    onClick = {
-                        viewModel.showOverlay(BookshelfOverlay.ImportSheet)
-                        dismiss()
-                    },
-                    leadingIcon = { Icon(Icons.Default.CloudDownload, null) }
-                )
-                RoundDropdownMenuItem(
-                    text = stringResource(R.string.log),
-                    onClick = {
-                        viewModel.showOverlay(BookshelfOverlay.LogSheet)
-                        dismiss()
-                    },
-                    leadingIcon = { Icon(Icons.Default.History, null) }
-                )
-            }
-        } else null,
-        selectionActions = if (isEditMode) {
-            SelectionActions(
-                primaryAction = ActionItem(
-                    text = stringResource(R.string.action_download),
-                    icon = { Icon(Icons.Default.Download, contentDescription = null) },
-                    onClick = {
-                        if (selectedBookUrls.isNotEmpty()) {
-                            viewModel.showOverlay(BookshelfOverlay.BatchDownloadConfirmDialog)
+        topBar = {
+            DynamicTopAppBar(
+                title = uiState.title.ifEmpty { stringResource(R.string.bookshelf) },
+                subtitle = uiState.subtitle,
+                state = uiState,
+                scrollBehavior = scrollBehavior,
+                onBackClick = if (isEditMode) exitEditMode else null,
+                backNavigationIcon = AppIcons.Close,
+                showSearchAction = true,
+                onSearchToggle = { active ->
+                    if (BookshelfConfig.bookshelfSearchActionDirectToSearch) {
+                        onNavigateToSearch(uiState.searchKey.trim())
+                    } else {
+                        viewModel.setSearchMode(active)
+                        if (!active && uiState.selectedGroupId != currentTabGroupId) {
+                            viewModel.changeGroup(currentTabGroupId)
                         }
                     }
-                ),
-                secondaryActions = listOf(
-                    ActionItem(
-                        text = stringResource(R.string.move_to_group),
-                        icon = { Icon(Icons.Default.Bookmarks, contentDescription = null) },
-                        onClick = {
-                            if (selectedBookUrls.isNotEmpty()) {
-                                viewModel.showOverlay(BookshelfOverlay.GroupSelectSheet)
-                            }
-                        }
-                    )
-                ),
+                },
+                onSearchQueryChange = { viewModel.setSearchKey(it) },
+                onSearchSubmit = { rawQuery ->
+                    rawQuery.trim()
+                        .takeIf { it.isNotEmpty() }
+                        ?.let(onNavigateToSearch)
+                },
+                searchTrailingIcon = {
+                    if (uiState.searchKey.isNotEmpty()) {
+                        TopBarActionButton(
+                            onClick = { viewModel.setSearchKey("") },
+                            imageVector = AppIcons.Close,
+                            contentDescription = stringResource(R.string.clear)
+                        )
+                    }
+                },
+                searchPlaceholder = "搜索...",
                 onClearSelection = { viewModel.clearSelection() },
-                onSelectAll = { viewModel.selectAllVisible() },
-                onSelectInvert = { viewModel.invertVisibleSelection() }
-            )
-        } else {
-            null
-        },
-        snackbarHostState = snackbarHostState,
-        bottomContent = if (bookGroupStyle == 0) {
-            {
-                if (uiState.groups.isNotEmpty()) {
-                    val selectedTabIndex =
-                        pagerState.currentPage.coerceIn(0, uiState.groups.size - 1)
-                    Row(
-                        modifier = Modifier
-                            .fillMaxWidth()
-                            .adaptiveHorizontalPaddingTab(),
-                        verticalAlignment = Alignment.CenterVertically
-                    ) {
-                        val tabTitles = remember(uiState.groups) {
-                            uiState.groups.map { it.groupName }
-                        }
-
-                        AppTabRow(
-                            tabTitles = tabTitles,
-                            selectedTabIndex = selectedTabIndex,
-                            onTabSelected = { index ->
-                                scope.launch { pagerState.animateScrollToPage(index) }
+                topBarActions = {
+                    AnimatedVisibility(visible = isEditMode) {
+                        TopBarActionButton(
+                            onClick = { viewModel.selectAllVisible() },
+                            imageVector = Icons.Default.SelectAll,
+                            contentDescription = stringResource(R.string.select_all)
+                        )
+                    }
+                    AnimatedVisibility(visible = isEditMode) {
+                        TopBarActionButton(
+                            onClick = { viewModel.invertVisibleSelection() },
+                            imageVector = Icons.Default.Refresh,
+                            contentDescription = stringResource(R.string.revert_selection)
+                        )
+                    }
+                    AnimatedVisibility(visible = isEditMode) {
+                        TopBarActionButton(
+                            onClick = {
+                                if (selectedBookUrls.isNotEmpty()) {
+                                    viewModel.showOverlay(BookshelfOverlay.BatchDownloadConfirmDialog)
+                                }
                             },
-                            modifier = Modifier.weight(1f)
+                            imageVector = Icons.Default.Download,
+                            contentDescription = stringResource(R.string.action_download)
                         )
+                    }
+                    AnimatedVisibility(visible = isEditMode) {
+                        TopBarActionButton(
+                            onClick = {
+                                if (selectedBookUrls.isNotEmpty()) {
+                                    viewModel.showOverlay(BookshelfOverlay.GroupSelectSheet)
+                                }
+                            },
+                            imageVector = Icons.Default.Bookmarks,
+                            contentDescription = stringResource(R.string.move_to_group)
+                        )
+                    }
+                },
+                dropDownMenuContent = if (!isEditMode) {
+                    { dismiss ->
+                        RoundDropdownMenuItem(
+                            text = stringResource(R.string.add_remote_book),
+                            onClick = { onNavigateToRemoteImport(); dismiss() },
+                            leadingIcon = { Icon(Icons.Default.Wifi, null) }
+                        )
+                        RoundDropdownMenuItem(
+                            text = stringResource(R.string.book_local),
+                            onClick = { onNavigateToLocalImport(); dismiss() },
+                            leadingIcon = { Icon(Icons.Default.Save, null) }
+                        )
+                        RoundDropdownMenuItem(
+                            text = stringResource(R.string.update_toc),
+                            onClick = { viewModel.upToc(uiState.items); dismiss() },
+                            leadingIcon = { Icon(Icons.Default.Refresh, null) }
+                        )
+                        RoundDropdownMenuItem(
+                            text = stringResource(R.string.layout_setting),
+                            onClick = {
+                                viewModel.showOverlay(BookshelfOverlay.ConfigSheet)
+                                dismiss()
+                            },
+                            leadingIcon = { Icon(Icons.Default.GridView, null) }
+                        )
+                        RoundDropdownMenuItem(
+                            text = stringResource(R.string.group_manage),
+                            onClick = {
+                                viewModel.showOverlay(BookshelfOverlay.GroupManageSheet)
+                                dismiss()
+                            },
+                            leadingIcon = { Icon(Icons.Default.Edit, null) }
+                        )
+                        RoundDropdownMenuItem(
+                            text = stringResource(R.string.add_url),
+                            onClick = {
+                                viewModel.showOverlay(BookshelfOverlay.AddUrlDialog)
+                                dismiss()
+                            },
+                            leadingIcon = { Icon(Icons.Default.Link, null) }
+                        )
+                        RoundDropdownMenuItem(
+                            text = stringResource(R.string.edit),
+                            onClick = {
+                                toggleEditMode()
+                                dismiss()
+                            },
+                            leadingIcon = { Icon(Icons.Default.Edit, null) }
+                        )
+                        RoundDropdownMenuItem(
+                            text = stringResource(R.string.bookshelf_management),
+                            onClick = {
+                                val groupId =
+                                    uiState.groups.getOrNull(uiState.selectedGroupIndex)?.groupId ?: -1L
+                                onNavigateToCache(groupId)
+                                dismiss()
+                            },
+                            leadingIcon = { Icon(Icons.Default.Bookmarks, null) }
+                        )
+                        RoundDropdownMenuItem(
+                            text = stringResource(R.string.export_bookshelf),
+                            onClick = {
+                                viewModel.showOverlay(BookshelfOverlay.ExportSheet)
+                                dismiss()
+                            },
+                            leadingIcon = { Icon(Icons.Default.UploadFile, null) }
+                        )
+                        RoundDropdownMenuItem(
+                            text = stringResource(R.string.import_bookshelf),
+                            onClick = {
+                                viewModel.showOverlay(BookshelfOverlay.ImportSheet)
+                                dismiss()
+                            },
+                            leadingIcon = { Icon(Icons.Default.CloudDownload, null) }
+                        )
+                        RoundDropdownMenuItem(
+                            text = stringResource(R.string.log),
+                            onClick = {
+                                viewModel.showOverlay(BookshelfOverlay.LogSheet)
+                                dismiss()
+                            },
+                            leadingIcon = { Icon(Icons.Default.History, null) }
+                        )
+                    }
+                } else null,
+                bottomContent = if (bookGroupStyle == 0) {
+                    {
+                        if (uiState.groups.isNotEmpty()) {
+                            val selectedTabIndex =
+                                pagerState.currentPage.coerceIn(0, uiState.groups.size - 1)
+                            Row(
+                                modifier = Modifier
+                                    .fillMaxWidth()
+                                    .adaptiveHorizontalPaddingTab(),
+                                verticalAlignment = Alignment.CenterVertically
+                            ) {
+                                val tabTitles = remember(uiState.groups) {
+                                    uiState.groups.map { it.groupName }
+                                }
 
-                        if (BookshelfConfig.shouldShowExpandButton) {
-                            Box(modifier = Modifier) {
-                                SmallOutlinedIconToggleButton(
-                                    checked = showGroupMenu,
-                                    onCheckedChange = {
-                                        if (it) {
-                                            viewModel.showOverlay(BookshelfOverlay.GroupMenu)
-                                        } else {
-                                            viewModel.dismissOverlay()
-                                        }
+                                AppTabRow(
+                                    tabTitles = tabTitles,
+                                    selectedTabIndex = selectedTabIndex,
+                                    onTabSelected = { index ->
+                                        scope.launch { pagerState.animateScrollToPage(index) }
                                     },
-                                    imageVector = Icons.AutoMirrored.Filled.FormatListBulleted,
-                                    contentDescription = stringResource(R.string.group_manage)
+                                    modifier = Modifier.weight(1f)
                                 )
-                                RoundDropdownMenu(
-                                    expanded = showGroupMenu,
-                                    onDismissRequest = { viewModel.dismissOverlay() }
-                                ) { dismiss ->
-                                    uiState.groups.forEachIndexed { index, group ->
-                                        RoundDropdownMenuItem(
-                                            text = group.groupName,
-                                            onClick = {
-                                                if (uiState.isSearch) {
-                                                    viewModel.changeGroup(group.groupId)
-                                                }
-                                                scope.launch { pagerState.animateScrollToPage(index) }
-                                                dismiss()
-                                            },
-                                            trailingIcon = {
-                                                val isSelected = if (uiState.isSearch) {
-                                                    uiState.selectedGroupId == group.groupId
+
+                                if (BookshelfConfig.shouldShowExpandButton) {
+                                    Box(modifier = Modifier) {
+                                        SmallOutlinedIconToggleButton(
+                                            checked = showGroupMenu,
+                                            onCheckedChange = {
+                                                if (it) {
+                                                    viewModel.showOverlay(BookshelfOverlay.GroupMenu)
                                                 } else {
-                                                    selectedTabIndex == index
+                                                    viewModel.dismissOverlay()
                                                 }
-                                                if (isSelected) {
-                                                    Icon(
-                                                        Icons.Default.Check,
-                                                        null,
-                                                        modifier = Modifier.size(18.dp)
-                                                    )
-                                                }
-                                            }
+                                            },
+                                            imageVector = Icons.AutoMirrored.Filled.FormatListBulleted,
+                                            contentDescription = stringResource(R.string.group_manage)
                                         )
-                                    }
-
-                                    if (uiState.isSearch) {
-                                        val allGroup = uiState.allGroups.firstOrNull {
-                                            it.groupId == BookGroup.IdAll
-                                        }
-                                        val hiddenGroups = uiState.allGroups.filter {
-                                            !it.show && it.groupId != BookGroup.IdAll
-                                        }
-
-                                        if (allGroup != null || hiddenGroups.isNotEmpty()) {
-                                            PillHeaderDivider(
-                                                title = "${stringResource(R.string.all)} / ${stringResource(R.string.hide)}"
-                                            )
-
-                                            allGroup?.let { group ->
+                                        RoundDropdownMenu(
+                                            expanded = showGroupMenu,
+                                            onDismissRequest = { viewModel.dismissOverlay() }
+                                        ) { dismiss ->
+                                            uiState.groups.forEachIndexed { index, group ->
                                                 RoundDropdownMenuItem(
                                                     text = group.groupName,
                                                     onClick = {
-                                                        viewModel.changeGroup(group.groupId)
+                                                        if (uiState.isSearch) {
+                                                            viewModel.changeGroup(group.groupId)
+                                                        }
+                                                        scope.launch { pagerState.animateScrollToPage(index) }
                                                         dismiss()
                                                     },
                                                     trailingIcon = {
-                                                        if (uiState.selectedGroupId == group.groupId) {
+                                                        val isSelected = if (uiState.isSearch) {
+                                                            uiState.selectedGroupId == group.groupId
+                                                        } else {
+                                                            selectedTabIndex == index
+                                                        }
+                                                        if (isSelected) {
                                                             Icon(
                                                                 Icons.Default.Check,
                                                                 null,
@@ -605,23 +586,57 @@ fun BookshelfScreen(
                                                 )
                                             }
 
-                                            hiddenGroups.forEach { group ->
-                                                RoundDropdownMenuItem(
-                                                    text = group.groupName,
-                                                    onClick = {
-                                                        viewModel.changeGroup(group.groupId)
-                                                        dismiss()
-                                                    },
-                                                    trailingIcon = {
-                                                        if (uiState.selectedGroupId == group.groupId) {
-                                                            Icon(
-                                                                Icons.Default.Check,
-                                                                null,
-                                                                modifier = Modifier.size(18.dp)
-                                                            )
-                                                        }
+                                            if (uiState.isSearch) {
+                                                val allGroup = uiState.allGroups.firstOrNull {
+                                                    it.groupId == BookGroup.IdAll
+                                                }
+                                                val hiddenGroups = uiState.allGroups.filter {
+                                                    !it.show && it.groupId != BookGroup.IdAll
+                                                }
+
+                                                if (allGroup != null || hiddenGroups.isNotEmpty()) {
+                                                    PillHeaderDivider(
+                                                        title = "${stringResource(R.string.all)} / ${stringResource(R.string.hide)}"
+                                                    )
+
+                                                    allGroup?.let { group ->
+                                                        RoundDropdownMenuItem(
+                                                            text = group.groupName,
+                                                            onClick = {
+                                                                viewModel.changeGroup(group.groupId)
+                                                                dismiss()
+                                                            },
+                                                            trailingIcon = {
+                                                                if (uiState.selectedGroupId == group.groupId) {
+                                                                    Icon(
+                                                                        Icons.Default.Check,
+                                                                        null,
+                                                                        modifier = Modifier.size(18.dp)
+                                                                    )
+                                                                }
+                                                            }
+                                                        )
                                                     }
-                                                )
+
+                                                    hiddenGroups.forEach { group ->
+                                                        RoundDropdownMenuItem(
+                                                            text = group.groupName,
+                                                            onClick = {
+                                                                viewModel.changeGroup(group.groupId)
+                                                                dismiss()
+                                                            },
+                                                            trailingIcon = {
+                                                                if (uiState.selectedGroupId == group.groupId) {
+                                                                    Icon(
+                                                                        Icons.Default.Check,
+                                                                        null,
+                                                                        modifier = Modifier.size(18.dp)
+                                                                    )
+                                                                }
+                                                            }
+                                                        )
+                                                    }
+                                                }
                                             }
                                         }
                                     }
@@ -629,9 +644,9 @@ fun BookshelfScreen(
                             }
                         }
                     }
-                }
-            }
-        } else null
+                } else null
+            )
+        }
     ) { paddingValues ->
         val pullToRefreshState = rememberPullToRefreshState()
         val currentGroup by remember {
@@ -883,7 +898,7 @@ fun BookshelfScreen(
                             }
                         }
                     }
-                    
+
 
                     if (summary.showGroupName) {
                         RoundDropdownMenu(
@@ -934,6 +949,41 @@ fun BookshelfScreen(
                     .align(Alignment.TopCenter)
                     .padding(top = paddingValues.calculateTopPadding())
             )
+
+            AnimatedVisibility(
+                visible = isEditMode && selectedBookUrls.isNotEmpty(),
+                modifier = Modifier
+                    .align(Alignment.BottomCenter)
+                    .padding(bottom = 16.dp + ScreenOffset)
+                    .zIndex(1f),
+                enter = slideInVertically { it } + fadeIn(),
+                exit = slideOutVertically { it } + fadeOut()
+            ) {
+                SelectionBottomBar(
+                    onSelectAll = { viewModel.selectAllVisible() },
+                    onSelectInvert = { viewModel.invertVisibleSelection() },
+                    primaryAction = ActionItem(
+                        text = stringResource(R.string.action_download),
+                        icon = { Icon(Icons.Default.Download, contentDescription = null) },
+                        onClick = {
+                            if (selectedBookUrls.isNotEmpty()) {
+                                viewModel.showOverlay(BookshelfOverlay.BatchDownloadConfirmDialog)
+                            }
+                        }
+                    ),
+                    secondaryActions = listOf(
+                        ActionItem(
+                            text = stringResource(R.string.move_to_group),
+                            icon = { Icon(Icons.Default.Bookmarks, contentDescription = null) },
+                            onClick = {
+                                if (selectedBookUrls.isNotEmpty()) {
+                                    viewModel.showOverlay(BookshelfOverlay.GroupSelectSheet)
+                                }
+                            }
+                        )
+                    )
+                )
+            }
         }
     }
 
@@ -972,7 +1022,7 @@ private fun BookshelfOverlays(
 
     GroupSelectSheet(
         show = activeOverlay == BookshelfOverlay.GroupSelectSheet,
-        groups = groups,
+        groups = groups.filter { it.groupId > 0 },
         currentGroupId = 0L,
         onDismissRequest = { viewModel.dismissOverlay() },
         onConfirm = { groupId ->

+ 1 - 2
app/src/main/java/io/legado/app/ui/main/my/MyScreen.kt

@@ -47,7 +47,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import io.legado.app.R
 import io.legado.app.ui.about.AboutActivity
 import io.legado.app.ui.book.bookmark.AllBookmarkActivity
-import io.legado.app.ui.book.readRecord.ReadRecordActivity
 import io.legado.app.ui.book.source.manage.BookSourceActivity
 import io.legado.app.ui.book.toc.rule.TxtTocRuleActivity
 import io.legado.app.ui.dict.rule.DictRuleActivity
@@ -186,7 +185,7 @@ fun MyScreen(
                     title = stringResource(R.string.read_record),
                     imageVector = Icons.Default.History,
                     onClick = {
-                        onNavigate(PrefClickEvent.StartActivity(ReadRecordActivity::class.java))
+                        onNavigate(PrefClickEvent.OpenReadRecord)
                     }
                 )
                 ClickableSettingItem(

+ 1 - 0
app/src/main/java/io/legado/app/ui/main/my/MyViewModel.kt

@@ -22,6 +22,7 @@ sealed class PrefClickEvent {
     data class CopyUrl(val url: String) : PrefClickEvent()
     data class ShowMd(val title: String, val path: String) : PrefClickEvent()
     data class StartActivity(val destination: Class<*>, val configTag: String? = null) : PrefClickEvent()
+    object OpenReadRecord : PrefClickEvent()
     object OpenBookCacheManage : PrefClickEvent()
     object ToggleWebService : PrefClickEvent()
     object ExitApp : PrefClickEvent()

+ 1 - 1
app/src/main/java/io/legado/app/ui/widget/components/card/GlassCard.kt

@@ -39,7 +39,7 @@ private fun BaseCard(
     alpha: Float = 1f,
     content: @Composable ColumnScope.() -> Unit
 ) {
-    val resolvedContainerColor = (containerColor ?: LegadoTheme.colorScheme.secondaryContainer)
+    val resolvedContainerColor = (containerColor ?: LegadoTheme.colorScheme.surfaceContainer)
         .let { it.copy(alpha = it.alpha * alpha) }
     val isTransparent = containerColor == Color.Transparent
     val shape = RoundedCornerShape(cornerRadius)

+ 2 - 1
app/src/main/java/io/legado/app/ui/widget/components/settingItem/CompactSettingItems.kt

@@ -262,6 +262,7 @@ fun CompactClickableSettingItem(
     imageVector: ImageVector? = null,
     color: Color? = MaterialTheme.colorScheme.surface,
     shape: Shape = MaterialTheme.shapes.small,
+    trailingContent: (@Composable () -> Unit)? = null,
     onClick: () -> Unit
 ) {
     if (ThemeResolver.isMiuixEngine(composeEngine)) {
@@ -278,7 +279,7 @@ fun CompactClickableSettingItem(
             imageVector = imageVector,
             color = color,
             shape = shape,
-            trailingContent = {
+            trailingContent = trailingContent ?: {
                 Icon(
                     imageVector = Icons.Default.ChevronRight,
                     contentDescription = null,

+ 6 - 1
app/src/main/res/values-zh-rCN/strings.xml

@@ -774,7 +774,12 @@
     <string name="auto_page_speed">自动翻页速度</string>
     <string name="sort_by_url">地址排序</string>
     <string name="backup_summary">本地和 WebDav 一起备份</string>
-    <string name="restore_summary">优先从 WebDav 恢复,长按从本地恢复</string>
+    <string name="backup_sync_mode">自动备份同步</string>
+    <string name="backup_sync_mode_summary">选择自动备份时的同步行为</string>
+    <string name="backup_to_local_and_network">备份至本地与网络</string>
+    <string name="backup_to_local">备份至本地</string>
+    <string name="backup_to_network">备份至网络</string>
+    <string name="restore_summary">选择从本地或网络恢复备份</string>
     <string name="import_old_summary">选择旧版备份文件夹</string>
     <string name="enabled">已启用</string>
     <string name="disabled">已禁用</string>

+ 6 - 1
app/src/main/res/values-zh-rHK/strings.xml

@@ -737,7 +737,12 @@
     <string name="other">其它</string>
     <string name="official_account">legado-top</string>
     <string name="backup_summary">本地和WebDav壹起備份</string>
-    <string name="restore_summary">優先從WebDav恢復,長按從本地恢復</string>
+    <string name="backup_sync_mode">自動備份同步</string>
+    <string name="backup_sync_mode_summary">選擇自動備份時的同步行為</string>
+    <string name="backup_to_local_and_network">備份至本地與網絡</string>
+    <string name="backup_to_local">備份至本地</string>
+    <string name="backup_to_network">備份至網絡</string>
+    <string name="restore_summary">選擇從本地或網絡恢復備份</string>
     <string name="import_old_summary">選擇舊版備份文件夾</string>
     <string name="enabled">已啓用</string>
     <string name="disabled">已禁用</string>

+ 6 - 1
app/src/main/res/values-zh-rTW/strings.xml

@@ -746,7 +746,12 @@
     <string name="auto_page_speed">自動翻頁速度</string>
     <string name="sort_by_url">地址排序</string>
     <string name="backup_summary">本機和 WebDav 一起備份</string>
-    <string name="restore_summary">優先從 WebDav 復原,長按從本機復原</string>
+    <string name="backup_sync_mode">自動備份同步</string>
+    <string name="backup_sync_mode_summary">選擇自動備份時的同步行為</string>
+    <string name="backup_to_local_and_network">備份至本地與網路</string>
+    <string name="backup_to_local">備份至本地</string>
+    <string name="backup_to_network">備份至網路</string>
+    <string name="restore_summary">選擇從本地或網路恢復備份</string>
     <string name="import_old_summary">選擇舊版備份資料夾</string>
     <string name="enabled">已啟用</string>
     <string name="disabled">已禁用</string>

+ 6 - 0
app/src/main/res/values/array_values.xml

@@ -141,4 +141,10 @@
         <item>High</item>
     </string-array>
 
+    <string-array name="backup_sync_mode_value">
+        <item>both</item>
+        <item>local</item>
+        <item>webdav</item>
+    </string-array>
+
 </resources>

+ 6 - 0
app/src/main/res/values/arrays.xml

@@ -324,5 +324,11 @@
 		<item>Maximum</item>
 	</string-array>
 
+	<string-array name="backup_sync_mode">
+		<item>@string/backup_to_local_and_network</item>
+		<item>@string/backup_to_local</item>
+		<item>@string/backup_to_network</item>
+	</string-array>
+
 	<string name="all_version">All Version</string>
 </resources>

+ 6 - 1
app/src/main/res/values/strings.xml

@@ -804,7 +804,12 @@
     <string name="auto_page_speed">Auto scroll speed</string>
     <string name="sort_by_url">Sort by URL</string>
     <string name="backup_summary">Backup the local and WebDAV simultaneously</string>
-    <string name="restore_summary">Restore from WebDAV first, Restore form the local backup on long click</string>
+    <string name="backup_sync_mode">Auto Backup Sync</string>
+    <string name="backup_sync_mode_summary">Choose sync behavior for automatic backup</string>
+    <string name="backup_to_local_and_network">Backup to Local &amp; Network</string>
+    <string name="backup_to_local">Backup to Local</string>
+    <string name="backup_to_network">Backup to Network</string>
+    <string name="restore_summary">Choose to restore from local or network backup</string>
     <string name="import_old_summary">Select a legacy backup folder</string>
     <string name="enabled">Enabled</string>
     <string name="disabled">Disabled</string>