HapeLee 5 месяцев назад
Родитель
Сommit
e64e50bf38
23 измененных файлов с 1436 добавлено и 1274 удалено
  1. 0 10
      app/src/main/AndroidManifest.xml
  2. 1 0
      app/src/main/java/io/legado/app/constant/PreferKey.kt
  3. 26 26
      app/src/main/java/io/legado/app/data/dao/BookDao.kt
  4. 12 8
      app/src/main/java/io/legado/app/di/appModule.kt
  5. 8 3
      app/src/main/java/io/legado/app/ui/config/bookshelfConfig/BookshelfConfig.kt
  6. 77 876
      app/src/main/java/io/legado/app/ui/main/MainActivity.kt
  7. 152 0
      app/src/main/java/io/legado/app/ui/main/MainIntent.kt
  8. 423 0
      app/src/main/java/io/legado/app/ui/main/MainNavGraph.kt
  9. 106 0
      app/src/main/java/io/legado/app/ui/main/MainNavKey.kt
  10. 253 0
      app/src/main/java/io/legado/app/ui/main/MainNavigator.kt
  11. 5 1
      app/src/main/java/io/legado/app/ui/main/MainScreen.kt
  12. 100 77
      app/src/main/java/io/legado/app/ui/main/bookshelf/BookItem.kt
  13. 173 115
      app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfConfigSheet.kt
  14. 2 2
      app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt
  15. 7 5
      app/src/main/java/io/legado/app/ui/main/rss/RssScreen.kt
  16. 0 52
      app/src/main/java/io/legado/app/ui/rss/favorites/RssFavoritesActivity.kt
  17. 2 2
      app/src/main/java/io/legado/app/ui/rss/favorites/RssFavoritesScreen.kt
  18. 0 23
      app/src/main/java/io/legado/app/ui/rss/subscription/RuleSubActivity.kt
  19. 3 3
      app/src/main/java/io/legado/app/ui/rss/subscription/RuleSubScreen.kt
  20. 2 2
      app/src/main/java/io/legado/app/ui/theme/AdaptivePadding.kt
  21. 69 60
      app/src/main/java/io/legado/app/ui/widget/components/book/SearchBookItem.kt
  22. 11 5
      app/src/main/java/io/legado/app/ui/widget/components/divider/PillDivider.kt
  23. 4 4
      app/src/main/java/io/legado/app/ui/widget/components/settingItem/CompactSettingItems.kt

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

@@ -193,11 +193,6 @@
             android:name=".ui.qrcode.QrCodeActivity"
             android:enableOnBackInvokedCallback="true"
             android:launchMode="singleTask" />
-        <!-- 规则订阅 -->
-        <activity
-            android:name=".ui.rss.subscription.RuleSubActivity"
-            android:enableOnBackInvokedCallback="true"
-            android:launchMode="singleTask" />
         <!-- 书源编辑 -->
         <activity
             android:name=".ui.book.source.edit.BookSourceEditActivity"
@@ -268,11 +263,6 @@
             android:name=".ui.rss.source.debug.RssSourceDebugActivity"
             android:enableOnBackInvokedCallback="true"
             android:launchMode="singleTop" />
-        <!-- Rss收藏 -->
-        <activity
-            android:name=".ui.rss.favorites.RssFavoritesActivity"
-            android:enableOnBackInvokedCallback="true"
-            android:launchMode="singleTop" />
         <!-- 书签 -->
         <activity
             android:name=".ui.book.bookmark.AllBookmarkActivity"

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

@@ -271,6 +271,7 @@ object PreferKey {
     const val bookshelfCoverShadow = "bookshelfCoverShadow"
     const val bookshelfSearchActionDirectToSearch = "bookshelfSearchActionDirectToSearch"
     const val bookshelfCardColor = "bookshelfCardColor"
+    const val bookshelfCardColorDark = "bookshelfCardColorDark"
     const val bookshelfGroupListStyle = "bookshelfGroupListStyle"
     const val bookshelfGroupCoverCount = "bookshelfGroupCoverCount"
 

+ 26 - 26
app/src/main/java/io/legado/app/data/dao/BookDao.kt

@@ -723,8 +723,8 @@ interface BookDao {
             type, `group`, `order`, canUpdate,
             ifnull(customIntro, intro) as intro, kind
         FROM books
-        ORDER BY CASE WHEN (coverUrl IS NOT NULL OR customCoverUrl IS NOT NULL) THEN 0 ELSE 1 END, durChapterTime DESC
-        LIMIT 4
+        ORDER BY durChapterTime DESC
+        LIMIT 10
         """
     )
     fun flowBookShelfPreview(): Flow<List<BookShelfItem>>
@@ -741,8 +741,8 @@ interface BookDao {
         WHERE type & ${BookType.text} > 0 AND type & ${BookType.local} = 0
             AND ((SELECT COALESCE(SUM(groupId), 0) FROM book_groups WHERE groupId > 0) & `group`) = 0
             AND (SELECT show FROM book_groups WHERE groupId = ${BookGroup.IdNetNone}) != 1
-        ORDER BY CASE WHEN (coverUrl IS NOT NULL OR customCoverUrl IS NOT NULL) THEN 0 ELSE 1 END, durChapterTime DESC
-        LIMIT 4
+        ORDER BY durChapterTime DESC
+        LIMIT 10
         """
     )
     fun flowBookShelfRootPreview(): Flow<List<BookShelfItem>>
@@ -757,8 +757,8 @@ interface BookDao {
             ifnull(customIntro, intro) as intro, kind
         FROM books
         WHERE type & ${BookType.local} > 0
-        ORDER BY CASE WHEN (coverUrl IS NOT NULL OR customCoverUrl IS NOT NULL) THEN 0 ELSE 1 END, durChapterTime DESC
-        LIMIT 4
+        ORDER BY durChapterTime DESC
+        LIMIT 10
         """
     )
     fun flowBookShelfLocalPreview(): Flow<List<BookShelfItem>>
@@ -773,8 +773,8 @@ interface BookDao {
             ifnull(customIntro, intro) as intro, kind
         FROM books
         WHERE type & ${BookType.audio} > 0
-        ORDER BY CASE WHEN (coverUrl IS NOT NULL OR customCoverUrl IS NOT NULL) THEN 0 ELSE 1 END, durChapterTime DESC
-        LIMIT 4
+        ORDER BY durChapterTime DESC
+        LIMIT 10
         """
     )
     fun flowBookShelfAudioPreview(): Flow<List<BookShelfItem>>
@@ -790,8 +790,8 @@ interface BookDao {
         FROM books
         WHERE type & ${BookType.audio} = 0 AND type & ${BookType.local} = 0
             AND ((SELECT COALESCE(SUM(groupId), 0) FROM book_groups WHERE groupId > 0) & `group`) = 0
-        ORDER BY CASE WHEN (coverUrl IS NOT NULL OR customCoverUrl IS NOT NULL) THEN 0 ELSE 1 END, durChapterTime DESC
-        LIMIT 4
+        ORDER BY durChapterTime DESC
+        LIMIT 10
         """
     )
     fun flowBookShelfNetNoGroupPreview(): Flow<List<BookShelfItem>>
@@ -807,8 +807,8 @@ interface BookDao {
         FROM books
         WHERE type & ${BookType.local} > 0
             AND ((SELECT COALESCE(SUM(groupId), 0) FROM book_groups WHERE groupId > 0) & `group`) = 0
-        ORDER BY CASE WHEN (coverUrl IS NOT NULL OR customCoverUrl IS NOT NULL) THEN 0 ELSE 1 END, durChapterTime DESC
-        LIMIT 4
+        ORDER BY durChapterTime DESC
+        LIMIT 10
         """
     )
     fun flowBookShelfLocalNoGroupPreview(): Flow<List<BookShelfItem>>
@@ -823,8 +823,8 @@ interface BookDao {
             ifnull(customIntro, intro) as intro, kind
         FROM books
         WHERE type & ${BookType.image} > 0
-        ORDER BY CASE WHEN (coverUrl IS NOT NULL OR customCoverUrl IS NOT NULL) THEN 0 ELSE 1 END, durChapterTime DESC
-        LIMIT 4
+        ORDER BY durChapterTime DESC
+        LIMIT 10
         """
     )
     fun flowBookShelfMangaPreview(): Flow<List<BookShelfItem>>
@@ -839,8 +839,8 @@ interface BookDao {
             ifnull(customIntro, intro) as intro, kind
         FROM books
         WHERE type & ${BookType.text} > 0
-        ORDER BY CASE WHEN (coverUrl IS NOT NULL OR customCoverUrl IS NOT NULL) THEN 0 ELSE 1 END, durChapterTime DESC
-        LIMIT 4
+        ORDER BY durChapterTime DESC
+        LIMIT 10
         """
     )
     fun flowBookShelfTextPreview(): Flow<List<BookShelfItem>>
@@ -855,8 +855,8 @@ interface BookDao {
             ifnull(customIntro, intro) as intro, kind
         FROM books
         WHERE type & ${BookType.updateError} > 0
-        ORDER BY CASE WHEN (coverUrl IS NOT NULL OR customCoverUrl IS NOT NULL) THEN 0 ELSE 1 END, durChapterTime DESC
-        LIMIT 4
+        ORDER BY durChapterTime DESC
+        LIMIT 10
         """
     )
     fun flowBookShelfUpdateErrorPreview(): Flow<List<BookShelfItem>>
@@ -871,8 +871,8 @@ interface BookDao {
             ifnull(customIntro, intro) as intro, kind
         FROM books
         WHERE durChapterIndex = 0 AND durChapterPos = 0
-        ORDER BY CASE WHEN (coverUrl IS NOT NULL OR customCoverUrl IS NOT NULL) THEN 0 ELSE 1 END, durChapterTime DESC
-        LIMIT 4
+        ORDER BY durChapterTime DESC
+        LIMIT 10
         """
     )
     fun flowBookShelfUnreadPreview(): Flow<List<BookShelfItem>>
@@ -887,8 +887,8 @@ interface BookDao {
             ifnull(customIntro, intro) as intro, kind
         FROM books
         WHERE totalChapterNum > 0 AND durChapterIndex > 0 AND durChapterIndex < totalChapterNum - 1
-        ORDER BY CASE WHEN (coverUrl IS NOT NULL OR customCoverUrl IS NOT NULL) THEN 0 ELSE 1 END, durChapterTime DESC
-        LIMIT 4
+        ORDER BY durChapterTime DESC
+        LIMIT 10
         """
     )
     fun flowBookShelfReadingPreview(): Flow<List<BookShelfItem>>
@@ -903,8 +903,8 @@ interface BookDao {
             ifnull(customIntro, intro) as intro, kind
         FROM books
         WHERE totalChapterNum > 0 AND durChapterIndex >= totalChapterNum - 1
-        ORDER BY CASE WHEN (coverUrl IS NOT NULL OR customCoverUrl IS NOT NULL) THEN 0 ELSE 1 END, durChapterTime DESC
-        LIMIT 4
+        ORDER BY durChapterTime DESC
+        LIMIT 10
         """
     )
     fun flowBookShelfReadFinishedPreview(): Flow<List<BookShelfItem>>
@@ -919,8 +919,8 @@ interface BookDao {
             ifnull(customIntro, intro) as intro, kind
         FROM books
         WHERE (`group` & :groupId) > 0
-        ORDER BY CASE WHEN (coverUrl IS NOT NULL OR customCoverUrl IS NOT NULL) THEN 0 ELSE 1 END, durChapterTime DESC
-        LIMIT 4
+        ORDER BY durChapterTime DESC
+        LIMIT 10
         """
     )
     fun flowBookShelfPreviewByUserGroup(groupId: Long): Flow<List<BookShelfItem>>

+ 12 - 8
app/src/main/java/io/legado/app/di/appModule.kt

@@ -14,23 +14,23 @@ import io.legado.app.data.repository.BookRepository
 import io.legado.app.data.repository.BookSourceCallbackRepository
 import io.legado.app.data.repository.CacheBookDownloadRepository
 import io.legado.app.data.repository.DatabaseMaintenanceRepository
-import io.legado.app.data.repository.DirectLinkUploadRepository
 import io.legado.app.data.repository.DictRuleRepository
+import io.legado.app.data.repository.DirectLinkUploadRepository
 import io.legado.app.data.repository.ExploreRepository
 import io.legado.app.data.repository.ExploreRepositoryImpl
 import io.legado.app.data.repository.LocalBookRepository
 import io.legado.app.data.repository.ReadRecordRepository
 import io.legado.app.data.repository.RemoteBookRepository
 import io.legado.app.data.repository.RssRepository
+import io.legado.app.data.repository.SearchContentRepository
 import io.legado.app.data.repository.SearchRepository
 import io.legado.app.data.repository.SearchRepositoryImpl
-import io.legado.app.data.repository.SearchContentRepository
 import io.legado.app.data.repository.SettingsRepository
 import io.legado.app.data.repository.UploadRepository
 import io.legado.app.data.repository.WebDavBackupRepository
 import io.legado.app.data.repository.WebDavReadingProgressRepository
-import io.legado.app.domain.gateway.BookCacheCleanupGateway
 import io.legado.app.domain.gateway.AppStartupGateway
+import io.legado.app.domain.gateway.BookCacheCleanupGateway
 import io.legado.app.domain.gateway.BookCacheDownloadGateway
 import io.legado.app.domain.gateway.BookSearchGateway
 import io.legado.app.domain.gateway.BookSourceCallbackGateway
@@ -47,14 +47,13 @@ import io.legado.app.domain.usecase.ClearBookCacheUseCase
 import io.legado.app.domain.usecase.DeleteBooksUseCase
 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
 import io.legado.app.domain.usecase.UploadReadingProgressUseCase
 import io.legado.app.domain.usecase.WebDavBackupUseCase
+import io.legado.app.domain.usecase.readRecord.GetReadRecordOverviewUseCase
 import io.legado.app.help.coil.CoverFetcher
 import io.legado.app.help.coil.CoverInterceptor
 import io.legado.app.help.http.okHttpClient
@@ -64,7 +63,6 @@ import io.legado.app.ui.book.cache.manage.BookCacheManageViewModel
 import io.legado.app.ui.book.changecover.ChangeCoverViewModel
 import io.legado.app.ui.book.changesource.ChangeBookSourceComposeViewModel
 import io.legado.app.ui.book.changesource.ChangeBookSourceViewModel
-import io.legado.app.ui.book.manage.BookshelfManageScreenViewModel
 import io.legado.app.ui.book.explore.ExploreShowViewModel
 import io.legado.app.ui.book.group.GroupViewModel
 import io.legado.app.ui.book.import.local.ImportBookViewModel
@@ -72,10 +70,11 @@ import io.legado.app.ui.book.import.remote.RemoteBookViewModel
 import io.legado.app.ui.book.import.remote.ServerConfigViewModel
 import io.legado.app.ui.book.import.remote.ServersViewModel
 import io.legado.app.ui.book.info.BookInfoViewModel
+import io.legado.app.ui.book.manage.BookshelfManageScreenViewModel
 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.readRecord.ReadRecordViewModel
 import io.legado.app.ui.book.search.SearchViewModel
 import io.legado.app.ui.book.searchContent.SearchContentViewModel
 import io.legado.app.ui.book.toc.TocViewModel
@@ -97,10 +96,13 @@ import io.legado.app.ui.main.rss.RssViewModel
 import io.legado.app.ui.replace.ReplaceEditRoute
 import io.legado.app.ui.replace.ReplaceRuleViewModel
 import io.legado.app.ui.replace.edit.ReplaceEditViewModel
-import io.legado.app.ui.rss.source.manage.RssSourceViewModel
 import io.legado.app.ui.rss.article.RssArticlesViewModel
 import io.legado.app.ui.rss.article.RssSortViewModel
+import io.legado.app.ui.rss.favorites.RssFavoritesViewModel
 import io.legado.app.ui.rss.read.ReadRssViewModel
+import io.legado.app.ui.rss.source.manage.RssSourceViewModel
+import io.legado.app.ui.rss.subscription.RuleSubViewModel
+import io.legado.app.ui.widget.components.explore.ExploreKindUiUseCase
 import org.koin.core.module.dsl.singleOf
 import org.koin.core.module.dsl.viewModel
 import org.koin.core.module.dsl.viewModelOf
@@ -179,6 +181,8 @@ val appModule = module {
     viewModelOf(::RssSortViewModel)
     viewModelOf(::RssArticlesViewModel)
     viewModelOf(::ReadRssViewModel)
+    viewModelOf(::RssFavoritesViewModel)
+    viewModelOf(::RuleSubViewModel)
     viewModelOf(::ReadRecordViewModel)
     viewModelOf(::ReadRecordOverviewViewModel)
     viewModelOf(::ExploreShowViewModel)

+ 8 - 3
app/src/main/java/io/legado/app/ui/config/bookshelfConfig/BookshelfConfig.kt

@@ -269,12 +269,17 @@ object BookshelfConfig {
     /**
      * 书架卡片背景颜色
      */
-    private val _bookshelfCardColor = prefStateDelegate(PreferKey.bookshelfCardColor, 0) {
-        postEvent(EventBus.NOTIFY_MAIN, false)
-    }
+    private val _bookshelfCardColor = prefStateDelegate(PreferKey.bookshelfCardColor, 0)
     var bookshelfCardColor by _bookshelfCardColor
     val bookshelfCardColorState: State<Int> get() = _bookshelfCardColor.state
 
+    /**
+     * 书架卡片背景颜色 (夜间)
+     */
+    private val _bookshelfCardColorDark = prefStateDelegate(PreferKey.bookshelfCardColorDark, 0)
+    var bookshelfCardColorDark by _bookshelfCardColorDark
+    val bookshelfCardColorDarkState: State<Int> get() = _bookshelfCardColorDark.state
+
     /**
      * 文件夹在列表模式下的样式: 0: 默认, 2: 横排封面
      */

+ 77 - 876
app/src/main/java/io/legado/app/ui/main/MainActivity.kt

@@ -15,18 +15,12 @@ import androidx.compose.animation.fadeIn
 import androidx.compose.animation.fadeOut
 import androidx.compose.animation.scaleOut
 import androidx.compose.animation.togetherWith
-import androidx.compose.runtime.DisposableEffect
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.LifecycleEventObserver
-import androidx.lifecycle.compose.LocalLifecycleOwner
 import androidx.lifecycle.lifecycleScope
 import androidx.navigation3.runtime.NavKey
-import androidx.navigation3.runtime.entryProvider
 import androidx.navigation3.runtime.rememberNavBackStack
-import androidx.navigation3.ui.LocalNavAnimatedContentScope
 import androidx.navigation3.ui.NavDisplay
 import io.legado.app.BuildConfig
 import io.legado.app.R
@@ -43,49 +37,18 @@ import io.legado.app.lib.dialogs.alert
 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.cache.manage.BookCacheManageRouteScreen
-import io.legado.app.ui.book.explore.ExploreShowScreen
-import io.legado.app.ui.book.info.BookInfoRouteScreen
-import io.legado.app.ui.book.info.BookInfoViewModel
-import io.legado.app.ui.book.import.local.ImportBookScreen
-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
-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.downloadCacheConfig.DownloadCacheConfigScreen
 import io.legado.app.ui.config.themeConfig.ThemeConfig
-import io.legado.app.ui.config.otherConfig.OtherConfigScreen
-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.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
-import io.legado.app.ui.rss.read.RssReadRouteScreen
 import io.legado.app.ui.welcome.WelcomeActivity
 import io.legado.app.ui.widget.dialog.TextDialog
 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
 import kotlinx.coroutines.withContext
-import kotlinx.serialization.Serializable
-import org.koin.androidx.compose.koinViewModel
 import org.koin.androidx.viewmodel.ext.android.viewModel
 import kotlin.coroutines.resume
 import kotlin.coroutines.suspendCoroutine
@@ -96,81 +59,19 @@ import kotlin.coroutines.suspendCoroutine
 open class MainActivity : BaseComposeActivity(), VariableDialog.Callback {
 
     companion object {
-        const val EXTRA_START_ROUTE = "startRoute"
-        private const val ROUTE_MAIN = "main"
-        private const val ROUTE_SETTINGS = "settings"
-        private const val ROUTE_SETTINGS_OTHER = "settings/other"
-        private const val ROUTE_SETTINGS_READ = "settings/read"
-        private const val ROUTE_SETTINGS_COVER = "settings/cover"
-        private const val ROUTE_SETTINGS_THEME = "settings/theme"
-        private const val ROUTE_SETTINGS_BACKUP = "settings/backup"
-        private const val ROUTE_SETTINGS_CUSTOM_THEME = "settings/custom_theme"
-        private const val ROUTE_SETTINGS_DOWNLOAD_CACHE = "settings/download_cache"
-        private const val ROUTE_IMPORT_LOCAL = "import/local"
-        private const val ROUTE_IMPORT_REMOTE = "import/remote"
-        private const val ROUTE_CACHE = "cache"
-        private const val ROUTE_BOOK_CACHE_MANAGE = "book/cache/manage"
-        private const val ROUTE_SEARCH = "search"
-        private const val ROUTE_BOOK_INFO = "book/info"
-        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"
-        private const val EXTRA_BOOK_NAME = "name"
-        private const val EXTRA_BOOK_AUTHOR = "author"
-        private const val EXTRA_BOOK_URL = "bookUrl"
-        private const val EXTRA_EXPLORE_NAME = "exploreName"
-        private const val EXTRA_SOURCE_URL = "sourceUrl"
-        private const val EXTRA_EXPLORE_URL = "exploreUrl"
+        fun createLauncherIntent(context: Context): Intent =
+            MainIntent.createLauncherIntent(context)
 
-        private const val EXTRA_RSS_SOURCE_URL = "extra_rss_source_url"
-        private const val EXTRA_RSS_SORT_URL = "extra_rss_sort_url"
-        private const val EXTRA_RSS_KEY = "extra_rss_key"
-
-        private const val EXTRA_RSS_READ_TITLE = "extra_rss_read_title"
-        private const val EXTRA_RSS_READ_ORIGIN = "extra_rss_read_origin"
-        private const val EXTRA_RSS_READ_LINK = "extra_rss_read_link"
-        private const val EXTRA_RSS_READ_OPEN_URL = "extra_rss_read_open_url"
-
-        fun createLauncherIntent(context: Context): Intent {
-            val launcherComponent =
-                context.packageManager.getLaunchIntentForPackage(context.packageName)?.component
-            return if (launcherComponent != null) {
-                Intent().setComponent(launcherComponent)
-            } else {
-                Intent(context, MainActivity::class.java)
-            }
-        }
-
-        fun createHomeIntent(context: Context): Intent {
-            return createLauncherIntent(context).apply {
-                putExtra(EXTRA_START_ROUTE, ROUTE_MAIN)
-            }
-        }
-
-        fun createIntent(context: Context, configTag: String? = null): Intent {
-            return createLauncherIntent(context).apply {
-                putExtra(EXTRA_START_ROUTE, routeForConfigTag(configTag))
-            }
-        }
+        fun createHomeIntent(context: Context): Intent = MainIntent.createHomeIntent(context)
+        fun createIntent(context: Context, configTag: String? = null): Intent =
+            MainIntent.createIntent(context, configTag)
 
         fun createRssSortIntent(
             context: Context,
             sourceUrl: String,
             sortUrl: String? = null,
             key: String? = null
-        ): Intent {
-            return createLauncherIntent(context).apply {
-                putExtra(EXTRA_START_ROUTE, ROUTE_RSS_SORT)
-                putExtra(EXTRA_RSS_SOURCE_URL, sourceUrl)
-                putExtra(EXTRA_RSS_SORT_URL, sortUrl)
-                putExtra(EXTRA_RSS_KEY, key)
-            }
-        }
+        ): Intent = MainIntent.createRssSortIntent(context, sourceUrl, sortUrl, key)
 
         fun createRssReadIntent(
             context: Context,
@@ -178,165 +79,42 @@ open class MainActivity : BaseComposeActivity(), VariableDialog.Callback {
             origin: String,
             link: String? = null,
             openUrl: String? = null
-        ): Intent {
-            return createLauncherIntent(context).apply {
-                putExtra(EXTRA_START_ROUTE, ROUTE_RSS_READ)
-                putExtra(EXTRA_RSS_READ_TITLE, title)
-                putExtra(EXTRA_RSS_READ_ORIGIN, origin)
-                putExtra(EXTRA_RSS_READ_LINK, link)
-                putExtra(EXTRA_RSS_READ_OPEN_URL, openUrl)
-            }
-        }
+        ): Intent = MainIntent.createRssReadIntent(context, title, origin, link, openUrl)
 
-        fun createBookshelfManageScreenIntent(
-            context: Context,
-            groupId: Long = -1L
-        ): Intent {
-            return createLauncherIntent(context).apply {
-                putExtra(EXTRA_START_ROUTE, ROUTE_CACHE)
-                putExtra(EXTRA_CACHE_GROUP_ID, groupId)
-            }
-        }
+        fun createBookshelfManageScreenIntent(context: Context, groupId: Long = -1L): Intent =
+            MainIntent.createBookshelfManageScreenIntent(context, groupId)
 
-        fun createCacheIntent(
-            context: Context,
-            groupId: Long = -1L
-        ): Intent = createBookshelfManageScreenIntent(context, groupId)
+        fun createCacheIntent(context: Context, groupId: Long = -1L): Intent =
+            MainIntent.createCacheIntent(context, groupId)
 
-        fun createBookCacheManageIntent(context: Context): Intent {
-            return createLauncherIntent(context).apply {
-                putExtra(EXTRA_START_ROUTE, ROUTE_BOOK_CACHE_MANAGE)
-            }
-        }
+        fun createBookCacheManageIntent(context: Context): Intent =
+            MainIntent.createBookCacheManageIntent(context)
 
         fun createSearchIntent(
             context: Context,
             key: String? = null,
             scopeRaw: String? = null
-        ): Intent {
-            return createLauncherIntent(context).apply {
-                putExtra(EXTRA_START_ROUTE, ROUTE_SEARCH)
-                putExtra(EXTRA_SEARCH_KEY, key)
-                putExtra(EXTRA_SEARCH_SCOPE, scopeRaw)
-            }
-        }
+        ): Intent = MainIntent.createSearchIntent(context, key, scopeRaw)
 
         fun createBookInfoIntent(
             context: Context,
             name: String? = null,
             author: String? = null,
             bookUrl: String
-        ): Intent {
-            return createLauncherIntent(context).apply {
-                putExtra(EXTRA_START_ROUTE, ROUTE_BOOK_INFO)
-                putExtra(EXTRA_BOOK_NAME, name)
-                putExtra(EXTRA_BOOK_AUTHOR, author)
-                putExtra(EXTRA_BOOK_URL, bookUrl)
-            }
-        }
+        ): Intent = MainIntent.createBookInfoIntent(context, name, author, bookUrl)
 
         fun createExploreShowIntent(
             context: Context,
             exploreName: String? = null,
             sourceUrl: String,
             exploreUrl: String? = null
-        ): Intent {
-            return createLauncherIntent(context).apply {
-                putExtra(EXTRA_START_ROUTE, ROUTE_EXPLORE_SHOW)
-                putExtra(EXTRA_EXPLORE_NAME, exploreName)
-                putExtra(EXTRA_SOURCE_URL, sourceUrl)
-                putExtra(EXTRA_EXPLORE_URL, exploreUrl)
-            }
-        }
-
-        private fun routeForConfigTag(configTag: String?): String {
-            return when (configTag) {
-                ConfigTag.OTHER_CONFIG -> ROUTE_SETTINGS_OTHER
-                ConfigTag.READ_CONFIG -> ROUTE_SETTINGS_READ
-                ConfigTag.COVER_CONFIG -> ROUTE_SETTINGS_COVER
-                ConfigTag.THEME_CONFIG -> ROUTE_SETTINGS_THEME
-                ConfigTag.BACKUP_CONFIG -> ROUTE_SETTINGS_BACKUP
-                ConfigTag.DOWNLOAD_CACHE_CONFIG -> ROUTE_SETTINGS_DOWNLOAD_CACHE
-                else -> ROUTE_SETTINGS
-            }
-        }
+        ): Intent = MainIntent.createExploreShowIntent(context, exploreName, sourceUrl, exploreUrl)
     }
 
     private val viewModel by viewModel<MainViewModel>()
     private val routeEvents = MutableSharedFlow<NavKey>(extraBufferCapacity = 1)
     private var bookInfoVariableSetter: ((String, String?) -> Unit)? = null
 
-    @Serializable
-    private sealed interface MainRoute : NavKey
-
-    @Serializable
-    private data object MainRouteHome : MainRoute
-
-    @Serializable
-    private data object MainRouteSettings : MainRoute
-
-    @Serializable
-    private data object MainRouteSettingsOther : MainRoute
-
-    @Serializable
-    private data object MainRouteSettingsRead : MainRoute
-
-    @Serializable
-    private data object MainRouteSettingsCover : MainRoute
-
-    @Serializable
-    private data object MainRouteSettingsTheme : MainRoute
-
-    @Serializable
-    private data object MainRouteSettingsBackup : MainRoute
-
-    @Serializable
-    private data object MainRouteSettingsCustomTheme : MainRoute
-
-    @Serializable
-    private data object MainRouteSettingsThemeManage : MainRoute
-
-    @Serializable
-    private data object MainRouteSettingsDownloadCache : MainRoute
-
-    @Serializable
-    private data object MainRouteImportLocal : MainRoute
-
-    @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
-
-    @Serializable
-    private data object MainRouteBookCacheManage : MainRoute
-
-    @Serializable
-    private data class MainRouteSearch(
-        val key: String?,
-        val scopeRaw: String? = null
-    ) : MainRoute
-
-    @Serializable
-    private data class MainRouteBookInfo(
-        val name: String?,
-        val author: String?,
-        val bookUrl: String,
-    ) : MainRoute
-
-    @Serializable
-    private data class MainRouteExploreShow(
-        val title: String?,
-        val sourceUrl: String,
-        val exploreUrl: String?,
-    ) : MainRoute
-
     override fun onCreate(savedInstanceState: Bundle?) {
         installSplashScreen()
         super.onCreate(savedInstanceState)
@@ -367,7 +145,7 @@ open class MainActivity : BaseComposeActivity(), VariableDialog.Callback {
     override fun onNewIntent(intent: Intent) {
         super.onNewIntent(intent)
         setIntent(intent)
-        routeEvents.tryEmit(resolveStartRoute(intent))
+        routeEvents.tryEmit(MainNavigator.resolveStartRoute(intent))
     }
 
     @OptIn(ExperimentalSharedTransitionApi::class)
@@ -385,11 +163,11 @@ open class MainActivity : BaseComposeActivity(), VariableDialog.Callback {
             else -> false
         }
 
-        val backStack = rememberNavBackStack(resolveStartRoute(intent))
+        val backStack = rememberNavBackStack(MainNavigator.resolveStartRoute(intent))
 
         LaunchedEffect(backStack) {
             routeEvents.collect { route ->
-                navigateToRoute(backStack, route)
+                MainNavigator.navigateToRoute(backStack, route)
             }
         }
 
@@ -397,556 +175,69 @@ open class MainActivity : BaseComposeActivity(), VariableDialog.Callback {
             NavDisplay(
                 backStack = backStack,
                 transitionSpec = {
-                (slideIntoContainer(
-                    towards = AnimatedContentTransitionScope.SlideDirection.Start,
-                    animationSpec = tween(
-                        durationMillis = 480,
-                        easing = FastOutSlowInEasing
-                    ),
-                    initialOffset = { fullWidth -> fullWidth }
-                ) + fadeIn(
-                    animationSpec = tween(
-                        durationMillis = 360,
-                        easing = LinearOutSlowInEasing
-                    )
-                )) togetherWith (slideOutOfContainer(
-                    towards = AnimatedContentTransitionScope.SlideDirection.Start,
-                    animationSpec = tween(
-                        durationMillis = 480,
-                        easing = FastOutSlowInEasing
-                    ),
-                    targetOffset = { fullWidth -> fullWidth / 4 }
-                ) + fadeOut(
-                    animationSpec = tween(
-                        durationMillis = 360,
-                        easing = LinearOutSlowInEasing
-                    )
-                ))
-            },
-            popTransitionSpec = {
-                (slideIntoContainer(
-                    towards = AnimatedContentTransitionScope.SlideDirection.Start,
-                    animationSpec = tween(
-                        durationMillis = 480,
-                        easing = FastOutSlowInEasing
-                    ),
-                    initialOffset = { fullWidth -> -fullWidth / 4 }
-                ) + fadeIn(
-                    animationSpec = tween(
-                        durationMillis = 360,
-                        easing = LinearOutSlowInEasing
-                    )
-                )) togetherWith (scaleOut(
-                    targetScale = 0.8f,
-                    animationSpec = tween(
-                        durationMillis = 480,
-                        easing = FastOutSlowInEasing
-                    )
-                ) + fadeOut(
-                    animationSpec = tween(durationMillis = 360)
-                ))
-            },
-            predictivePopTransitionSpec = { _ ->
-                (slideIntoContainer(
-                    towards = AnimatedContentTransitionScope.SlideDirection.Start,
-                    animationSpec = tween(
-                        easing = FastOutSlowInEasing
-                    ),
-                    initialOffset = { fullWidth -> -fullWidth / 4 }
-                ) + fadeIn(
-                    animationSpec = tween(
-                        easing = LinearOutSlowInEasing
-                    )
-                )) togetherWith (scaleOut(
-                    targetScale = 0.8f,
-                    animationSpec = tween(
-                        easing = FastOutSlowInEasing
-                    )
-                ) + fadeOut(
-                    animationSpec = tween()
-                ))
-            },
-            onBack = {
-                if (backStack.size > 1) {
-                    backStack.removeLastOrNull()
-                } else {
-                    finish()
-                }
-            },
-                entryProvider = entryProvider {
-                entry<MainRouteHome> {
-                    MainScreen(
-                        useRail = useRail,
-                        onOpenSettings = {
-                            navigateToRoute(backStack, MainRouteSettings)
-                        },
-                        onNavigateToSearch = { key ->
-                            navigateToRoute(
-                                backStack,
-                                MainRouteSearch(
-                                    key = key?.trim()?.takeIf { it.isNotEmpty() }
-                                )
-                            )
-                        },
-                        onNavigateToRemoteImport = {
-                            navigateToRoute(backStack, MainRouteImportRemote)
-                        },
-                        onNavigateToLocalImport = {
-                            navigateToRoute(backStack, MainRouteImportLocal)
-                        },
-                        onNavigateToCache = { groupId ->
-                            navigateToRoute(backStack, MainRouteCache(groupId))
-                        },
-                        onNavigateToBookCacheManage = {
-                            navigateToRoute(backStack, MainRouteBookCacheManage)
-                        },
-                        onNavigateToBookInfo = { name, author, bookUrl ->
-                            navigateToRoute(
-                                backStack,
-                                MainRouteBookInfo(
-                                    name = name,
-                                    author = author,
-                                    bookUrl = bookUrl
-                                )
-                            )
-                        },
-                        onNavigateToExploreShow = { title, sourceUrl, exploreUrl ->
-                            navigateToRoute(
-                                backStack,
-                                MainRouteExploreShow(
-                                    title = title,
-                                    sourceUrl = sourceUrl,
-                                    exploreUrl = exploreUrl
-                                )
-                            )
-                        },
-                        onNavigateToRssSort = { sourceUrl, sortUrl, key ->
-                            navigateToRoute(
-                                backStack,
-                                MainRouteRssSort(
-                                    sourceUrl = sourceUrl,
-                                    sortUrl = sortUrl,
-                                    key = key
-                                )
-                            )
-                        },
-                        onNavigateToRssRead = { title, origin, link, openUrl ->
-                            navigateToRoute(
-                                backStack,
-                                MainRouteRssRead(
-                                    title = title,
-                                    origin = origin,
-                                    link = link,
-                                    openUrl = openUrl
-                                )
-                            )
-                        },
-                        onNavigateToReadRecord = {
-                            navigateToRoute(backStack, MainRouteReadRecord)
-                        },
-                        sharedTransitionScope = this@SharedTransitionLayout,
-                        animatedVisibilityScope = LocalNavAnimatedContentScope.current,
-                    )
-                }
-
-                entry<MainRouteSettings> {
-                    ConfigNavScreen(
-                        onBackClick = { navigateBack(backStack) },
-                        onNavigateToOther = { backStack.add(MainRouteSettingsOther) },
-                        onNavigateToRead = { backStack.add(MainRouteSettingsRead) },
-                        onNavigateToCover = { backStack.add(MainRouteSettingsCover) },
-                        onNavigateToTheme = { backStack.add(MainRouteSettingsTheme) },
-                        onNavigateToBackup = { backStack.add(MainRouteSettingsBackup) },
-                        onNavigateToDownloadCache = { backStack.add(MainRouteSettingsDownloadCache) }
-                    )
-                }
-
-                entry<MainRouteSettingsOther> {
-                    OtherConfigScreen(onBackClick = { navigateBack(backStack) })
-                }
-
-                entry<MainRouteSettingsRead> {
-                    ReadConfigScreen(onBackClick = { navigateBack(backStack) })
-                }
-
-                entry<MainRouteSettingsCover> {
-                    CoverConfigScreen(onBackClick = { navigateBack(backStack) })
-                }
-
-                entry<MainRouteSettingsTheme> {
-                    ThemeConfigScreen(
-                        onBackClick = { navigateBack(backStack) },
-                        onNavigateToCustomTheme = { backStack.add(MainRouteSettingsCustomTheme) },
-                        onNavigateToThemeManage = { backStack.add(MainRouteSettingsThemeManage) }
-                    )
-                }
-
-                entry<MainRouteSettingsBackup> {
-                    BackupConfigScreen(onBackClick = { navigateBack(backStack) })
-                }
-
-                entry<MainRouteSettingsDownloadCache> {
-                    DownloadCacheConfigScreen(onBackClick = { navigateBack(backStack) })
-                }
-
-                entry<MainRouteSettingsCustomTheme> {
-                    CustomThemeScreen(
-                        onBackClick = { navigateBack(backStack) }
-                    )
-                }
-
-                entry<MainRouteSettingsThemeManage> {
-                    ThemeManageScreen(onBackClick = { navigateBack(backStack) })
-                }
-
-                entry<MainRouteImportLocal> {
-                    ImportBookScreen(
-                        onBackClick = { navigateBack(backStack) }
-                    )
-                }
-
-                entry<MainRouteImportRemote> {
-                    RemoteBookScreen(
-                        onBackClick = { navigateBack(backStack) }
-                    )
-                }
-
-                entry<MainRouteCache> { route ->
-                    BookshelfManageRouteScreen(
-                        groupId = route.groupId,
-                        onBackClick = { navigateBack(backStack) },
-                        onOpenBookInfo = { name, author, bookUrl ->
-                            navigateToRoute(
-                                backStack,
-                                MainRouteBookInfo(
-                                    name = name,
-                                    author = author,
-                                    bookUrl = bookUrl
-                                )
-                            )
-                        }
-                    )
-                }
-
-                entry<MainRouteBookCacheManage> {
-                    BookCacheManageRouteScreen(
-                        onBackClick = { navigateBack(backStack) }
-                    )
-                }
-
-                entry<MainRouteSearch> { route ->
-                    val searchViewModel = koinViewModel<SearchViewModel>()
-
-                    LaunchedEffect(route.key, route.scopeRaw, searchViewModel) {
-                        searchViewModel.onIntent(
-                            SearchIntent.Initialize(
-                                key = route.key,
-                                scopeRaw = route.scopeRaw
-                            )
+                    (slideIntoContainer(
+                        towards = AnimatedContentTransitionScope.SlideDirection.Start,
+                        animationSpec = tween(durationMillis = 480, easing = FastOutSlowInEasing),
+                        initialOffset = { fullWidth -> fullWidth }
+                    ) + fadeIn(
+                        animationSpec = tween(
+                            durationMillis = 360,
+                            easing = LinearOutSlowInEasing
                         )
-                    }
-
-                    SearchScreen(
-                        viewModel = searchViewModel,
-                        onBack = {
-                            searchViewModel.onIntent(SearchIntent.ClearSearchResults)
-                            navigateBack(backStack)
-                        },
-                        onOpenBookInfo = { name, author, bookUrl ->
-                            navigateToRoute(
-                                backStack,
-                                MainRouteBookInfo(
-                                    name = name,
-                                    author = author,
-                                    bookUrl = bookUrl
-                                )
-                            )
-                        },
-                        onOpenSourceManage = {
-                            this@MainActivity.startActivity<BookSourceActivity>()
-                        },
-                        sharedTransitionScope = this@SharedTransitionLayout,
-                        animatedVisibilityScope = LocalNavAnimatedContentScope.current,
-                    )
-                }
-
-                entry<MainRouteRssSort> { route ->
-                    RssSortRouteScreen(
-                        sourceUrl = route.sourceUrl,
-                        initialSortUrl = route.sortUrl,
-                        onBackClick = { navigateBack(backStack) },
-                        onOpenRead = { title, origin, link, openUrl ->
-                            if (link?.contains("@js:") == true) {
-                                navigateToRoute(
-                                    backStack,
-                                    MainRouteRssSort(
-                                        sourceUrl = origin,
-                                        sortUrl = link
-                                    )
-                                )
-                            } else {
-                                navigateToRoute(
-                                    backStack,
-                                    MainRouteRssRead(
-                                        title = title,
-                                        origin = origin,
-                                        link = link,
-                                        openUrl = openUrl
-                                    )
-                                )
-                            }
-                        }
-                    )
-                }
-
-                entry<MainRouteRssRead> { route ->
-                    RssReadRouteScreen(
-                        title = route.title,
-                        origin = route.origin,
-                        link = route.link,
-                        openUrl = route.openUrl,
-                        onBackClick = { navigateBack(backStack) }
-                    )
-                }
-
-                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
-                        val fromStr = from.toString()
-                        if (from is MainRouteHome || from is MainRouteExploreShow || from is MainRouteSearch ||
-                            fromStr.startsWith("MainRouteHome") || fromStr.startsWith("MainRouteExploreShow") || fromStr.startsWith("MainRouteSearch")) {
-                            fadeIn(animationSpec = tween(300)) togetherWith
-                                    fadeOut(animationSpec = tween(300))
-                        } else null
-                    } + NavDisplay.popTransitionSpec {
-                        val to = targetState.key
-                        val toStr = to.toString()
-                        if (to is MainRouteHome || to is MainRouteExploreShow || to is MainRouteSearch ||
-                            toStr.startsWith("MainRouteHome") || toStr.startsWith("MainRouteExploreShow") || toStr.startsWith("MainRouteSearch")) {
-                            fadeIn(animationSpec = tween(300)) togetherWith
-                                    fadeOut(animationSpec = tween(300))
-                        } else null
-                    } + NavDisplay.predictivePopTransitionSpec { _ ->
-                        val to = targetState.key
-                        val toStr = to.toString()
-                        if (to is MainRouteHome || to is MainRouteExploreShow || to is MainRouteSearch ||
-                            toStr.startsWith("MainRouteHome") || toStr.startsWith("MainRouteExploreShow") || toStr.startsWith("MainRouteSearch")) {
-                            fadeIn(animationSpec = tween(300)) togetherWith
-                                    fadeOut(animationSpec = tween(300))
-                        } else null
-                    }
-                ) { route ->
-                    val bookInfoViewModel = koinViewModel<BookInfoViewModel>()
-                    BookInfoRouteScreen(
-                        bookUrl = route.bookUrl,
-                        viewModel = bookInfoViewModel,
-                        onBack = { navigateBack(backStack) },
-                        onFinish = { _, _ -> navigateBack(backStack) },
-                        onOpenSearch = { keyword ->
-                            navigateToRoute(backStack, MainRouteSearch(key = keyword))
-                        },
-                        sharedTransitionScope = this@SharedTransitionLayout,
-                        animatedVisibilityScope = LocalNavAnimatedContentScope.current,
-                        sharedCoverKey = bookCoverSharedElementKey(route.bookUrl),
-                        onRegisterVariableSetter = { setter ->
-                            bookInfoVariableSetter = setter
-                        }
-                    )
-                }
-
-                entry<MainRouteExploreShow> { route ->
-                    ExploreShowScreen(
-                        title = route.title ?: "探索",
-                        sourceUrl = route.sourceUrl,
-                        exploreUrl = route.exploreUrl,
-                        onBack = { navigateBack(backStack) },
-                        onBookClick = { book ->
-                            navigateToRoute(
-                                backStack,
-                                MainRouteBookInfo(
-                                    name = book.name,
-                                    author = book.author,
-                                    bookUrl = book.bookUrl
-                                )
-                            )
-                        },
-                        sharedTransitionScope = this@SharedTransitionLayout,
-                        animatedVisibilityScope = LocalNavAnimatedContentScope.current,
-                    )
-                }
-                }
+                    )) togetherWith (slideOutOfContainer(
+                        towards = AnimatedContentTransitionScope.SlideDirection.Start,
+                        animationSpec = tween(durationMillis = 480, easing = FastOutSlowInEasing),
+                        targetOffset = { fullWidth -> fullWidth / 4 }
+                    ) + fadeOut(
+                        animationSpec = tween(
+                            durationMillis = 360,
+                            easing = LinearOutSlowInEasing
+                        )
+                    ))
+                },
+                popTransitionSpec = {
+                    (slideIntoContainer(
+                        towards = AnimatedContentTransitionScope.SlideDirection.Start,
+                        animationSpec = tween(durationMillis = 480, easing = FastOutSlowInEasing),
+                        initialOffset = { fullWidth -> -fullWidth / 4 }
+                    ) + fadeIn(
+                        animationSpec = tween(
+                            durationMillis = 360,
+                            easing = LinearOutSlowInEasing
+                        )
+                    )) togetherWith (scaleOut(
+                        targetScale = 0.8f,
+                        animationSpec = tween(durationMillis = 480, easing = FastOutSlowInEasing)
+                    ) + fadeOut(animationSpec = tween(durationMillis = 360)))
+                },
+                predictivePopTransitionSpec = { _ ->
+                    (slideIntoContainer(
+                        towards = AnimatedContentTransitionScope.SlideDirection.Start,
+                        animationSpec = tween(easing = FastOutSlowInEasing),
+                        initialOffset = { fullWidth -> -fullWidth / 4 }
+                    ) + fadeIn(animationSpec = tween(easing = LinearOutSlowInEasing))) togetherWith (scaleOut(
+                        targetScale = 0.8f,
+                        animationSpec = tween(easing = FastOutSlowInEasing)
+                    ) + fadeOut(animationSpec = tween()))
+                },
+                onBack = { MainNavigator.navigateBack(this@MainActivity, backStack) },
+                entryProvider = mainEntryProvider(
+                    backStack = backStack,
+                    useRail = useRail,
+                    sharedTransitionScope = this@SharedTransitionLayout,
+                    onNavigateToRoute = { route ->
+                        MainNavigator.navigateToRoute(
+                            backStack,
+                            route
+                        )
+                    },
+                    onNavigateBack = { MainNavigator.navigateBack(this@MainActivity, backStack) },
+                    onRegisterVariableSetter = { setter -> bookInfoVariableSetter = setter }
+                )
             )
         }
     }
 
-    private fun navigateToRoute(backStack: MutableList<NavKey>, route: NavKey) {
-        val currentRoute = backStack.lastOrNull()
-        if (currentRoute == route) return
-
-        when (route) {
-            MainRouteHome -> {
-                backStack.clear()
-                backStack.add(MainRouteHome)
-            }
-
-            MainRouteSettings -> {
-                if (currentRoute == MainRouteHome) {
-                    backStack.add(MainRouteSettings)
-                } else {
-                    backStack.clear()
-                    backStack.add(MainRouteHome)
-                    backStack.add(MainRouteSettings)
-                }
-            }
-
-            MainRouteSettingsOther,
-            MainRouteSettingsRead,
-            MainRouteSettingsCover,
-            MainRouteSettingsTheme,
-            MainRouteSettingsBackup,
-            MainRouteSettingsCustomTheme,
-            MainRouteSettingsThemeManage,
-            MainRouteSettingsDownloadCache -> {
-                backStack.clear()
-                backStack.add(MainRouteHome)
-                backStack.add(MainRouteSettings)
-                backStack.add(route)
-            }
-
-            MainRouteImportLocal,
-            MainRouteImportRemote,
-            is MainRouteCache,
-            MainRouteBookCacheManage -> {
-                if (currentRoute == MainRouteHome) {
-                    backStack.add(route)
-                } else {
-                    backStack.clear()
-                    backStack.add(MainRouteHome)
-                    backStack.add(route)
-                }
-            }
-
-            is MainRouteSearch -> {
-                if (currentRoute == MainRouteHome) {
-                    backStack.add(route)
-                } else {
-                    backStack.clear()
-                    backStack.add(MainRouteHome)
-                    backStack.add(route)
-                }
-            }
-
-            is MainRouteBookInfo -> {
-                if (
-                    currentRoute == MainRouteHome ||
-                    currentRoute is MainRouteSearch ||
-                    currentRoute is MainRouteExploreShow
-                ) {
-                    backStack.add(route)
-                } else {
-                    backStack.clear()
-                    backStack.add(MainRouteHome)
-                    backStack.add(route)
-                }
-            }
-
-            is MainRouteExploreShow -> {
-                if (currentRoute == MainRouteHome) {
-                    backStack.add(route)
-                } else {
-                    backStack.clear()
-                    backStack.add(MainRouteHome)
-                    backStack.add(route)
-                }
-            }
-
-            is MainRouteRssSort -> {
-                if (currentRoute == MainRouteHome) {
-                    backStack.add(route)
-                } else {
-                    backStack.clear()
-                    backStack.add(MainRouteHome)
-                    backStack.add(route)
-                }
-            }
-
-            is MainRouteRssRead -> {
-                if (currentRoute == MainRouteHome || currentRoute is MainRouteRssSort) {
-                    backStack.add(route)
-                } else {
-                    backStack.clear()
-                    backStack.add(MainRouteHome)
-                    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)
-                }
-            }
-        }
-    }
-
-    private fun navigateBack(backStack: MutableList<NavKey>) {
-        if (backStack.size > 1) {
-            backStack.removeLastOrNull()
-        } else {
-            finish()
-        }
-    }
-
     private fun checkStartupRoute(): Boolean {
         return when {
             LocalConfig.isFirstOpenApp -> {
@@ -1057,96 +348,6 @@ open class MainActivity : BaseComposeActivity(), VariableDialog.Callback {
         }
     }
 
-    private fun resolveStartRoute(intent: Intent?): NavKey {
-        val route = intent?.getStringExtra(EXTRA_START_ROUTE)
-        resolveRssStartRoute(route, intent)?.let { return it }
-        return resolveStartRoute(route)
-    }
-
-    private fun resolveRssStartRoute(route: String?, intent: Intent?): NavKey? {
-        return when (route) {
-            ROUTE_RSS_SORT -> {
-                val sourceUrl = intent?.getStringExtra(EXTRA_RSS_SOURCE_URL)
-                if (sourceUrl.isNullOrBlank()) {
-                    null
-                } else {
-                    MainRouteRssSort(
-                        sourceUrl = sourceUrl,
-                        sortUrl = intent.getStringExtra(EXTRA_RSS_SORT_URL),
-                        key = intent.getStringExtra(EXTRA_RSS_KEY)
-                    )
-                }
-            }
-
-            ROUTE_RSS_READ -> {
-                val origin = intent?.getStringExtra(EXTRA_RSS_READ_ORIGIN)
-                if (origin.isNullOrBlank()) {
-                    null
-                } else {
-                    MainRouteRssRead(
-                        title = intent.getStringExtra(EXTRA_RSS_READ_TITLE),
-                        origin = origin,
-                        link = intent.getStringExtra(EXTRA_RSS_READ_LINK),
-                        openUrl = intent.getStringExtra(EXTRA_RSS_READ_OPEN_URL)
-                    )
-                }
-            }
-
-            else -> null
-        }
-    }
-
-    private fun resolveStartRoute(route: String?): MainRoute {
-        return when (route) {
-            "main" -> MainRouteHome
-            "settings" -> MainRouteSettings
-            "settings/other" -> MainRouteSettingsOther
-            "settings/read" -> MainRouteSettingsRead
-            "settings/cover" -> MainRouteSettingsCover
-            "settings/theme" -> MainRouteSettingsTheme
-            "settings/backup" -> MainRouteSettingsBackup
-            "settings/custom_theme" -> MainRouteSettingsCustomTheme
-            "settings/download_cache" -> MainRouteSettingsDownloadCache
-            "import/local" -> MainRouteImportLocal
-            "import/remote" -> MainRouteImportRemote
-            ROUTE_MAIN -> MainRouteHome
-            ROUTE_SETTINGS -> MainRouteSettings
-            ROUTE_SETTINGS_OTHER -> MainRouteSettingsOther
-            ROUTE_SETTINGS_READ -> MainRouteSettingsRead
-            ROUTE_SETTINGS_COVER -> MainRouteSettingsCover
-            ROUTE_SETTINGS_THEME -> MainRouteSettingsTheme
-            ROUTE_SETTINGS_BACKUP -> MainRouteSettingsBackup
-            ROUTE_SETTINGS_DOWNLOAD_CACHE -> MainRouteSettingsDownloadCache
-            ROUTE_IMPORT_LOCAL -> MainRouteImportLocal
-            ROUTE_IMPORT_REMOTE -> MainRouteImportRemote
-            ROUTE_CACHE -> MainRouteCache(intent?.getLongExtra(EXTRA_CACHE_GROUP_ID, -1L) ?: -1L)
-            ROUTE_BOOK_CACHE_MANAGE -> MainRouteBookCacheManage
-            ROUTE_SEARCH -> MainRouteSearch(
-                key = intent?.getStringExtra(EXTRA_SEARCH_KEY),
-                scopeRaw = intent?.getStringExtra(EXTRA_SEARCH_SCOPE)
-            )
-            ROUTE_BOOK_INFO -> intent?.getStringExtra(EXTRA_BOOK_URL)
-                ?.takeIf { it.isNotBlank() }
-                ?.let { bookUrl ->
-                    MainRouteBookInfo(
-                        name = intent.getStringExtra(EXTRA_BOOK_NAME),
-                        author = intent.getStringExtra(EXTRA_BOOK_AUTHOR),
-                        bookUrl = bookUrl
-                    )
-                } ?: MainRouteHome
-            ROUTE_EXPLORE_SHOW -> intent?.getStringExtra(EXTRA_SOURCE_URL)
-                ?.takeIf { it.isNotBlank() }
-                ?.let { sourceUrl ->
-                    MainRouteExploreShow(
-                        title = intent.getStringExtra(EXTRA_EXPLORE_NAME),
-                        sourceUrl = sourceUrl,
-                        exploreUrl = intent.getStringExtra(EXTRA_EXPLORE_URL)
-                    )
-                } ?: MainRouteHome
-            else -> MainRouteHome
-        }
-    }
-
     override fun setVariable(key: String, variable: String?) {
         bookInfoVariableSetter?.invoke(key, variable)
     }

+ 152 - 0
app/src/main/java/io/legado/app/ui/main/MainIntent.kt

@@ -0,0 +1,152 @@
+package io.legado.app.ui.main
+
+import android.content.Context
+import android.content.Intent
+import io.legado.app.ui.config.ConfigTag
+
+object MainIntent {
+    const val EXTRA_START_ROUTE = "startRoute"
+    const val EXTRA_CACHE_GROUP_ID = "extra_cache_group_id"
+    const val EXTRA_SEARCH_KEY = "extra_search_key"
+    const val EXTRA_SEARCH_SCOPE = "extra_search_scope"
+    const val EXTRA_BOOK_NAME = "name"
+    const val EXTRA_BOOK_AUTHOR = "author"
+    const val EXTRA_BOOK_URL = "bookUrl"
+    const val EXTRA_EXPLORE_NAME = "exploreName"
+    const val EXTRA_SOURCE_URL = "sourceUrl"
+    const val EXTRA_EXPLORE_URL = "exploreUrl"
+
+    const val EXTRA_RSS_SOURCE_URL = "extra_rss_source_url"
+    const val EXTRA_RSS_SORT_URL = "extra_rss_sort_url"
+    const val EXTRA_RSS_KEY = "extra_rss_key"
+
+    const val EXTRA_RSS_READ_TITLE = "extra_rss_read_title"
+    const val EXTRA_RSS_READ_ORIGIN = "extra_rss_read_origin"
+    const val EXTRA_RSS_READ_LINK = "extra_rss_read_link"
+    const val EXTRA_RSS_READ_OPEN_URL = "extra_rss_read_open_url"
+
+    fun createLauncherIntent(context: Context): Intent {
+        val launcherComponent =
+            context.packageManager.getLaunchIntentForPackage(context.packageName)?.component
+        return if (launcherComponent != null) {
+            Intent().setComponent(launcherComponent)
+        } else {
+            Intent(context, MainActivity::class.java)
+        }
+    }
+
+    fun createHomeIntent(context: Context): Intent {
+        return createLauncherIntent(context).apply {
+            putExtra(EXTRA_START_ROUTE, MainRouteConst.ROUTE_MAIN)
+        }
+    }
+
+    fun createIntent(context: Context, configTag: String? = null): Intent {
+        return createLauncherIntent(context).apply {
+            putExtra(EXTRA_START_ROUTE, routeForConfigTag(configTag))
+        }
+    }
+
+    fun createRssSortIntent(
+        context: Context,
+        sourceUrl: String,
+        sortUrl: String? = null,
+        key: String? = null
+    ): Intent {
+        return createLauncherIntent(context).apply {
+            putExtra(EXTRA_START_ROUTE, MainRouteConst.ROUTE_RSS_SORT)
+            putExtra(EXTRA_RSS_SOURCE_URL, sourceUrl)
+            putExtra(EXTRA_RSS_SORT_URL, sortUrl)
+            putExtra(EXTRA_RSS_KEY, key)
+        }
+    }
+
+    fun createRssReadIntent(
+        context: Context,
+        title: String? = null,
+        origin: String,
+        link: String? = null,
+        openUrl: String? = null
+    ): Intent {
+        return createLauncherIntent(context).apply {
+            putExtra(EXTRA_START_ROUTE, MainRouteConst.ROUTE_RSS_READ)
+            putExtra(EXTRA_RSS_READ_TITLE, title)
+            putExtra(EXTRA_RSS_READ_ORIGIN, origin)
+            putExtra(EXTRA_RSS_READ_LINK, link)
+            putExtra(EXTRA_RSS_READ_OPEN_URL, openUrl)
+        }
+    }
+
+    fun createBookshelfManageScreenIntent(
+        context: Context,
+        groupId: Long = -1L
+    ): Intent {
+        return createLauncherIntent(context).apply {
+            putExtra(EXTRA_START_ROUTE, MainRouteConst.ROUTE_CACHE)
+            putExtra(EXTRA_CACHE_GROUP_ID, groupId)
+        }
+    }
+
+    fun createCacheIntent(
+        context: Context,
+        groupId: Long = -1L
+    ): Intent = createBookshelfManageScreenIntent(context, groupId)
+
+    fun createBookCacheManageIntent(context: Context): Intent {
+        return createLauncherIntent(context).apply {
+            putExtra(EXTRA_START_ROUTE, MainRouteConst.ROUTE_BOOK_CACHE_MANAGE)
+        }
+    }
+
+    fun createSearchIntent(
+        context: Context,
+        key: String? = null,
+        scopeRaw: String? = null
+    ): Intent {
+        return createLauncherIntent(context).apply {
+            putExtra(EXTRA_START_ROUTE, MainRouteConst.ROUTE_SEARCH)
+            putExtra(EXTRA_SEARCH_KEY, key)
+            putExtra(EXTRA_SEARCH_SCOPE, scopeRaw)
+        }
+    }
+
+    fun createBookInfoIntent(
+        context: Context,
+        name: String? = null,
+        author: String? = null,
+        bookUrl: String
+    ): Intent {
+        return createLauncherIntent(context).apply {
+            putExtra(EXTRA_START_ROUTE, MainRouteConst.ROUTE_BOOK_INFO)
+            putExtra(EXTRA_BOOK_NAME, name)
+            putExtra(EXTRA_BOOK_AUTHOR, author)
+            putExtra(EXTRA_BOOK_URL, bookUrl)
+        }
+    }
+
+    fun createExploreShowIntent(
+        context: Context,
+        exploreName: String? = null,
+        sourceUrl: String,
+        exploreUrl: String? = null
+    ): Intent {
+        return createLauncherIntent(context).apply {
+            putExtra(EXTRA_START_ROUTE, MainRouteConst.ROUTE_EXPLORE_SHOW)
+            putExtra(EXTRA_EXPLORE_NAME, exploreName)
+            putExtra(EXTRA_SOURCE_URL, sourceUrl)
+            putExtra(EXTRA_EXPLORE_URL, exploreUrl)
+        }
+    }
+
+    private fun routeForConfigTag(configTag: String?): String {
+        return when (configTag) {
+            ConfigTag.OTHER_CONFIG -> MainRouteConst.ROUTE_SETTINGS_OTHER
+            ConfigTag.READ_CONFIG -> MainRouteConst.ROUTE_SETTINGS_READ
+            ConfigTag.COVER_CONFIG -> MainRouteConst.ROUTE_SETTINGS_COVER
+            ConfigTag.THEME_CONFIG -> MainRouteConst.ROUTE_SETTINGS_THEME
+            ConfigTag.BACKUP_CONFIG -> MainRouteConst.ROUTE_SETTINGS_BACKUP
+            ConfigTag.DOWNLOAD_CACHE_CONFIG -> MainRouteConst.ROUTE_SETTINGS_DOWNLOAD_CACHE
+            else -> MainRouteConst.ROUTE_SETTINGS
+        }
+    }
+}

+ 423 - 0
app/src/main/java/io/legado/app/ui/main/MainNavGraph.kt

@@ -0,0 +1,423 @@
+package io.legado.app.ui.main
+
+import androidx.compose.animation.ExperimentalSharedTransitionApi
+import androidx.compose.animation.SharedTransitionScope
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.togetherWith
+import androidx.compose.runtime.LaunchedEffect
+import androidx.lifecycle.lifecycleScope
+import androidx.navigation3.runtime.NavKey
+import androidx.navigation3.runtime.entryProvider
+import androidx.navigation3.ui.LocalNavAnimatedContentScope
+import androidx.navigation3.ui.NavDisplay
+import io.legado.app.ui.book.cache.manage.BookCacheManageRouteScreen
+import io.legado.app.ui.book.explore.ExploreShowScreen
+import io.legado.app.ui.book.import.local.ImportBookScreen
+import io.legado.app.ui.book.import.remote.RemoteBookScreen
+import io.legado.app.ui.book.info.BookInfoRouteScreen
+import io.legado.app.ui.book.info.BookInfoViewModel
+import io.legado.app.ui.book.manage.BookshelfManageRouteScreen
+import io.legado.app.ui.book.readRecord.ReadRecordOverviewScreen
+import io.legado.app.ui.book.readRecord.ReadRecordScreen
+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.source.manage.BookSourceActivity
+import io.legado.app.ui.config.ConfigNavScreen
+import io.legado.app.ui.config.backupConfig.BackupConfigScreen
+import io.legado.app.ui.config.coverConfig.CoverConfigScreen
+import io.legado.app.ui.config.customTheme.CustomThemeScreen
+import io.legado.app.ui.config.downloadCacheConfig.DownloadCacheConfigScreen
+import io.legado.app.ui.config.otherConfig.OtherConfigScreen
+import io.legado.app.ui.config.readConfig.ReadConfigScreen
+import io.legado.app.ui.config.themeConfig.ThemeConfigScreen
+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.favorites.RssFavoritesScreen
+import io.legado.app.ui.rss.read.MainRouteRssRead
+import io.legado.app.ui.rss.read.RssReadRouteScreen
+import io.legado.app.ui.rss.subscription.RuleSubScreen
+import io.legado.app.utils.startActivity
+import io.legado.app.utils.startActivityForBook
+import kotlinx.coroutines.Dispatchers.IO
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import org.koin.androidx.compose.koinViewModel
+
+@OptIn(ExperimentalSharedTransitionApi::class)
+fun MainActivity.mainEntryProvider(
+    backStack: MutableList<NavKey>,
+    useRail: Boolean,
+    sharedTransitionScope: SharedTransitionScope,
+    onNavigateToRoute: (NavKey) -> Unit,
+    onNavigateBack: () -> Unit,
+    onRegisterVariableSetter: (((String, String?) -> Unit)?) -> Unit
+) = entryProvider {
+    entry<MainRouteHome> {
+        MainScreen(
+            useRail = useRail,
+            onOpenSettings = {
+                onNavigateToRoute(MainRouteSettings)
+            },
+            onNavigateToSearch = { key ->
+                onNavigateToRoute(
+                    MainRouteSearch(
+                        key = key?.trim()?.takeIf { it.isNotEmpty() }
+                    )
+                )
+            },
+            onNavigateToRemoteImport = {
+                onNavigateToRoute(MainRouteImportRemote)
+            },
+            onNavigateToLocalImport = {
+                onNavigateToRoute(MainRouteImportLocal)
+            },
+            onNavigateToCache = { groupId ->
+                onNavigateToRoute(MainRouteCache(groupId))
+            },
+            onNavigateToBookCacheManage = {
+                onNavigateToRoute(MainRouteBookCacheManage)
+            },
+            onNavigateToBookInfo = { name, author, bookUrl ->
+                onNavigateToRoute(
+                    MainRouteBookInfo(
+                        name = name,
+                        author = author,
+                        bookUrl = bookUrl
+                    )
+                )
+            },
+            onNavigateToExploreShow = { title, sourceUrl, exploreUrl ->
+                onNavigateToRoute(
+                    MainRouteExploreShow(
+                        title = title,
+                        sourceUrl = sourceUrl,
+                        exploreUrl = exploreUrl
+                    )
+                )
+            },
+            onNavigateToRssSort = { sourceUrl, sortUrl, key ->
+                onNavigateToRoute(
+                    MainRouteRssSort(
+                        sourceUrl = sourceUrl,
+                        sortUrl = sortUrl,
+                        key = key
+                    )
+                )
+            },
+            onNavigateToRssRead = { title, origin, link, openUrl ->
+                onNavigateToRoute(
+                    MainRouteRssRead(
+                        title = title,
+                        origin = origin,
+                        link = link,
+                        openUrl = openUrl
+                    )
+                )
+            },
+            onNavigateToRssFavorites = {
+                onNavigateToRoute(MainRouteRssFavorites)
+            },
+            onNavigateToRuleSub = {
+                onNavigateToRoute(MainRouteRuleSub)
+            },
+            onNavigateToReadRecord = {
+                onNavigateToRoute(MainRouteReadRecord)
+            },
+            sharedTransitionScope = sharedTransitionScope,
+            animatedVisibilityScope = LocalNavAnimatedContentScope.current,
+        )
+    }
+
+    entry<MainRouteSettings> {
+        ConfigNavScreen(
+            onBackClick = { onNavigateBack() },
+            onNavigateToOther = { backStack.add(MainRouteSettingsOther) },
+            onNavigateToRead = { backStack.add(MainRouteSettingsRead) },
+            onNavigateToCover = { backStack.add(MainRouteSettingsCover) },
+            onNavigateToTheme = { backStack.add(MainRouteSettingsTheme) },
+            onNavigateToBackup = { backStack.add(MainRouteSettingsBackup) },
+            onNavigateToDownloadCache = { backStack.add(MainRouteSettingsDownloadCache) }
+        )
+    }
+
+    entry<MainRouteSettingsOther> {
+        OtherConfigScreen(onBackClick = { onNavigateBack() })
+    }
+
+    entry<MainRouteSettingsRead> {
+        ReadConfigScreen(onBackClick = { onNavigateBack() })
+    }
+
+    entry<MainRouteSettingsCover> {
+        CoverConfigScreen(onBackClick = { onNavigateBack() })
+    }
+
+    entry<MainRouteSettingsTheme> {
+        ThemeConfigScreen(
+            onBackClick = { onNavigateBack() },
+            onNavigateToCustomTheme = { backStack.add(MainRouteSettingsCustomTheme) },
+            onNavigateToThemeManage = { backStack.add(MainRouteSettingsThemeManage) }
+        )
+    }
+
+    entry<MainRouteSettingsBackup> {
+        BackupConfigScreen(onBackClick = { onNavigateBack() })
+    }
+
+    entry<MainRouteSettingsDownloadCache> {
+        DownloadCacheConfigScreen(onBackClick = { onNavigateBack() })
+    }
+
+    entry<MainRouteSettingsCustomTheme> {
+        CustomThemeScreen(
+            onBackClick = { onNavigateBack() }
+        )
+    }
+
+    entry<MainRouteSettingsThemeManage> {
+        ThemeManageScreen(onBackClick = { onNavigateBack() })
+    }
+
+    entry<MainRouteImportLocal> {
+        ImportBookScreen(
+            onBackClick = { onNavigateBack() }
+        )
+    }
+
+    entry<MainRouteImportRemote> {
+        RemoteBookScreen(
+            onBackClick = { onNavigateBack() }
+        )
+    }
+
+    entry<MainRouteCache> { route ->
+        BookshelfManageRouteScreen(
+            groupId = route.groupId,
+            onBackClick = { onNavigateBack() },
+            onOpenBookInfo = { name, author, bookUrl ->
+                onNavigateToRoute(
+                    MainRouteBookInfo(
+                        name = name,
+                        author = author,
+                        bookUrl = bookUrl
+                    )
+                )
+            }
+        )
+    }
+
+    entry<MainRouteBookCacheManage> {
+        BookCacheManageRouteScreen(
+            onBackClick = { onNavigateBack() }
+        )
+    }
+
+    entry<MainRouteSearch> { route ->
+        val searchViewModel = koinViewModel<SearchViewModel>()
+
+        LaunchedEffect(route.key, route.scopeRaw, searchViewModel) {
+            searchViewModel.onIntent(
+                SearchIntent.Initialize(
+                    key = route.key,
+                    scopeRaw = route.scopeRaw
+                )
+            )
+        }
+
+        SearchScreen(
+            viewModel = searchViewModel,
+            onBack = {
+                searchViewModel.onIntent(SearchIntent.ClearSearchResults)
+                onNavigateBack()
+            },
+            onOpenBookInfo = { name, author, bookUrl ->
+                onNavigateToRoute(
+                    MainRouteBookInfo(
+                        name = name,
+                        author = author,
+                        bookUrl = bookUrl
+                    )
+                )
+            },
+            onOpenSourceManage = {
+                this@mainEntryProvider.startActivity<BookSourceActivity>()
+            },
+            sharedTransitionScope = sharedTransitionScope,
+            animatedVisibilityScope = LocalNavAnimatedContentScope.current,
+        )
+    }
+
+    entry<MainRouteRssSort> { route ->
+        RssSortRouteScreen(
+            sourceUrl = route.sourceUrl,
+            initialSortUrl = route.sortUrl,
+            onBackClick = { onNavigateBack() },
+            onOpenRead = { title, origin, link, openUrl ->
+                if (link?.contains("@js:") == true) {
+                    onNavigateToRoute(
+                        MainRouteRssSort(
+                            sourceUrl = origin,
+                            sortUrl = link
+                        )
+                    )
+                } else {
+                    onNavigateToRoute(
+                        MainRouteRssRead(
+                            title = title,
+                            origin = origin,
+                            link = link,
+                            openUrl = openUrl
+                        )
+                    )
+                }
+            }
+        )
+    }
+
+    entry<MainRouteRssRead> { route ->
+        RssReadRouteScreen(
+            title = route.title,
+            origin = route.origin,
+            link = route.link,
+            openUrl = route.openUrl,
+            onBackClick = { onNavigateBack() }
+        )
+    }
+
+    entry<MainRouteRssFavorites> {
+        RssFavoritesScreen(
+            onBackClick = { onNavigateBack() },
+            onOpenRead = { title, origin, link, openUrl ->
+                onNavigateToRoute(
+                    MainRouteRssRead(
+                        title = title,
+                        origin = origin,
+                        link = link,
+                        openUrl = openUrl
+                    )
+                )
+            }
+        )
+    }
+
+    entry<MainRouteRuleSub> {
+        RuleSubScreen(
+            onBackClick = { onNavigateBack() }
+        )
+    }
+
+    entry<MainRouteReadRecord> {
+        ReadRecordScreen(
+            onBackClick = { onNavigateBack() },
+            onBookClick = { name, author ->
+                lifecycleScope.launch {
+                    val book = withContext(IO) {
+                        io.legado.app.data.appDb.bookDao.getBook(name, author)
+                    }
+                    if (book != null) this@mainEntryProvider.startActivityForBook(book)
+                    else {
+                        onNavigateToRoute(MainRouteSearch(key = name))
+                    }
+                }
+            },
+            onSummaryClick = {
+                onNavigateToRoute(MainRouteReadRecordOverview)
+            }
+        )
+    }
+
+    entry<MainRouteReadRecordOverview> {
+        ReadRecordOverviewScreen(
+            onBackClick = { onNavigateBack() },
+            onBookClick = { name, author ->
+                lifecycleScope.launch {
+                    val book = withContext(IO) {
+                        io.legado.app.data.appDb.bookDao.getBook(name, author)
+                    }
+                    if (book != null) this@mainEntryProvider.startActivityForBook(book)
+                    else {
+                        onNavigateToRoute(MainRouteSearch(key = name))
+                    }
+                }
+            }
+        )
+    }
+
+    entry<MainRouteBookInfo>(
+        metadata = NavDisplay.transitionSpec {
+            val from = initialState.key
+            val fromStr = from.toString()
+            if (from is MainRouteHome || from is MainRouteExploreShow || from is MainRouteSearch ||
+                fromStr.startsWith("MainRouteHome") || fromStr.startsWith("MainRouteExploreShow") || fromStr.startsWith(
+                    "MainRouteSearch"
+                )
+            ) {
+                fadeIn(animationSpec = tween(300)) togetherWith
+                        fadeOut(animationSpec = tween(300))
+            } else null
+        } + NavDisplay.popTransitionSpec {
+            val to = targetState.key
+            val toStr = to.toString()
+            if (to is MainRouteHome || to is MainRouteExploreShow || to is MainRouteSearch ||
+                toStr.startsWith("MainRouteHome") || toStr.startsWith("MainRouteExploreShow") || toStr.startsWith(
+                    "MainRouteSearch"
+                )
+            ) {
+                fadeIn(animationSpec = tween(300)) togetherWith
+                        fadeOut(animationSpec = tween(300))
+            } else null
+        } + NavDisplay.predictivePopTransitionSpec { _ ->
+            val to = targetState.key
+            val toStr = to.toString()
+            if (to is MainRouteHome || to is MainRouteExploreShow || to is MainRouteSearch ||
+                toStr.startsWith("MainRouteHome") || toStr.startsWith("MainRouteExploreShow") || toStr.startsWith(
+                    "MainRouteSearch"
+                )
+            ) {
+                fadeIn(animationSpec = tween(300)) togetherWith
+                        fadeOut(animationSpec = tween(300))
+            } else null
+        }
+    ) { route ->
+        val bookInfoViewModel = koinViewModel<BookInfoViewModel>()
+        BookInfoRouteScreen(
+            bookUrl = route.bookUrl,
+            viewModel = bookInfoViewModel,
+            onBack = { onNavigateBack() },
+            onFinish = { _, _ -> onNavigateBack() },
+            onOpenSearch = { keyword ->
+                onNavigateToRoute(MainRouteSearch(key = keyword))
+            },
+            sharedTransitionScope = sharedTransitionScope,
+            animatedVisibilityScope = LocalNavAnimatedContentScope.current,
+            sharedCoverKey = bookCoverSharedElementKey(route.bookUrl),
+            onRegisterVariableSetter = { setter ->
+                onRegisterVariableSetter(setter)
+            }
+        )
+    }
+
+    entry<MainRouteExploreShow> { route ->
+        ExploreShowScreen(
+            title = route.title ?: "探索",
+            sourceUrl = route.sourceUrl,
+            exploreUrl = route.exploreUrl,
+            onBack = { onNavigateBack() },
+            onBookClick = { book ->
+                onNavigateToRoute(
+                    MainRouteBookInfo(
+                        name = book.name,
+                        author = book.author,
+                        bookUrl = book.bookUrl
+                    )
+                )
+            },
+            sharedTransitionScope = sharedTransitionScope,
+            animatedVisibilityScope = LocalNavAnimatedContentScope.current,
+        )
+    }
+}

+ 106 - 0
app/src/main/java/io/legado/app/ui/main/MainNavKey.kt

@@ -0,0 +1,106 @@
+package io.legado.app.ui.main
+
+import androidx.navigation3.runtime.NavKey
+import kotlinx.serialization.Serializable
+
+@Serializable
+sealed interface MainRoute : NavKey
+
+@Serializable
+data object MainRouteHome : MainRoute
+
+@Serializable
+data object MainRouteSettings : MainRoute
+
+@Serializable
+data object MainRouteSettingsOther : MainRoute
+
+@Serializable
+data object MainRouteSettingsRead : MainRoute
+
+@Serializable
+data object MainRouteSettingsCover : MainRoute
+
+@Serializable
+data object MainRouteSettingsTheme : MainRoute
+
+@Serializable
+data object MainRouteSettingsBackup : MainRoute
+
+@Serializable
+data object MainRouteSettingsCustomTheme : MainRoute
+
+@Serializable
+data object MainRouteSettingsThemeManage : MainRoute
+
+@Serializable
+data object MainRouteSettingsDownloadCache : MainRoute
+
+@Serializable
+data object MainRouteImportLocal : MainRoute
+
+@Serializable
+data object MainRouteImportRemote : MainRoute
+
+@Serializable
+data object MainRouteReadRecord : MainRoute
+
+@Serializable
+data object MainRouteReadRecordOverview : MainRoute
+
+@Serializable
+data class MainRouteCache(val groupId: Long) : MainRoute
+
+@Serializable
+data object MainRouteBookCacheManage : MainRoute
+
+@Serializable
+data class MainRouteSearch(
+    val key: String?,
+    val scopeRaw: String? = null
+) : MainRoute
+
+@Serializable
+data class MainRouteBookInfo(
+    val name: String?,
+    val author: String?,
+    val bookUrl: String,
+) : MainRoute
+
+@Serializable
+data object MainRouteRssFavorites : MainRoute
+
+@Serializable
+data object MainRouteRuleSub : MainRoute
+
+@Serializable
+data class MainRouteExploreShow(
+    val title: String?,
+    val sourceUrl: String,
+    val exploreUrl: String?,
+) : MainRoute
+
+object MainRouteConst {
+    const val ROUTE_MAIN = "main"
+    const val ROUTE_SETTINGS = "settings"
+    const val ROUTE_SETTINGS_OTHER = "settings/other"
+    const val ROUTE_SETTINGS_READ = "settings/read"
+    const val ROUTE_SETTINGS_COVER = "settings/cover"
+    const val ROUTE_SETTINGS_THEME = "settings/theme"
+    const val ROUTE_SETTINGS_BACKUP = "settings/backup"
+    const val ROUTE_SETTINGS_CUSTOM_THEME = "settings/custom_theme"
+    const val ROUTE_SETTINGS_DOWNLOAD_CACHE = "settings/download_cache"
+    const val ROUTE_IMPORT_LOCAL = "import/local"
+    const val ROUTE_IMPORT_REMOTE = "import/remote"
+    const val ROUTE_CACHE = "cache"
+    const val ROUTE_BOOK_CACHE_MANAGE = "book/cache/manage"
+    const val ROUTE_SEARCH = "search"
+    const val ROUTE_BOOK_INFO = "book/info"
+    const val ROUTE_EXPLORE_SHOW = "explore/show"
+    const val ROUTE_RSS_SORT = "rss/sort"
+    const val ROUTE_RSS_READ = "rss/read"
+    const val ROUTE_RSS_FAVORITES = "rss/favorites"
+    const val ROUTE_RULE_SUB = "rss/rule_sub"
+    const val ROUTE_READ_RECORD = "read_record"
+    const val ROUTE_READ_RECORD_OVERVIEW = "read_record_overview"
+}

+ 253 - 0
app/src/main/java/io/legado/app/ui/main/MainNavigator.kt

@@ -0,0 +1,253 @@
+package io.legado.app.ui.main
+
+import android.app.Activity
+import android.content.Intent
+import androidx.navigation3.runtime.NavKey
+import io.legado.app.ui.rss.article.MainRouteRssSort
+import io.legado.app.ui.rss.read.MainRouteRssRead
+
+object MainNavigator {
+
+    fun navigateToRoute(backStack: MutableList<NavKey>, route: NavKey) {
+        val currentRoute = backStack.lastOrNull()
+        if (currentRoute == route) return
+
+        when (route) {
+            MainRouteHome -> {
+                backStack.clear()
+                backStack.add(MainRouteHome)
+            }
+
+            MainRouteSettings -> {
+                if (currentRoute == MainRouteHome) {
+                    backStack.add(MainRouteSettings)
+                } else {
+                    backStack.clear()
+                    backStack.add(MainRouteHome)
+                    backStack.add(MainRouteSettings)
+                }
+            }
+
+            MainRouteSettingsOther,
+            MainRouteSettingsRead,
+            MainRouteSettingsCover,
+            MainRouteSettingsTheme,
+            MainRouteSettingsBackup,
+            MainRouteSettingsCustomTheme,
+            MainRouteSettingsThemeManage,
+            MainRouteSettingsDownloadCache -> {
+                backStack.clear()
+                backStack.add(MainRouteHome)
+                backStack.add(MainRouteSettings)
+                backStack.add(route)
+            }
+
+            MainRouteImportLocal,
+            MainRouteImportRemote,
+            is MainRouteCache,
+            MainRouteBookCacheManage -> {
+                if (currentRoute == MainRouteHome) {
+                    backStack.add(route)
+                } else {
+                    backStack.clear()
+                    backStack.add(MainRouteHome)
+                    backStack.add(route)
+                }
+            }
+
+            is MainRouteSearch -> {
+                if (
+                    currentRoute == MainRouteHome ||
+                    currentRoute is MainRouteBookInfo ||
+                    currentRoute is MainRouteExploreShow ||
+                    currentRoute is MainRouteSearch
+                ) {
+                    backStack.add(route)
+                } else {
+                    backStack.clear()
+                    backStack.add(MainRouteHome)
+                    backStack.add(route)
+                }
+            }
+
+            is MainRouteBookInfo -> {
+                if (
+                    currentRoute == MainRouteHome ||
+                    currentRoute is MainRouteSearch ||
+                    currentRoute is MainRouteExploreShow ||
+                    currentRoute is MainRouteBookInfo
+                ) {
+                    backStack.add(route)
+                } else {
+                    backStack.clear()
+                    backStack.add(MainRouteHome)
+                    backStack.add(route)
+                }
+            }
+
+            is MainRouteExploreShow -> {
+                if (
+                    currentRoute == MainRouteHome ||
+                    currentRoute is MainRouteBookInfo ||
+                    currentRoute is MainRouteSearch ||
+                    currentRoute is MainRouteExploreShow
+                ) {
+                    backStack.add(route)
+                } else {
+                    backStack.clear()
+                    backStack.add(MainRouteHome)
+                    backStack.add(route)
+                }
+            }
+
+            is MainRouteRssSort -> {
+                if (
+                    currentRoute == MainRouteHome ||
+                    currentRoute is MainRouteRssSort ||
+                    currentRoute is MainRouteRssRead
+                ) {
+                    backStack.add(route)
+                } else {
+                    backStack.clear()
+                    backStack.add(MainRouteHome)
+                    backStack.add(route)
+                }
+            }
+
+            is MainRouteRssRead -> {
+                if (
+                    currentRoute == MainRouteHome ||
+                    currentRoute is MainRouteRssSort ||
+                    currentRoute is MainRouteRssRead
+                ) {
+                    backStack.add(route)
+                } else {
+                    backStack.clear()
+                    backStack.add(MainRouteHome)
+                    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)
+                }
+            }
+        }
+    }
+
+    fun navigateBack(activity: Activity, backStack: MutableList<NavKey>) {
+        if (backStack.size > 1) {
+            backStack.removeLastOrNull()
+        } else {
+            activity.finish()
+        }
+    }
+
+    fun resolveStartRoute(intent: Intent?): NavKey {
+        val route = intent?.getStringExtra(MainIntent.EXTRA_START_ROUTE)
+        resolveRssStartRoute(route, intent)?.let { return it }
+        return resolveStartRoute(route, intent)
+    }
+
+    private fun resolveRssStartRoute(route: String?, intent: Intent?): NavKey? {
+        return when (route) {
+            MainRouteConst.ROUTE_RSS_SORT -> {
+                val sourceUrl = intent?.getStringExtra(MainIntent.EXTRA_RSS_SOURCE_URL)
+                if (sourceUrl.isNullOrBlank()) {
+                    null
+                } else {
+                    MainRouteRssSort(
+                        sourceUrl = sourceUrl,
+                        sortUrl = intent.getStringExtra(MainIntent.EXTRA_RSS_SORT_URL),
+                        key = intent.getStringExtra(MainIntent.EXTRA_RSS_KEY)
+                    )
+                }
+            }
+
+            MainRouteConst.ROUTE_RSS_READ -> {
+                val origin = intent?.getStringExtra(MainIntent.EXTRA_RSS_READ_ORIGIN)
+                if (origin.isNullOrBlank()) {
+                    null
+                } else {
+                    MainRouteRssRead(
+                        title = intent.getStringExtra(MainIntent.EXTRA_RSS_READ_TITLE),
+                        origin = origin,
+                        link = intent.getStringExtra(MainIntent.EXTRA_RSS_READ_LINK),
+                        openUrl = intent.getStringExtra(MainIntent.EXTRA_RSS_READ_OPEN_URL)
+                    )
+                }
+            }
+
+            MainRouteConst.ROUTE_RSS_FAVORITES -> MainRouteRssFavorites
+
+            MainRouteConst.ROUTE_RULE_SUB -> MainRouteRuleSub
+
+            else -> null
+        }
+    }
+
+    private fun resolveStartRoute(route: String?, intent: Intent?): MainRoute {
+        return when (route) {
+            MainRouteConst.ROUTE_MAIN -> MainRouteHome
+            MainRouteConst.ROUTE_SETTINGS -> MainRouteSettings
+            MainRouteConst.ROUTE_SETTINGS_OTHER -> MainRouteSettingsOther
+            MainRouteConst.ROUTE_SETTINGS_READ -> MainRouteSettingsRead
+            MainRouteConst.ROUTE_SETTINGS_COVER -> MainRouteSettingsCover
+            MainRouteConst.ROUTE_SETTINGS_THEME -> MainRouteSettingsTheme
+            MainRouteConst.ROUTE_SETTINGS_BACKUP -> MainRouteSettingsBackup
+            MainRouteConst.ROUTE_SETTINGS_CUSTOM_THEME -> MainRouteSettingsCustomTheme
+            MainRouteConst.ROUTE_SETTINGS_DOWNLOAD_CACHE -> MainRouteSettingsDownloadCache
+            MainRouteConst.ROUTE_IMPORT_LOCAL -> MainRouteImportLocal
+            MainRouteConst.ROUTE_IMPORT_REMOTE -> MainRouteImportRemote
+            MainRouteConst.ROUTE_CACHE -> MainRouteCache(
+                intent?.getLongExtra(
+                    MainIntent.EXTRA_CACHE_GROUP_ID,
+                    -1L
+                ) ?: -1L
+            )
+
+            MainRouteConst.ROUTE_BOOK_CACHE_MANAGE -> MainRouteBookCacheManage
+            MainRouteConst.ROUTE_SEARCH -> MainRouteSearch(
+                key = intent?.getStringExtra(MainIntent.EXTRA_SEARCH_KEY),
+                scopeRaw = intent?.getStringExtra(MainIntent.EXTRA_SEARCH_SCOPE)
+            )
+
+            MainRouteConst.ROUTE_BOOK_INFO -> intent?.getStringExtra(MainIntent.EXTRA_BOOK_URL)
+                ?.takeIf { it.isNotBlank() }
+                ?.let { bookUrl ->
+                    MainRouteBookInfo(
+                        name = intent.getStringExtra(MainIntent.EXTRA_BOOK_NAME),
+                        author = intent.getStringExtra(MainIntent.EXTRA_BOOK_AUTHOR),
+                        bookUrl = bookUrl
+                    )
+                } ?: MainRouteHome
+
+            MainRouteConst.ROUTE_EXPLORE_SHOW -> intent?.getStringExtra(MainIntent.EXTRA_SOURCE_URL)
+                ?.takeIf { it.isNotBlank() }
+                ?.let { sourceUrl ->
+                    MainRouteExploreShow(
+                        title = intent.getStringExtra(MainIntent.EXTRA_EXPLORE_NAME),
+                        sourceUrl = sourceUrl,
+                        exploreUrl = intent.getStringExtra(MainIntent.EXTRA_EXPLORE_URL)
+                    )
+                } ?: MainRouteHome
+
+            else -> MainRouteHome
+        }
+    }
+}

+ 5 - 1
app/src/main/java/io/legado/app/ui/main/MainScreen.kt

@@ -113,6 +113,8 @@ fun MainScreen(
     onNavigateToExploreShow: (title: String?, sourceUrl: String, exploreUrl: String?) -> Unit,
     onNavigateToRssSort: (sourceUrl: String, sortUrl: String?, key: String?) -> Unit,
     onNavigateToRssRead: (title: String?, origin: String, link: String?, openUrl: String?) -> Unit,
+    onNavigateToRssFavorites: () -> Unit,
+    onNavigateToRuleSub: () -> Unit,
     onNavigateToReadRecord: () -> Unit,
     sharedTransitionScope: SharedTransitionScope? = null,
     animatedVisibilityScope: AnimatedVisibilityScope? = null,
@@ -383,7 +385,9 @@ fun MainScreen(
                                 },
                                 onOpenRead = { title, origin, link, openUrl ->
                                     onNavigateToRssRead(title, origin, link, openUrl)
-                                }
+                                },
+                                onOpenFavorites = onNavigateToRssFavorites,
+                                onOpenRuleSub = onNavigateToRuleSub
                             )
                             MainDestination.My -> MyScreen(
                                 onOpenSettings = onOpenSettings,

+ 100 - 77
app/src/main/java/io/legado/app/ui/main/bookshelf/BookItem.kt

@@ -26,7 +26,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
 import androidx.compose.material3.HorizontalDivider
-import androidx.compose.material3.MaterialTheme
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
@@ -36,12 +35,12 @@ import androidx.compose.ui.graphics.Brush
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Shadow
 import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.text.AnnotatedString
 import androidx.compose.ui.text.SpanStyle
 import androidx.compose.ui.text.buildAnnotatedString
+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.text.withStyle
 import androidx.compose.ui.unit.dp
 import io.legado.app.R
@@ -49,13 +48,11 @@ import io.legado.app.constant.BookType
 import io.legado.app.ui.config.bookshelfConfig.BookshelfConfig
 import io.legado.app.ui.config.themeConfig.ThemeConfig
 import io.legado.app.ui.theme.LegadoTheme
-import io.legado.app.ui.widget.components.card.GlassCard
 import io.legado.app.ui.widget.components.card.NormalCard
 import io.legado.app.ui.widget.components.card.TextCard
-import io.legado.app.ui.widget.components.cover.CoilBookCover
 import io.legado.app.ui.widget.components.cover.BookshelfCover
+import io.legado.app.ui.widget.components.cover.CoilBookCover
 import io.legado.app.ui.widget.components.icon.AppIcon
-import io.legado.app.ui.widget.components.icon.AppIcons
 import io.legado.app.ui.widget.components.text.AppText
 import io.legado.app.utils.splitNotBlank
 import io.legado.app.utils.toTimeAgo
@@ -90,10 +87,11 @@ fun BookshelfItem(
     onClick: () -> Unit,
     onLongClick: (() -> Unit)?
 ) {
-    val containerColor = if (!isGrid && BookshelfConfig.bookshelfCardColor != 0) {
-        Color(BookshelfConfig.bookshelfCardColor)
-    } else if (ThemeConfig.enableDeepPersonalization && ThemeConfig.secondaryThemeColor != 0) {
-        Color(ThemeConfig.secondaryThemeColor)
+    val isDark = LegadoTheme.isDark
+    val bookshelfCardColor =
+        if (isDark) BookshelfConfig.bookshelfCardColorDark else BookshelfConfig.bookshelfCardColor
+    val containerColor = if (!isGrid && bookshelfCardColor != 0) {
+        Color(bookshelfCardColor)
     } else {
         LegadoTheme.colorScheme.cardContainer
     }
@@ -189,7 +187,7 @@ fun BookshelfItem(
                     Box(
                         modifier = Modifier
                             .align(Alignment.Top)
-                            .width(if (!isCompact) 80.dp else 56.dp)
+                            .width(if (!isCompact) 84.dp else 56.dp)
                             .padding(end = 8.dp)
                     ) {
                         Box(
@@ -209,11 +207,13 @@ fun BookshelfItem(
                         }
                     }
                     Column(
-                        modifier = Modifier.weight(1f).padding(top = 8.dp, bottom = 8.dp, end = 8.dp),
+                        modifier = Modifier
+                            .weight(1f)
+                            .padding(top = 8.dp, bottom = 8.dp, end = 8.dp),
                         verticalArrangement = Arrangement.Center
                     ) {
                     Row(
-                        verticalAlignment = Alignment.CenterVertically
+                        verticalAlignment = Alignment.Top
                     ) {
                         AppText(
                             text = title,
@@ -222,11 +222,15 @@ fun BookshelfItem(
                             } else {
                                 LegadoTheme.typography.titleMediumEmphasized
                             },
-                            maxLines = 1,
+                            maxLines = BookshelfConfig.bookshelfTitleMaxLines,
                             overflow = TextOverflow.Ellipsis,
                             modifier = Modifier.weight(1f)
                         )
-                        titleEnd?.invoke()
+                        titleEnd?.let {
+                            Box(modifier = Modifier.padding(top = 4.dp)) {
+                                it.invoke()
+                            }
+                        }
                     }
                         subTitle?.let {
                             AppText(
@@ -296,68 +300,82 @@ fun BookGroupCover(
                 modifier = Modifier.fillMaxSize()
             )
         } else {
-            Column(modifier = Modifier.fillMaxSize()) {
-                Row(modifier = Modifier.weight(1f)) {
-                    Box(
-                        modifier = Modifier
-                            .weight(1f)
-                            .fillMaxHeight()
-                            .padding(1.dp)
-                    ) {
-                        books.getOrNull(0)?.let {
-                            CoilBookCover(
-                                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 {
-                            CoilBookCover(
-                                name = it.name,
-                                author = it.author,
-                                path = it.getDisplayCover(),
-                                modifier = Modifier.fillMaxSize()
-                            )
-                        }
+            Box(
+                modifier = Modifier.run {
+                    if (BookshelfConfig.bookshelfCoverShadow) {
+                        background(LegadoTheme.colorScheme.surface)
+                    } else {
+                        this
                     }
                 }
-                Row(modifier = Modifier.weight(1f)) {
-                    Box(
-                        modifier = Modifier
-                            .weight(1f)
-                            .fillMaxHeight()
-                            .padding(1.dp)
-                    ) {
-                        books.getOrNull(2)?.let {
-                            CoilBookCover(
-                                name = it.name,
-                                author = it.author,
-                                path = it.getDisplayCover(),
-                                modifier = Modifier.fillMaxSize()
-                            )
+            ) {
+                Column(
+                    modifier = Modifier
+                        .fillMaxSize()
+                        .padding(horizontal = 1.dp),
+                ) {
+                    Row(modifier = Modifier.weight(1f)) {
+                        Box(
+                            modifier = Modifier
+                                .weight(1f)
+                                .fillMaxHeight()
+                                .padding(1.dp)
+                        ) {
+                            books.getOrNull(0)?.let {
+                                CoilBookCover(
+                                    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 {
+                                CoilBookCover(
+                                    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 {
-                            CoilBookCover(
-                                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 {
+                                CoilBookCover(
+                                    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 {
+                                CoilBookCover(
+                                    name = it.name,
+                                    author = it.author,
+                                    path = it.getDisplayCover(),
+                                    modifier = Modifier.fillMaxSize()
+                                )
+                            }
                         }
                     }
                 }
@@ -482,13 +500,16 @@ fun BookGroupItemHorizontalCovers(
     onBookClick: ((BookShelfItem) -> Unit)? = null
 ) {
     Column {
+        val isDark = LegadoTheme.isDark
+        val bookshelfCardColor =
+            if (isDark) BookshelfConfig.bookshelfCardColorDark else BookshelfConfig.bookshelfCardColor
         NormalCard(
             modifier = modifier
                 .fillMaxWidth()
                 .padding(all = 4.dp),
             cornerRadius = 12.dp,
-            containerColor = if (BookshelfConfig.bookshelfCardColor != 0) {
-                Color(BookshelfConfig.bookshelfCardColor)
+            containerColor = if (bookshelfCardColor != 0) {
+                Color(bookshelfCardColor)
             } else {
                 LegadoTheme.colorScheme.cardContainer
             },
@@ -634,7 +655,9 @@ fun BookItem(
                 path = book.getDisplayCover(),
                 isUpdating = isUpdating,
                 modifier = modifier,
-                coverModifier = Modifier.fillMaxWidth().aspectRatio(5f / 7f),
+                coverModifier = Modifier
+                    .fillMaxWidth()
+                    .aspectRatio(5f / 7f),
                 sourceOrigin = book.origin,
                 badgeText = if (layoutMode != 0) unreadText else null,
                 showBadgeDot = BookshelfConfig.showUnread && BookshelfConfig.showUnreadNew && book.isNew,

+ 173 - 115
app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfConfigSheet.kt

@@ -3,11 +3,16 @@ package io.legado.app.ui.main.bookshelf
 import android.content.res.Configuration
 import androidx.compose.animation.AnimatedVisibility
 import androidx.compose.animation.animateContentSize
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
 import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.fillMaxWidth
 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
@@ -17,6 +22,8 @@ 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.graphics.toArgb
 import androidx.compose.ui.platform.LocalConfiguration
 import androidx.compose.ui.res.stringArrayResource
@@ -27,8 +34,8 @@ import io.legado.app.ui.config.bookshelfConfig.BookshelfConfig
 import io.legado.app.ui.config.themeConfig.LabelColorManageSheet
 import io.legado.app.ui.config.themeConfig.ThemeConfig
 import io.legado.app.ui.theme.LegadoTheme
-import io.legado.app.ui.widget.components.card.GlassCard
 import io.legado.app.ui.widget.components.dialog.ColorPickerSheet
+import io.legado.app.ui.widget.components.divider.PillDivider
 import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
 import io.legado.app.ui.widget.components.settingItem.CompactClickableSettingItem
 import io.legado.app.ui.widget.components.settingItem.CompactDropdownSettingItem
@@ -45,15 +52,13 @@ fun BookshelfConfigSheet(
     val isLandscape = configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
     var showLabelColorManage by remember { mutableStateOf(false) }
     var showColorPicker by remember { mutableStateOf(false) }
+    var showColorPickerDark by remember { mutableStateOf(false) }
 
     AppModalBottomSheet(
         title = stringResource(R.string.bookshelf_layout),
         show = show,
         onDismissRequest = onDismissRequest
     ) {
-        GlassCard() {
-
-        }
         Column(
             modifier = Modifier
                 .fillMaxWidth()
@@ -99,21 +104,12 @@ fun BookshelfConfigSheet(
                 if (isLandscape) BookshelfConfig.bookshelfFolderLayoutModeLandscape
                 else BookshelfConfig.bookshelfFolderLayoutModePortrait
 
-            CompactDropdownSettingItem(
-                title = stringResource(R.string.layout_mode),
-                description = stringResource(if (isLandscape) R.string.screen_landscape else R.string.screen_portrait),
-                selectedValue = layoutMode.toString(),
-                displayEntries = arrayOf(stringResource(R.string.layout_mode_list),stringResource(R.string.layout_mode_grid)),
-                entryValues = arrayOf("0", "1"),
-                onValueChange = {
-                    if (isLandscape) BookshelfConfig.bookshelfLayoutModeLandscape = it.toInt()
-                    else BookshelfConfig.bookshelfLayoutModePortrait = it.toInt()
-                }
-            )
-
             // Folder Layout Mode
             AnimatedVisibility(visible = BookshelfConfig.bookGroupStyle == 2) {
                 Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
+
+                    PillDivider()
+
                     CompactDropdownSettingItem(
                         title = "文件夹布局模式",
                         description = stringResource(if (isLandscape) R.string.screen_landscape else R.string.screen_portrait),
@@ -157,34 +153,55 @@ fun BookshelfConfigSheet(
                             }
                         )
                     }
-                }
-            }
 
-            AnimatedVisibility(
-                visible = BookshelfConfig.bookGroupStyle == 2 && folderLayoutMode == 0
-            ) {
-                Column(
-                    verticalArrangement = Arrangement.spacedBy(8.dp)
-                ) {
-                    CompactDropdownSettingItem(
-                        title = "文件夹列表样式",
-                        selectedValue = BookshelfConfig.bookshelfGroupListStyle.toString(),
-                        displayEntries = arrayOf("默认", "紧凑", "横排封面"),
-                        entryValues = arrayOf("0", "1", "2"),
-                        onValueChange = { BookshelfConfig.bookshelfGroupListStyle = it.toInt() }
-                    )
-                    AnimatedVisibility(visible = BookshelfConfig.bookshelfGroupListStyle == 2) {
-                        CompactSliderSettingItem(
-                            title = "横排封面数量",
-                            value = BookshelfConfig.bookshelfGroupCoverCount.toFloat(),
-                            valueRange = 1f..10f,
-                            steps = 9,
-                            onValueChange = { BookshelfConfig.bookshelfGroupCoverCount = it.toInt() }
-                        )
+                    AnimatedVisibility(
+                        visible = BookshelfConfig.bookGroupStyle == 2 && folderLayoutMode == 0
+                    ) {
+                        Column(
+                            verticalArrangement = Arrangement.spacedBy(8.dp)
+                        ) {
+                            CompactDropdownSettingItem(
+                                title = "文件夹列表样式",
+                                selectedValue = BookshelfConfig.bookshelfGroupListStyle.toString(),
+                                displayEntries = arrayOf("默认", "紧凑", "横排封面"),
+                                entryValues = arrayOf("0", "1", "2"),
+                                onValueChange = {
+                                    BookshelfConfig.bookshelfGroupListStyle = it.toInt()
+                                }
+                            )
+                            AnimatedVisibility(visible = BookshelfConfig.bookshelfGroupListStyle == 2) {
+                                CompactSliderSettingItem(
+                                    title = "横排封面数量",
+                                    value = BookshelfConfig.bookshelfGroupCoverCount.toFloat(),
+                                    valueRange = 1f..10f,
+                                    steps = 9,
+                                    onValueChange = {
+                                        BookshelfConfig.bookshelfGroupCoverCount = it.toInt()
+                                    }
+                                )
+                            }
+                        }
                     }
+
+                    PillDivider()
                 }
             }
 
+            CompactDropdownSettingItem(
+                title = stringResource(R.string.layout_mode),
+                description = stringResource(if (isLandscape) R.string.screen_landscape else R.string.screen_portrait),
+                selectedValue = layoutMode.toString(),
+                displayEntries = arrayOf(
+                    stringResource(R.string.layout_mode_list),
+                    stringResource(R.string.layout_mode_grid)
+                ),
+                entryValues = arrayOf("0", "1"),
+                onValueChange = {
+                    if (isLandscape) BookshelfConfig.bookshelfLayoutModeLandscape = it.toInt()
+                    else BookshelfConfig.bookshelfLayoutModePortrait = it.toInt()
+                }
+            )
+
             AnimatedVisibility(
                 visible = layoutMode == 1
             ) {
@@ -243,103 +260,135 @@ fun BookshelfConfigSheet(
                     )
 
                     CompactClickableSettingItem(
-                        title = "卡片背景颜色",
+                        title = "日间卡片背景颜色",
                         color = LegadoTheme.colorScheme.surface,
-                        onClick = { showColorPicker = true }
+                        onClick = { showColorPicker = true },
+                        trailingContent = {
+                            if (BookshelfConfig.bookshelfCardColor != 0) {
+                                Box(
+                                    modifier = Modifier
+                                        .size(20.dp)
+                                        .clip(CircleShape)
+                                        .background(Color(BookshelfConfig.bookshelfCardColor))
+                                        .border(
+                                            1.dp,
+                                            MaterialTheme.colorScheme.outlineVariant,
+                                            CircleShape
+                                        )
+                                )
+                            }
+                        }
                     )
 
-                    val isHorizontalCovers = BookshelfConfig.bookGroupStyle == 2 && BookshelfConfig.bookshelfGroupListStyle == 2
+                    CompactClickableSettingItem(
+                        title = "夜间卡片背景颜色",
+                        color = LegadoTheme.colorScheme.surface,
+                        onClick = { showColorPickerDark = true },
+                        trailingContent = {
+                            if (BookshelfConfig.bookshelfCardColorDark != 0) {
+                                Box(
+                                    modifier = Modifier
+                                        .size(20.dp)
+                                        .clip(CircleShape)
+                                        .background(Color(BookshelfConfig.bookshelfCardColorDark))
+                                        .border(
+                                            1.dp,
+                                            MaterialTheme.colorScheme.outlineVariant,
+                                            CircleShape
+                                        )
+                                )
+                            }
+                        }
+                    )
+
+                    if (BookshelfConfig.bookGroupStyle != 2) {
+                        CompactSwitchSettingItem(
+                            title = stringResource(R.string.compact_mode),
+                            checked = BookshelfConfig.bookshelfLayoutCompact,
+                            color = LegadoTheme.colorScheme.surface,
+                            onCheckedChange = { BookshelfConfig.bookshelfLayoutCompact = it }
+                        )
+                    }
 
-                    AnimatedVisibility(visible = !isHorizontalCovers) {
+                    val listColCount =
+                        if (isLandscape) BookshelfConfig.bookshelfLayoutListLandscape else BookshelfConfig.bookshelfLayoutListPortrait
+                    CompactSliderSettingItem(
+                        title = stringResource(R.string.number_rows_columns),
+                        value = listColCount.toFloat(),
+                        valueRange = 1f..5f,
+                        steps = 4,
+                        onValueChange = {
+                            if (isLandscape) BookshelfConfig.bookshelfLayoutListLandscape =
+                                it.toInt()
+                            else BookshelfConfig.bookshelfLayoutListPortrait = it.toInt()
+                        }
+                    )
+
+                    CompactSwitchSettingItem(
+                        title = "显示更多信息",
+                        checked = BookshelfConfig.showBookIntro,
+                        color = LegadoTheme.colorScheme.surface,
+                        onCheckedChange = { BookshelfConfig.showBookIntro = it }
+                    )
+
+                    AnimatedVisibility(visible = BookshelfConfig.showBookIntro) {
                         Column(
                             verticalArrangement = Arrangement.spacedBy(8.dp)
                         ) {
-                            if (BookshelfConfig.bookGroupStyle != 2) {
-                                CompactSwitchSettingItem(
-                                    title = stringResource(R.string.compact_mode),
-                                    checked = BookshelfConfig.bookshelfLayoutCompact,
-                                    color = LegadoTheme.colorScheme.surface,
-                                    onCheckedChange = { BookshelfConfig.bookshelfLayoutCompact = it }
-                                )
-                            }
-
-                            val listColCount =
-                                if (isLandscape) BookshelfConfig.bookshelfLayoutListLandscape else BookshelfConfig.bookshelfLayoutListPortrait
-                            CompactSliderSettingItem(
-                                title = stringResource(R.string.number_rows_columns),
-                                value = listColCount.toFloat(),
-                                valueRange = 1f..5f,
-                                steps = 4,
-                                onValueChange = {
-                                    if (isLandscape) BookshelfConfig.bookshelfLayoutListLandscape =
-                                        it.toInt()
-                                    else BookshelfConfig.bookshelfLayoutListPortrait = it.toInt()
+                            CompactSwitchSettingItem(
+                                title = "显示最新章节",
+                                checked = BookshelfConfig.bookshelfShowLatestChapter,
+                                color = LegadoTheme.colorScheme.surface,
+                                onCheckedChange = {
+                                    BookshelfConfig.bookshelfShowLatestChapter = it
                                 }
                             )
 
                             CompactSwitchSettingItem(
-                                title = "显示更多信息",
-                                checked = BookshelfConfig.showBookIntro,
+                                title = "显示简介",
+                                checked = BookshelfConfig.bookshelfShowIntro,
                                 color = LegadoTheme.colorScheme.surface,
-                                onCheckedChange = { BookshelfConfig.showBookIntro = it }
+                                onCheckedChange = { BookshelfConfig.bookshelfShowIntro = it }
                             )
-
-                            AnimatedVisibility(visible = BookshelfConfig.showBookIntro) {
+                            AnimatedVisibility(
+                                visible = BookshelfConfig.bookshelfShowIntro
+                            ) {
+                                CompactSliderSettingItem(
+                                    title = "简介行数",
+                                    description = if (BookshelfConfig.bookshelfIntroMaxLines == 0) "显示全部简介" else "显示 ${BookshelfConfig.bookshelfIntroMaxLines} 行简介",
+                                    value = BookshelfConfig.bookshelfIntroMaxLines.toFloat(),
+                                    valueRange = 0f..10f,
+                                    steps = 10,
+                                    onValueChange = {
+                                        BookshelfConfig.bookshelfIntroMaxLines = it.toInt()
+                                    }
+                                )
+                            }
+                            CompactSwitchSettingItem(
+                                title = "显示标签",
+                                checked = BookshelfConfig.bookshelfShowTag,
+                                color = LegadoTheme.colorScheme.surface,
+                                onCheckedChange = { BookshelfConfig.bookshelfShowTag = it }
+                            )
+                            AnimatedVisibility(
+                                visible = BookshelfConfig.bookshelfShowTag
+                            ) {
                                 Column(
                                     verticalArrangement = Arrangement.spacedBy(8.dp)
                                 ) {
                                     CompactSwitchSettingItem(
-                                        title = "显示最新章节",
-                                        checked = BookshelfConfig.bookshelfShowLatestChapter,
+                                        title = "自定义标签颜色",
+                                        checked = ThemeConfig.enableCustomTagColors,
                                         color = LegadoTheme.colorScheme.surface,
-                                        onCheckedChange = { BookshelfConfig.bookshelfShowLatestChapter = it }
+                                        onCheckedChange = { ThemeConfig.enableCustomTagColors = it }
                                     )
-
-                                    CompactSwitchSettingItem(
-                                        title = "显示简介",
-                                        checked = BookshelfConfig.bookshelfShowIntro,
-                                        color = LegadoTheme.colorScheme.surface,
-                                        onCheckedChange = { BookshelfConfig.bookshelfShowIntro = it }
-                                    )
-                                    AnimatedVisibility(
-                                        visible = BookshelfConfig.bookshelfShowIntro
-                                    ) {
-                                        CompactSliderSettingItem(
-                                            title = "简介行数",
-                                            description = if (BookshelfConfig.bookshelfIntroMaxLines == 0) "显示全部简介" else "显示 ${BookshelfConfig.bookshelfIntroMaxLines} 行简介",
-                                            value = BookshelfConfig.bookshelfIntroMaxLines.toFloat(),
-                                            valueRange = 0f..10f,
-                                            steps = 10,
-                                            onValueChange = { BookshelfConfig.bookshelfIntroMaxLines = it.toInt() }
+                                    AnimatedVisibility(visible = ThemeConfig.enableCustomTagColors) {
+                                        CompactClickableSettingItem(
+                                            title = "管理标签颜色",
+                                            color = LegadoTheme.colorScheme.surface,
+                                            onClick = { showLabelColorManage = true }
                                         )
                                     }
-                                    CompactSwitchSettingItem(
-                                        title = "显示标签",
-                                        checked = BookshelfConfig.bookshelfShowTag,
-                                        color = LegadoTheme.colorScheme.surface,
-                                        onCheckedChange = { BookshelfConfig.bookshelfShowTag = it }
-                                    )
-                                    AnimatedVisibility(
-                                        visible = BookshelfConfig.bookshelfShowTag
-                                    ) {
-                                        Column(
-                                            verticalArrangement = Arrangement.spacedBy(8.dp)
-                                        ) {
-                                            CompactSwitchSettingItem(
-                                                title = "自定义标签颜色",
-                                                checked = ThemeConfig.enableCustomTagColors,
-                                                color = LegadoTheme.colorScheme.surface,
-                                                onCheckedChange = { ThemeConfig.enableCustomTagColors = it }
-                                            )
-                                            AnimatedVisibility(visible = ThemeConfig.enableCustomTagColors) {
-                                                CompactClickableSettingItem(
-                                                    title = "管理标签颜色",
-                                                    color = LegadoTheme.colorScheme.surface,
-                                                    onClick = { showLabelColorManage = true }
-                                                )
-                                            }
-                                        }
-                                    }
                                 }
                             }
                         }
@@ -347,6 +396,8 @@ fun BookshelfConfigSheet(
                 }
             }
 
+
+
             CompactSliderSettingItem(
                 title = stringResource(R.string.max_title_lines),
                 value = BookshelfConfig.bookshelfTitleMaxLines.toFloat(),
@@ -448,5 +499,12 @@ fun BookshelfConfigSheet(
             onDismissRequest = { showColorPicker = false },
             onColorSelected = { BookshelfConfig.bookshelfCardColor = it }
         )
+
+        ColorPickerSheet(
+            show = showColorPickerDark,
+            initialColor = if (BookshelfConfig.bookshelfCardColorDark != 0) BookshelfConfig.bookshelfCardColorDark else LegadoTheme.colorScheme.surfaceVariant.toArgb(),
+            onDismissRequest = { showColorPickerDark = false },
+            onColorSelected = { BookshelfConfig.bookshelfCardColorDark = it }
+        )
     }
 }

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

@@ -778,7 +778,7 @@ fun BookshelfScreen(
                             ),
                         contentPadding = adaptiveContentPaddingBookshelf(
                             top = paddingValues.calculateTopPadding(),
-                            bottom = if (ThemeConfig.useFloatingBottomBar) 120.dp else 8.dp,
+                            bottom = if (ThemeConfig.useFloatingBottomBar || ThemeConfig.enableBlur) 120.dp else 8.dp,
                             horizontal = if (isGridMode) 8.dp else 4.dp
                         ),
                         verticalArrangement = Arrangement.spacedBy(if (isGridMode) 8.dp else 0.dp),
@@ -1342,7 +1342,7 @@ fun BookshelfPage(
                 ),
             contentPadding = adaptiveContentPaddingBookshelf(
                 top = paddingValues.calculateTopPadding(),
-                bottom = if (ThemeConfig.useFloatingBottomBar) 120.dp else 8.dp,
+                bottom = if (ThemeConfig.useFloatingBottomBar || ThemeConfig.enableBlur) 120.dp else 8.dp,
                 horizontal = if (isGridMode) 8.dp else 4.dp
             ),
             verticalArrangement = Arrangement.spacedBy(if (isGridMode) 8.dp else 0.dp),

+ 7 - 5
app/src/main/java/io/legado/app/ui/main/rss/RssScreen.kt

@@ -45,10 +45,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import io.legado.app.R
 import io.legado.app.data.entities.RssSource
 import io.legado.app.ui.login.SourceLoginActivity
-import io.legado.app.ui.rss.favorites.RssFavoritesActivity
 import io.legado.app.ui.rss.source.edit.RssSourceEditActivity
 import io.legado.app.ui.rss.source.manage.RssSourceActivity
-import io.legado.app.ui.rss.subscription.RuleSubActivity
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.theme.adaptiveContentPadding
 import io.legado.app.ui.widget.components.SourceIcon
@@ -71,7 +69,9 @@ import org.koin.androidx.compose.koinViewModel
 fun RssScreen(
     viewModel: RssViewModel = koinViewModel(),
     onOpenSort: (sourceUrl: String, sortUrl: String?, key: String?) -> Unit,
-    onOpenRead: (title: String?, origin: String, link: String?, openUrl: String?) -> Unit
+    onOpenRead: (title: String?, origin: String, link: String?, openUrl: String?) -> Unit,
+    onOpenRuleSub: () -> Unit,
+    onOpenFavorites: () -> Unit,
 ) {
     val context = LocalContext.current
     val uiState by viewModel.uiState.collectAsStateWithLifecycle()
@@ -82,6 +82,8 @@ fun RssScreen(
     val currentContext by rememberUpdatedState(context)
     val currentOnOpenSort by rememberUpdatedState(onOpenSort)
     val currentOnOpenRead by rememberUpdatedState(onOpenRead)
+    val currentOnOpenRuleSub by rememberUpdatedState(onOpenRuleSub)
+    val currentOnOpenFavorites by rememberUpdatedState(onOpenFavorites)
 
     LaunchedEffect(viewModel) {
         viewModel.effects.collectLatest { effect ->
@@ -112,11 +114,11 @@ fun RssScreen(
                 }
 
                 RssEffect.OpenRuleSub -> {
-                    currentContext.startActivity<RuleSubActivity>()
+                    currentOnOpenRuleSub()
                 }
 
                 RssEffect.OpenFavorites -> {
-                    currentContext.startActivity<RssFavoritesActivity>()
+                    currentOnOpenFavorites()
                 }
 
                 RssEffect.OpenSourceManage -> {

+ 0 - 52
app/src/main/java/io/legado/app/ui/rss/favorites/RssFavoritesActivity.kt

@@ -1,52 +0,0 @@
-package io.legado.app.ui.rss.favorites
-
-import android.os.Bundle
-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 io.legado.app.base.BaseComposeActivity
-import io.legado.app.ui.rss.read.RssReadRouteScreen
-
-/**
- * 收藏夹
- */
-class RssFavoritesActivity : BaseComposeActivity() {
-
-    override fun onCreate(savedInstanceState: Bundle?) {
-        super.onCreate(savedInstanceState)
-    }
-
-    @Composable
-    override fun Content() {
-        var readArgs by remember {
-            mutableStateOf<ReadArgs?>(null)
-        }
-
-        val currentReadArgs = readArgs
-        if (currentReadArgs == null) {
-            RssFavoritesScreen(
-                onBackClick = { finish() },
-                onOpenRead = { title, origin, link, openUrl ->
-                    readArgs = ReadArgs(title, origin, link, openUrl)
-                }
-            )
-        } else {
-            RssReadRouteScreen(
-                title = currentReadArgs.title,
-                origin = currentReadArgs.origin,
-                link = currentReadArgs.link,
-                openUrl = currentReadArgs.openUrl,
-                onBackClick = { readArgs = null }
-            )
-        }
-    }
-
-    private data class ReadArgs(
-        val title: String?,
-        val origin: String,
-        val link: String?,
-        val openUrl: String?
-    )
-}

+ 2 - 2
app/src/main/java/io/legado/app/ui/rss/favorites/RssFavoritesScreen.kt

@@ -28,7 +28,6 @@ import androidx.compose.ui.Modifier
 import androidx.compose.ui.layout.ContentScale
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.dp
-import androidx.lifecycle.viewmodel.compose.viewModel
 import io.legado.app.R
 import io.legado.app.data.entities.RssStar
 import io.legado.app.ui.theme.adaptiveContentPadding
@@ -41,13 +40,14 @@ import io.legado.app.ui.widget.components.dialog.TextListInputDialog
 import io.legado.app.ui.widget.components.divider.PillDivider
 import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
 import io.legado.app.ui.widget.components.rules.RuleListScaffold
+import org.koin.androidx.compose.koinViewModel
 
 @OptIn(ExperimentalMaterial3Api::class)
 @Composable
 fun RssFavoritesScreen(
     onBackClick: () -> Unit,
     onOpenRead: (title: String?, origin: String, link: String?, openUrl: String?) -> Unit,
-    viewModel: RssFavoritesViewModel = viewModel()
+    viewModel: RssFavoritesViewModel = koinViewModel()
 ) {
     val state by viewModel.state.collectAsState()
     val groups by viewModel.groups.collectAsState()

+ 0 - 23
app/src/main/java/io/legado/app/ui/rss/subscription/RuleSubActivity.kt

@@ -1,23 +0,0 @@
-package io.legado.app.ui.rss.subscription
-
-import android.os.Bundle
-import androidx.compose.runtime.Composable
-import io.legado.app.base.BaseComposeActivity
-
-/**
- * 规则订阅界面
- */
-class RuleSubActivity : BaseComposeActivity() {
-
-    override fun onCreate(savedInstanceState: Bundle?) {
-        super.onCreate(savedInstanceState)
-    }
-
-    @Composable
-    override fun Content() {
-        RuleSubScreen(
-            onBackClick = { finish() }
-        )
-    }
-
-}

+ 3 - 3
app/src/main/java/io/legado/app/ui/rss/subscription/RuleSubScreen.kt

@@ -18,7 +18,6 @@ import androidx.compose.material.icons.filled.Add
 import androidx.compose.material.icons.filled.Edit
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
-import io.legado.app.ui.widget.components.AppFloatingActionButton
 import androidx.compose.material3.Icon
 import androidx.compose.material3.IconButton
 import androidx.compose.material3.SnackbarHostState
@@ -37,7 +36,6 @@ import androidx.compose.ui.res.stringArrayResource
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.dp
 import androidx.core.net.toUri
-import androidx.lifecycle.viewmodel.compose.viewModel
 import io.legado.app.R
 import io.legado.app.data.entities.RuleSub
 import io.legado.app.data.entities.RuleSubType
@@ -46,6 +44,7 @@ import io.legado.app.ui.association.ImportReplaceRuleDialog
 import io.legado.app.ui.association.ImportRssSourceDialog
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.theme.adaptiveContentPadding
+import io.legado.app.ui.widget.components.AppFloatingActionButton
 import io.legado.app.ui.widget.components.AppTextField
 import io.legado.app.ui.widget.components.EmptyMessage
 import io.legado.app.ui.widget.components.alert.AppAlertDialog
@@ -57,12 +56,13 @@ import io.legado.app.ui.widget.components.rules.RuleListScaffold
 import io.legado.app.ui.widget.components.text.AppText
 import io.legado.app.utils.showDialogFragment
 import io.legado.app.utils.toastOnUi
+import org.koin.androidx.compose.koinViewModel
 
 @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
 @Composable
 fun RuleSubScreen(
     onBackClick: () -> Unit,
-    viewModel: RuleSubViewModel = viewModel()
+    viewModel: RuleSubViewModel = koinViewModel()
 ) {
     val context = LocalContext.current
     val state by viewModel.state.collectAsState()

+ 2 - 2
app/src/main/java/io/legado/app/ui/theme/AdaptivePadding.kt

@@ -55,7 +55,7 @@ fun adaptiveContentPadding(
     bottom: Dp
 ): PaddingValues {
     val horizontal = if (ThemeResolver.isMiuixEngine(composeEngine)) 12.dp else 16.dp
-    val adjustedTop = if (ThemeResolver.isMiuixEngine(composeEngine)) top + 8.dp else top
+    val adjustedTop = if (ThemeResolver.isMiuixEngine(composeEngine)) top + 12.dp else top + 16.dp
     return PaddingValues(
         top = adjustedTop,
         bottom = bottom,
@@ -70,7 +70,7 @@ fun adaptiveContentPadding(
     bottom: Dp,
     horizontal: Dp
 ): PaddingValues {
-    val adjustedTop = if (ThemeResolver.isMiuixEngine(composeEngine)) top + 6.dp else top + 4.dp
+    val adjustedTop = if (ThemeResolver.isMiuixEngine(composeEngine)) top + 12.dp else top + 16.dp
     return PaddingValues(
         top = adjustedTop,
         bottom = bottom,

+ 69 - 60
app/src/main/java/io/legado/app/ui/widget/components/book/SearchBookItem.kt

@@ -5,16 +5,16 @@ import androidx.compose.animation.ExperimentalSharedTransitionApi
 import androidx.compose.animation.SharedTransitionScope
 import androidx.compose.foundation.clickable
 import androidx.compose.foundation.horizontalScroll
+import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.IntrinsicSize
 import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.RowScope
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.rememberScrollState
 import androidx.compose.foundation.shape.RoundedCornerShape
@@ -23,37 +23,18 @@ import androidx.compose.material.icons.filled.Check
 import androidx.compose.material.icons.filled.Shuffle
 import androidx.compose.material3.Surface
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
 import io.legado.app.data.entities.SearchBook
 import io.legado.app.domain.model.BookShelfState
 import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.widget.components.card.TextCard
 import io.legado.app.ui.widget.components.cover.CoilBookCover
 import io.legado.app.ui.widget.components.text.AppText
-import kotlinx.coroutines.flow.Flow
-
-@Composable
-fun SearchBookListItem(
-    book: SearchBook,
-    shelfState: Flow<BookShelfState>,
-    onClick: () -> Unit,
-    modifier: Modifier = Modifier,
-) {
-    val currentShelfState by shelfState.collectAsState(initial = BookShelfState.NOT_IN_SHELF)
-    SearchBookListItem(
-        book = book,
-        shelfState = currentShelfState,
-        onClick = onClick,
-        modifier = modifier,
-    )
-}
 
 @OptIn(ExperimentalSharedTransitionApi::class)
 @Composable
@@ -72,17 +53,39 @@ fun SearchBookListItem(
             .clickable(onClick = onClick)
             .padding(horizontal = 16.dp, vertical = 8.dp)
     ) {
-        CoilBookCover(
-            name = book.name,
-            author = book.author,
-            path = book.coverUrl,
-            modifier = Modifier.width(72.dp).aspectRatio(5f / 7f),
-            sourceOrigin = book.origin,
-            sharedTransitionScope = sharedTransitionScope,
-            animatedVisibilityScope = animatedVisibilityScope,
-            sharedCoverKey = sharedCoverKey,
-            showLoadingPlaceholder = sharedCoverKey == null
-        )
+        Box(modifier = Modifier
+            .width(72.dp)
+            .aspectRatio(5f / 7f)) {
+            CoilBookCover(
+                name = book.name,
+                author = book.author,
+                path = book.coverUrl,
+                modifier = Modifier.fillMaxSize(),
+                sourceOrigin = book.origin,
+                sharedTransitionScope = sharedTransitionScope,
+                animatedVisibilityScope = animatedVisibilityScope,
+                sharedCoverKey = sharedCoverKey,
+                showLoadingPlaceholder = sharedCoverKey == null
+            )
+
+            val shelfIcon = when (shelfState) {
+                BookShelfState.IN_SHELF -> Icons.Default.Check
+                BookShelfState.SAME_NAME_AUTHOR -> Icons.Default.Shuffle
+                else -> null
+            }
+
+            if (shelfIcon != null) {
+                TextCard(
+                    icon = shelfIcon,
+                    modifier = Modifier
+                        .align(Alignment.TopEnd)
+                        .padding(2.dp),
+                    cornerRadius = 4.dp,
+                    horizontalPadding = 2.dp,
+                    verticalPadding = 2.dp
+                )
+            }
+        }
 
         Spacer(modifier = Modifier.width(8.dp))
 
@@ -151,22 +154,6 @@ fun SearchBookListItem(
     }
 }
 
-@Composable
-fun SearchBookGridItem(
-    book: SearchBook,
-    shelfState: Flow<BookShelfState>,
-    onClick: () -> Unit,
-    modifier: Modifier = Modifier,
-) {
-    val currentShelfState by shelfState.collectAsState(initial = BookShelfState.NOT_IN_SHELF)
-    SearchBookGridItem(
-        book = book,
-        shelfState = currentShelfState,
-        onClick = onClick,
-        modifier = modifier,
-    )
-}
-
 @OptIn(ExperimentalSharedTransitionApi::class)
 @Composable
 fun SearchBookGridItem(
@@ -184,19 +171,41 @@ fun SearchBookGridItem(
             .clickable(onClick = onClick)
             .padding(4.dp)
     ) {
-        CoilBookCover(
-            name = book.name,
-            author = book.author,
-            path = book.coverUrl,
+        Box(
             modifier = Modifier
                 .fillMaxWidth()
-                .aspectRatio(5f / 7f),
-            sourceOrigin = book.origin,
-            sharedTransitionScope = sharedTransitionScope,
-            animatedVisibilityScope = animatedVisibilityScope,
-            sharedCoverKey = sharedCoverKey,
-            showLoadingPlaceholder = sharedCoverKey == null
-        )
+                .aspectRatio(5f / 7f)
+        ) {
+            CoilBookCover(
+                name = book.name,
+                author = book.author,
+                path = book.coverUrl,
+                modifier = Modifier.fillMaxSize(),
+                sourceOrigin = book.origin,
+                sharedTransitionScope = sharedTransitionScope,
+                animatedVisibilityScope = animatedVisibilityScope,
+                sharedCoverKey = sharedCoverKey,
+                showLoadingPlaceholder = sharedCoverKey == null
+            )
+
+            val shelfIcon = when (shelfState) {
+                BookShelfState.IN_SHELF -> Icons.Default.Check
+                BookShelfState.SAME_NAME_AUTHOR -> Icons.Default.Shuffle
+                else -> null
+            }
+
+            if (shelfIcon != null) {
+                TextCard(
+                    icon = shelfIcon,
+                    modifier = Modifier
+                        .align(Alignment.TopEnd)
+                        .padding(2.dp),
+                    cornerRadius = 4.dp,
+                    horizontalPadding = 2.dp,
+                    verticalPadding = 2.dp
+                )
+            }
+        }
 
         Spacer(modifier = Modifier.height(4.dp))
 

+ 11 - 5
app/src/main/java/io/legado/app/ui/widget/components/divider/PillDivider.kt

@@ -27,12 +27,18 @@ fun PillDivider(
     val isMiuix = ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)
     if (isMiuix) {
         Box(
-            modifier = Modifier
+            modifier = modifier
                 .fillMaxWidth()
-                .padding(vertical = 2.dp)
-                .height(4.dp)
-                .background(MiuixTheme.colorScheme.dividerLine)
-        )
+                .padding(vertical = 4.dp),
+            contentAlignment = Alignment.Center
+        ) {
+            Box(
+                modifier = Modifier
+                    .fillMaxWidth(0.8f)
+                    .height(thickness)
+                    .background(MiuixTheme.colorScheme.dividerLine)
+            )
+        }
     } else {
         Box(
             modifier = modifier

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

@@ -48,7 +48,7 @@ fun CompactDropdownSettingItem(
     entryValues: Array<String>,
     description: String? = null,
     imageVector: ImageVector? = null,
-    color: Color? = LegadoTheme.colorScheme.surface,
+    color: Color? = LegadoTheme.colorScheme.onSheetContent,
     shape: Shape = MaterialTheme.shapes.small,
     onValueChange: (String) -> Unit
 ) {
@@ -126,7 +126,7 @@ fun CompactSliderSettingItem(
     steps: Int = 0,
     description: String? = null,
     imageVector: ImageVector? = null,
-    color: Color? = MaterialTheme.colorScheme.surface,
+    color: Color? = LegadoTheme.colorScheme.onSheetContent,
     shape: Shape = MaterialTheme.shapes.small,
     onValueChange: (Float) -> Unit
 ) {
@@ -222,7 +222,7 @@ fun CompactSwitchSettingItem(
     checked: Boolean,
     description: String? = null,
     imageVector: ImageVector? = null,
-    color: Color? = MaterialTheme.colorScheme.surface,
+    color: Color? = LegadoTheme.colorScheme.onSheetContent,
     shape: Shape = MaterialTheme.shapes.small,
     enabled: Boolean = true,
     onCheckedChange: (Boolean) -> Unit
@@ -260,7 +260,7 @@ fun CompactClickableSettingItem(
     title: String,
     description: String? = null,
     imageVector: ImageVector? = null,
-    color: Color? = MaterialTheme.colorScheme.surface,
+    color: Color? = LegadoTheme.colorScheme.onSheetContent,
     shape: Shape = MaterialTheme.shapes.small,
     trailingContent: (@Composable () -> Unit)? = null,
     onClick: () -> Unit