瀏覽代碼

[优化] 新增了新的外观设置、备份与恢复设置,现在可以在高级、测试处体验。
[修复] 封面状态不同步的问题、文件夹封面和书籍封面获取的问题、容器透明度不生效的问题等

HapeLee 7 月之前
父節點
當前提交
36649527ed
共有 25 個文件被更改,包括 2006 次插入 和 354 次删除
  1. 2 0
      app/src/main/java/io/legado/app/di/appModule.kt
  2. 20 67
      app/src/main/java/io/legado/app/model/BookCover.kt
  3. 1 1
      app/src/main/java/io/legado/app/ui/config/PrefDelegate.kt
  4. 19 3
      app/src/main/java/io/legado/app/ui/config/TestConfigActivity.kt
  5. 58 0
      app/src/main/java/io/legado/app/ui/config/backupConfig/BackupConfig.kt
  6. 615 0
      app/src/main/java/io/legado/app/ui/config/backupConfig/BackupConfigScreen.kt
  7. 74 0
      app/src/main/java/io/legado/app/ui/config/backupConfig/BackupConfigViewModel.kt
  8. 2 3
      app/src/main/java/io/legado/app/ui/config/themeConfig/BackgroundImageManageSheet.kt
  9. 200 0
      app/src/main/java/io/legado/app/ui/config/themeConfig/LauncherIconPickerSheet.kt
  10. 31 21
      app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfig.kt
  11. 648 18
      app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigScreen.kt
  12. 14 12
      app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigViewModel.kt
  13. 72 61
      app/src/main/java/io/legado/app/ui/main/bookshelf/BookItem.kt
  14. 87 66
      app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt
  15. 10 3
      app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt
  16. 58 6
      app/src/main/java/io/legado/app/ui/widget/components/AppScaffold.kt
  17. 1 4
      app/src/main/java/io/legado/app/ui/widget/components/GlassMediumFlexibleTopAppBar.kt
  18. 4 4
      app/src/main/java/io/legado/app/ui/widget/components/SplicedColumnGroup.kt
  19. 9 2
      app/src/main/java/io/legado/app/ui/widget/components/card/GlassCard.kt
  20. 13 5
      app/src/main/java/io/legado/app/ui/widget/components/cover/BookCover.kt
  21. 37 41
      app/src/main/java/io/legado/app/ui/widget/components/dialog/ColorPickerSheet.kt
  22. 0 1
      app/src/main/java/io/legado/app/ui/widget/components/importComponents/ImportComponents.kt
  23. 15 23
      app/src/main/java/io/legado/app/ui/widget/components/list/ListScaffold.kt
  24. 1 2
      app/src/main/java/io/legado/app/ui/widget/components/settingItem/SliderSettingItem.kt
  25. 15 11
      app/src/main/java/io/legado/app/ui/widget/image/CoverImageView.kt

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

@@ -28,6 +28,7 @@ import io.legado.app.ui.book.readRecord.ReadRecordViewModel
 import io.legado.app.ui.book.searchContent.SearchContentViewModel
 import io.legado.app.ui.book.toc.TocViewModel
 import io.legado.app.ui.book.toc.rule.TxtTocRuleViewModel
+import io.legado.app.ui.config.backupConfig.BackupConfigViewModel
 import io.legado.app.ui.config.coverConfig.CoverConfigViewModel
 import io.legado.app.ui.config.otherConfig.OtherConfigViewModel
 import io.legado.app.ui.config.readConfig.ReadConfigViewModel
@@ -94,6 +95,7 @@ val appModule = module {
     viewModelOf(::ReadConfigViewModel)
     viewModelOf(::CoverConfigViewModel)
     viewModelOf(::ThemeConfigViewModel)
+    viewModelOf(::BackupConfigViewModel)
     viewModelOf(::TocViewModel)
     viewModelOf(::RemoteBookViewModel)
     viewModelOf(::BookInfoViewModel)

+ 20 - 67
app/src/main/java/io/legado/app/model/BookCover.kt

@@ -3,7 +3,6 @@ package io.legado.app.model
 import android.annotation.SuppressLint
 import android.content.Context
 import android.graphics.Bitmap
-import android.graphics.Color
 import android.graphics.drawable.Drawable
 import androidx.annotation.Keep
 import androidx.core.graphics.drawable.toDrawable
@@ -35,8 +34,6 @@ import io.legado.app.model.analyzeRule.AnalyzeUrl
 import io.legado.app.utils.BitmapUtils
 import io.legado.app.utils.GSON
 import io.legado.app.utils.fromJsonObject
-import io.legado.app.utils.getPrefBoolean
-import io.legado.app.utils.getPrefInt
 import io.legado.app.utils.getPrefString
 import kotlinx.coroutines.currentCoroutineContext
 import splitties.init.appCtx
@@ -48,76 +45,32 @@ object BookCover {
 
     private const val coverRuleConfigKey = "legadoCoverRuleConfig"
     const val configFileName = "coverRule.json"
-    var drawBookName = true
-        private set
-    var drawBookAuthor = true
-        private set
-    var drawBookShadow = true
-        private set
-    var drawBookStroke = true
-        private set
-    var coverTextColor: Int = Color.WHITE
-        private set
-    var coverShadowColor: Int = Color.WHITE
-        private set
-    var coverTextColorN: Int = Color.WHITE
-        private set
-    var coverShadowColorN: Int = Color.WHITE
-        private set
-    var coverDefaultColor = true
-        private set
-    lateinit var defaultDrawable: Drawable
-        private set
 
-    init {
-        upDefaultCover()
-    }
-
-    @SuppressLint("UseCompatLoadingForDrawables")
-    fun upDefaultCover() {
-        val isNightTheme = AppConfig.isNightTheme
-        drawBookName = if (isNightTheme) {
-            appCtx.getPrefBoolean(PreferKey.coverShowNameN, true)
-        } else {
-            appCtx.getPrefBoolean(PreferKey.coverShowName, true)
-        }
-        drawBookAuthor = if (isNightTheme) {
-            appCtx.getPrefBoolean(PreferKey.coverShowAuthorN, true)
-        } else {
-            appCtx.getPrefBoolean(PreferKey.coverShowAuthor, true)
-        }
-        drawBookShadow =
-            appCtx.getPrefBoolean(PreferKey.coverShowShadow, false)
-        drawBookStroke =
-            appCtx.getPrefBoolean(PreferKey.coverShowStroke, true)
-        coverDefaultColor =
-            appCtx.getPrefBoolean(PreferKey.coverDefaultColor, true)
-        coverTextColor =
-            appCtx.getPrefInt(PreferKey.coverTextColor, Color.WHITE)
-        coverTextColorN =
-            appCtx.getPrefInt(PreferKey.coverTextColorN, Color.BLACK)
-        coverShadowColor =
-            appCtx.getPrefInt(PreferKey.coverShadowColor, Color.WHITE)
-        coverShadowColorN =
-            appCtx.getPrefInt(PreferKey.coverShadowColorN, Color.BLACK)
+    val defaultDrawable: Drawable
+        @SuppressLint("UseCompatLoadingForDrawables")
+        get() {
+            val isNightTheme = AppConfig.isNightTheme
+            val key = if (isNightTheme) PreferKey.defaultCoverDark else PreferKey.defaultCover
+            val paths = appCtx.getPrefString(key)?.split(",")?.filter { it.isNotBlank() }
 
-        val key = if (isNightTheme) PreferKey.defaultCoverDark else PreferKey.defaultCover
-        val paths = appCtx.getPrefString(key)?.split(",")?.filter { it.isNotBlank() }
+            if (paths.isNullOrEmpty()) {
+                return appCtx.resources.getDrawable(R.drawable.image_cover_default, null)
+            }
 
-        if (paths.isNullOrEmpty()) {
-            defaultDrawable = appCtx.resources.getDrawable(R.drawable.image_cover_default, null)
-            return
+            val randomPath = paths[Random.nextInt(paths.size)]
+            return kotlin.runCatching {
+                BitmapUtils.decodeBitmap(randomPath, 600, 900)!!.toDrawable(appCtx.resources)
+            }.getOrDefault(appCtx.resources.getDrawable(R.drawable.image_cover_default, null))
         }
 
-        val randomPath = paths[Random.nextInt(paths.size)]
-        defaultDrawable = kotlin.runCatching {
-            BitmapUtils.decodeBitmap(randomPath, 600, 900)!!.toDrawable(appCtx.resources)
-        }.getOrDefault(appCtx.resources.getDrawable(R.drawable.image_cover_default, null))
-    }
+    // 兼容旧代码,空实现
+    fun upDefaultCover() {}
 
-    fun getRandomDefaultPath(seed: Any? = null): String? {
-        val isNightTheme = AppConfig.isNightTheme
-        val key = if (isNightTheme) PreferKey.defaultCoverDark else PreferKey.defaultCover
+    fun getRandomDefaultPath(
+        seed: Any? = null,
+        isNight: Boolean = AppConfig.isNightTheme
+    ): String? {
+        val key = if (isNight) PreferKey.defaultCoverDark else PreferKey.defaultCover
         val paths = appCtx.getPrefString(key)?.split(",")?.filter { it.isNotBlank() }
         if (paths.isNullOrEmpty()) return null
         val random = if (seed != null) Random(seed.hashCode()) else Random

+ 1 - 1
app/src/main/java/io/legado/app/ui/config/PrefDelegate.kt

@@ -74,7 +74,7 @@ fun <T> prefDelegate(
         override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
             if (_value.value != value) {
                 when (value) {
-                    is String -> appCtx.putPrefString(key, value)
+                    is String? -> appCtx.putPrefString(key, value)
                     is Int -> appCtx.putPrefInt(key, value)
                     is Boolean -> appCtx.putPrefBoolean(key, value)
                     is Long -> appCtx.putPrefLong(key, value)

+ 19 - 3
app/src/main/java/io/legado/app/ui/config/TestConfigActivity.kt

@@ -17,6 +17,7 @@ import androidx.navigation.compose.composable
 import androidx.navigation.compose.rememberNavController
 import io.legado.app.R
 import io.legado.app.base.BaseComposeActivity
+import io.legado.app.ui.config.backupConfig.BackupConfigScreen
 import io.legado.app.ui.config.coverConfig.CoverConfigScreen
 import io.legado.app.ui.config.otherConfig.OtherConfigScreen
 import io.legado.app.ui.config.readConfig.ReadConfigScreen
@@ -46,6 +47,9 @@ class TestConfigActivity : BaseComposeActivity() {
     @Serializable
     object ThemeConfigRoute
 
+    @Serializable
+    object BackupConfigRoute
+
     @Composable
     override fun Content() {
         val navController = rememberNavController()
@@ -60,7 +64,8 @@ class TestConfigActivity : BaseComposeActivity() {
                     onNavigateToOther = { navController.navigate(OtherConfigRoute) },
                     onNavigateToRead = { navController.navigate(ReadConfigRoute) },
                     onNavigateToCover = { navController.navigate(CoverConfigRoute) },
-                    onNavigateToTheme = { navController.navigate(ThemeConfigRoute) }
+                    onNavigateToTheme = { navController.navigate(ThemeConfigRoute) },
+                    onNavigateToBackup = { navController.navigate(BackupConfigRoute) }
                 )
             }
 
@@ -77,7 +82,13 @@ class TestConfigActivity : BaseComposeActivity() {
             }
 
             composable<ThemeConfigRoute> {
-                ThemeConfigScreen(onBackClick = { navController.popBackStack() })
+                ThemeConfigScreen(
+                    onBackClick = { navController.popBackStack() }
+                )
+            }
+
+            composable<BackupConfigRoute> {
+                BackupConfigScreen(onBackClick = { navController.popBackStack() })
             }
         }
     }
@@ -89,7 +100,8 @@ class TestConfigActivity : BaseComposeActivity() {
         onNavigateToOther: () -> Unit,
         onNavigateToRead: () -> Unit,
         onNavigateToCover: () -> Unit,
-        onNavigateToTheme: () -> Unit
+        onNavigateToTheme: () -> Unit,
+        onNavigateToBackup: () -> Unit
     ) {
         val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
 
@@ -129,6 +141,10 @@ class TestConfigActivity : BaseComposeActivity() {
                         title = stringResource(R.string.cover_config),
                         onClick = onNavigateToCover
                     )
+                    ClickableSettingItem(
+                        title = stringResource(R.string.backup_restore),
+                        onClick = onNavigateToBackup
+                    )
                 }
             }
         }

+ 58 - 0
app/src/main/java/io/legado/app/ui/config/backupConfig/BackupConfig.kt

@@ -0,0 +1,58 @@
+package io.legado.app.ui.config.backupConfig
+
+import io.legado.app.constant.PreferKey
+import io.legado.app.ui.config.prefDelegate
+
+object BackupConfig {
+
+    var webDavUrl by prefDelegate(
+        PreferKey.webDavUrl,
+        ""
+    )
+
+    var webDavAccount by prefDelegate(
+        PreferKey.webDavAccount,
+        ""
+    )
+
+    var webDavPassword by prefDelegate(
+        PreferKey.webDavPassword,
+        ""
+    )
+
+    var webDavDir by prefDelegate(
+        PreferKey.webDavDir,
+        "legado"
+    )
+
+    var webDavDeviceName by prefDelegate(
+        PreferKey.webDavDeviceName,
+        ""
+    )
+
+    var syncBookProgress by prefDelegate(
+        PreferKey.syncBookProgress,
+        true
+    )
+
+    var syncBookProgressPlus by prefDelegate(
+        PreferKey.syncBookProgressPlus,
+        false
+    )
+
+    var autoCheckNewBackup by prefDelegate(
+        PreferKey.autoCheckNewBackup,
+        true
+    )
+
+    var onlyLatestBackup by prefDelegate(
+        PreferKey.onlyLatestBackup,
+        true
+    )
+
+    var backupPath by prefDelegate<String?>(
+        PreferKey.backupPath,
+        null
+    )
+
+}

+ 615 - 0
app/src/main/java/io/legado/app/ui/config/backupConfig/BackupConfigScreen.kt

@@ -0,0 +1,615 @@
+package io.legado.app.ui.config.backupConfig
+
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Visibility
+import androidx.compose.material.icons.filled.VisibilityOff
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.OutlinedButton
+import androidx.compose.material3.SnackbarHost
+import androidx.compose.material3.SnackbarHostState
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.material3.TextField
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.rememberUpdatedState
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.text.input.PasswordVisualTransformation
+import androidx.compose.ui.text.input.VisualTransformation
+import androidx.compose.ui.unit.dp
+import io.legado.app.R
+import io.legado.app.help.storage.Restore
+import io.legado.app.lib.permission.Permissions
+import io.legado.app.lib.permission.PermissionsCompat
+import io.legado.app.ui.widget.components.AppScaffold
+import io.legado.app.ui.widget.components.GlassMediumFlexibleTopAppBar
+import io.legado.app.ui.widget.components.GlassTopAppBarDefaults
+import io.legado.app.ui.widget.components.SplicedColumnGroup
+import io.legado.app.ui.widget.components.button.TopbarNavigationButton
+import io.legado.app.ui.widget.components.filePicker.FilePickerSheet
+import io.legado.app.ui.widget.components.settingItem.ClickableSettingItem
+import io.legado.app.ui.widget.components.settingItem.InputSettingItem
+import io.legado.app.ui.widget.components.settingItem.SwitchSettingItem
+import io.legado.app.utils.isContentScheme
+import io.legado.app.utils.takePersistablePermissionSafely
+import kotlinx.coroutines.launch
+import org.koin.androidx.compose.koinViewModel
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun BackupConfigScreen(
+    onBackClick: () -> Unit,
+    viewModel: BackupConfigViewModel = koinViewModel()
+) {
+    val context by rememberUpdatedState(LocalContext.current)
+    val scope = rememberCoroutineScope()
+    val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
+    val snackbarHostState = remember { SnackbarHostState() }
+
+    var showWebDavAuthDialog by remember { mutableStateOf(false) }
+    var showBackupIgnoreDialog by remember { mutableStateOf(false) }
+    var showConfirmDialog by remember { mutableStateOf(false) }
+    var confirmDialogTitle by remember { mutableStateOf("") }
+    var confirmDialogText by remember { mutableStateOf("") }
+    var onConfirmAction by remember { mutableStateOf<(() -> Unit)?>(null) }
+
+    var tempAccount by remember { mutableStateOf("") }
+    var tempPassword by remember { mutableStateOf("") }
+    var passwordVisible by remember { mutableStateOf(false) }
+
+    var showBackupFilePicker by remember { mutableStateOf(false) }
+    var showRestoreFilePicker by remember { mutableStateOf(false) }
+    var showRestoreSheet by remember { mutableStateOf(false) }
+    var backupNames by remember { mutableStateOf<List<String>>(emptyList()) }
+
+    var showLoadingDialog by remember { mutableStateOf(false) }
+    var loadingText by remember { mutableStateOf("") }
+
+    val selectBackupPathLauncher = rememberLauncherForActivityResult(
+        contract = ActivityResultContracts.OpenDocumentTree()
+    ) { uri ->
+        uri?.let {
+            it.takePersistablePermissionSafely(context)
+            if (it.isContentScheme()) {
+                BackupConfig.backupPath = it.toString()
+            } else {
+                BackupConfig.backupPath = it.path
+            }
+        }
+    }
+
+    val backupAndSelectLauncher = rememberLauncherForActivityResult(
+        contract = ActivityResultContracts.OpenDocumentTree()
+    ) { uri ->
+        uri?.let {
+            it.takePersistablePermissionSafely(context)
+            val path = if (it.isContentScheme()) {
+                it.toString()
+            } else {
+                it.path ?: ""
+            }
+            BackupConfig.backupPath = path
+            if (path.isNotEmpty()) {
+                startBackup(path, context, viewModel, {
+                    showLoadingDialog = false
+                    scope.launch {
+                        snackbarHostState.showSnackbar(context.getString(R.string.backup_success))
+                    }
+                }, { error ->
+                    showLoadingDialog = false
+                    scope.launch {
+                        snackbarHostState.showSnackbar(
+                            context.getString(
+                                R.string.backup_fail,
+                                error
+                            )
+                        )
+                    }
+                })
+            }
+        }
+    }
+
+    val restoreFileLauncher = rememberLauncherForActivityResult(
+        contract = ActivityResultContracts.OpenDocument()
+    ) { uri ->
+        uri?.let {
+            showLoadingDialog = true
+            loadingText = "恢复中…"
+            scope.launch {
+                try {
+                    Restore.restore(context, uri)
+                    showLoadingDialog = false
+                    snackbarHostState.showSnackbar("恢复成功")
+                } catch (e: Exception) {
+                    showLoadingDialog = false
+                    snackbarHostState.showSnackbar("恢复出错: ${e.localizedMessage}")
+                }
+            }
+        }
+    }
+
+    val importOldLauncher = rememberLauncherForActivityResult(
+        contract = ActivityResultContracts.OpenDocument()
+    ) { uri ->
+        uri?.let {
+            io.legado.app.help.storage.ImportOldData.importUri(context, uri)
+        }
+    }
+
+    AppScaffold(
+        modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
+        snackbarHost = {
+            SnackbarHost(
+                hostState = snackbarHostState
+            )
+        },
+        topBar = {
+            GlassMediumFlexibleTopAppBar(
+                title = {
+                    Text(stringResource(R.string.backup_restore))
+                },
+                scrollBehavior = scrollBehavior,
+                navigationIcon = {
+                    TopbarNavigationButton(onClick = onBackClick)
+                }
+            )
+        }
+    ) { paddingValues ->
+        Column(
+            modifier = Modifier
+                .fillMaxSize()
+                .padding(paddingValues)
+                .verticalScroll(rememberScrollState())
+                .padding(16.dp)
+        ) {
+            SplicedColumnGroup(title = stringResource(R.string.web_dav_set)) {
+                InputSettingItem(
+                    title = stringResource(R.string.web_dav_url),
+                    value = BackupConfig.webDavUrl,
+                    defaultValue = "",
+                    onConfirm = { BackupConfig.webDavUrl = it }
+                )
+
+                ClickableSettingItem(
+                    title = stringResource(R.string.web_dav_account),
+                    description = "设置 WebDAV 账号和密码",
+                    onClick = {
+                        tempAccount = viewModel.getWebDavAccount()
+                        tempPassword = viewModel.getWebDavPassword()
+                        showWebDavAuthDialog = true
+                    }
+                )
+
+                InputSettingItem(
+                    title = stringResource(R.string.sub_dir),
+                    value = BackupConfig.webDavDir,
+                    defaultValue = "legado",
+                    onConfirm = { BackupConfig.webDavDir = it }
+                )
+
+                InputSettingItem(
+                    title = stringResource(R.string.webdav_device_name),
+                    value = BackupConfig.webDavDeviceName,
+                    defaultValue = "",
+                    onConfirm = { BackupConfig.webDavDeviceName = it }
+                )
+
+                ClickableSettingItem(
+                    title = "测试 WebDav 配置",
+                    description = "测试 WebDav 服务工作状态",
+                    onClick = {
+                        scope.launch {
+                            showLoadingDialog = true
+                            loadingText = "测试中…"
+                            val success = viewModel.testWebDav()
+                            showLoadingDialog = false
+                            if (success) {
+                                snackbarHostState.showSnackbar("WebDav 配置正确")
+                            } else {
+                                snackbarHostState.showSnackbar("WebDav 配置错误")
+                            }
+                        }
+                    }
+                )
+
+                SwitchSettingItem(
+                    title = stringResource(R.string.sync_book_progress_t),
+                    description = stringResource(R.string.sync_book_progress_s),
+                    checked = BackupConfig.syncBookProgress,
+                    onCheckedChange = {
+                        BackupConfig.syncBookProgress = it
+                        if (!it) {
+                            BackupConfig.syncBookProgressPlus = false
+                        }
+                    }
+                )
+
+                if (BackupConfig.syncBookProgress) {
+                    SwitchSettingItem(
+                        title = stringResource(R.string.sync_book_progress_plus_t),
+                        description = stringResource(R.string.sync_book_progress_plus_s),
+                        checked = BackupConfig.syncBookProgressPlus,
+                        onCheckedChange = { BackupConfig.syncBookProgressPlus = it }
+                    )
+                }
+
+                SwitchSettingItem(
+                    title = stringResource(R.string.auto_check_new_backup_t),
+                    description = stringResource(R.string.auto_check_new_backup_s),
+                    checked = BackupConfig.autoCheckNewBackup,
+                    onCheckedChange = { BackupConfig.autoCheckNewBackup = it }
+                )
+            }
+
+            SplicedColumnGroup(title = stringResource(R.string.backup_restore)) {
+                ClickableSettingItem(
+                    title = stringResource(R.string.backup_path),
+                    description = BackupConfig.backupPath
+                        ?: stringResource(R.string.select_backup_path),
+                    onClick = { showBackupFilePicker = true }
+                )
+
+                val backupText = stringResource(R.string.backup)
+
+                ClickableSettingItem(
+                    title = stringResource(R.string.backup),
+                    description = stringResource(R.string.backup_summary),
+                    onClick = {
+                        val backupPath = BackupConfig.backupPath
+                        if (backupPath.isNullOrEmpty()) {
+                            showRestoreFilePicker = true
+                        } else {
+                            confirmDialogTitle = backupText
+                            confirmDialogText = "确定要备份吗?"
+                            onConfirmAction = {
+                                if (backupPath.isContentScheme()) {
+                                    startBackup(backupPath, context, viewModel, {
+                                        showLoadingDialog = false
+                                        scope.launch {
+                                            snackbarHostState.showSnackbar(context.getString(R.string.backup_success))
+                                        }
+                                    }, { error ->
+                                        showLoadingDialog = false
+                                        scope.launch {
+                                            snackbarHostState.showSnackbar(
+                                                context.getString(
+                                                    R.string.backup_fail,
+                                                    error
+                                                )
+                                            )
+                                        }
+                                    })
+                                } else {
+                                    PermissionsCompat.Builder()
+                                        .addPermissions(*Permissions.Group.STORAGE)
+                                        .rationale(R.string.tip_perm_request_storage)
+                                        .onGranted {
+                                            startBackup(backupPath, context, viewModel, {
+                                                showLoadingDialog = false
+                                                scope.launch {
+                                                    snackbarHostState.showSnackbar(
+                                                        context.getString(
+                                                            R.string.backup_success
+                                                        )
+                                                    )
+                                                }
+                                            }, { error ->
+                                                showLoadingDialog = false
+                                                scope.launch {
+                                                    snackbarHostState.showSnackbar(
+                                                        context.getString(
+                                                            R.string.backup_fail,
+                                                            error
+                                                        )
+                                                    )
+                                                }
+                                            })
+                                        }
+                                        .request()
+                                }
+                            }
+                            showConfirmDialog = true
+                        }
+                    }
+                )
+
+                ClickableSettingItem(
+                    title = stringResource(R.string.restore),
+                    description = stringResource(R.string.restore_summary),
+                    onClick = {
+                        scope.launch {
+                            showLoadingDialog = true
+                            loadingText = "加载中"
+                            try {
+                                val names = viewModel.getBackupNames()
+                                backupNames = names
+                                showRestoreSheet = true
+                            } catch (e: Exception) {
+                                confirmDialogTitle = "恢复"
+                                confirmDialogText =
+                                    "WebDavError\n${e.localizedMessage}\n将从本地备份恢复。"
+                                onConfirmAction = {
+                                    restoreFileLauncher.launch(arrayOf("application/zip"))
+                                }
+                                showConfirmDialog = true
+                            } finally {
+                                showLoadingDialog = false
+                            }
+                        }
+                    }
+                )
+
+                ClickableSettingItem(
+                    title = stringResource(R.string.restore_ignore),
+                    description = stringResource(R.string.restore_ignore_summary),
+                    onClick = { showBackupIgnoreDialog = true }
+                )
+
+                ClickableSettingItem(
+                    title = stringResource(R.string.menu_import_old_version),
+                    description = stringResource(R.string.import_old_summary),
+                    onClick = {
+                        importOldLauncher.launch(arrayOf("*/*"))
+                    }
+                )
+
+                SwitchSettingItem(
+                    title = stringResource(R.string.only_latest_backup_t),
+                    description = stringResource(R.string.only_latest_backup_s),
+                    checked = BackupConfig.onlyLatestBackup,
+                    onCheckedChange = { BackupConfig.onlyLatestBackup = it }
+                )
+            }
+        }
+    }
+
+    if (showWebDavAuthDialog) {
+        AlertDialog(
+            onDismissRequest = { showWebDavAuthDialog = false },
+            title = { Text(stringResource(R.string.web_dav_account)) },
+            text = {
+                Column {
+                    TextField(
+                        value = tempAccount,
+                        onValueChange = { tempAccount = it },
+                        label = { Text("账号") }
+                    )
+                    Spacer(modifier = Modifier.padding(8.dp))
+                    TextField(
+                        value = tempPassword,
+                        onValueChange = { tempPassword = it },
+                        label = { Text("密码") },
+                        visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
+                        keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
+                        trailingIcon = {
+                            val image = if (passwordVisible)
+                                Icons.Filled.Visibility
+                            else Icons.Filled.VisibilityOff
+
+                            val description = if (passwordVisible) "隐藏密码" else "显示密码"
+
+                            IconButton(onClick = { passwordVisible = !passwordVisible }) {
+                                Icon(imageVector = image, description)
+                            }
+                        }
+                    )
+                }
+            },
+            confirmButton = {
+                TextButton(onClick = {
+                    viewModel.setWebDavAccount(tempAccount, tempPassword)
+                    showWebDavAuthDialog = false
+                    scope.launch {
+                        showLoadingDialog = true
+                        loadingText = "测试中…"
+                        val success = viewModel.testWebDav()
+                        showLoadingDialog = false
+                        if (success) {
+                            snackbarHostState.showSnackbar("WebDav 配置正确")
+                        } else {
+                            snackbarHostState.showSnackbar("WebDav 配置错误")
+                        }
+                    }
+                }) {
+                    Text(stringResource(R.string.ok))
+                }
+            },
+            dismissButton = {
+                TextButton(onClick = { showWebDavAuthDialog = false }) {
+                    Text(stringResource(R.string.cancel))
+                }
+            }
+        )
+    }
+
+    if (showConfirmDialog) {
+        ConfirmDialog(
+            title = confirmDialogTitle,
+            text = confirmDialogText,
+            onConfirm = {
+                onConfirmAction?.invoke()
+                showConfirmDialog = false
+            },
+            onDismiss = { showConfirmDialog = false }
+        )
+    }
+
+    if (showBackupFilePicker) {
+        FilePickerSheet(
+            onDismissRequest = { showBackupFilePicker = false },
+            onSelectSysDir = {
+                showBackupFilePicker = false
+                try {
+                    selectBackupPathLauncher.launch(null)
+                } catch (e: Exception) {
+                }
+            }
+        )
+    }
+
+    if (showRestoreFilePicker) {
+        FilePickerSheet(
+            onDismissRequest = { showRestoreFilePicker = false },
+            onSelectSysDir = {
+                showRestoreFilePicker = false
+                try {
+                    backupAndSelectLauncher.launch(null)
+                } catch (e: Exception) {
+                }
+            }
+        )
+    }
+
+    if (showRestoreSheet && backupNames.isNotEmpty()) {
+        AlertDialog(
+            onDismissRequest = { showRestoreSheet = false },
+            title = { Text(stringResource(R.string.select_restore_file)) },
+            text = {
+                Column {
+                    backupNames.forEach { name ->
+                        TextButton(onClick = {
+                            showRestoreSheet = false
+                            showLoadingDialog = true
+                            loadingText = "恢复中…"
+                            viewModel.restoreWebDav(
+                                name,
+                                {
+                                    showLoadingDialog = false
+                                    scope.launch {
+                                        snackbarHostState.showSnackbar("恢复成功")
+                                    }
+                                },
+                                { error ->
+                                    showLoadingDialog = false
+                                    scope.launch {
+                                        snackbarHostState.showSnackbar("WebDav恢复出错\n$error")
+                                    }
+                                }
+                            )
+                        }) {
+                            Text(name)
+                        }
+                    }
+                }
+            },
+            confirmButton = {},
+            dismissButton = {
+                TextButton(onClick = { showRestoreSheet = false }) {
+                    Text(stringResource(R.string.cancel))
+                }
+            }
+        )
+    }
+
+    if (showBackupIgnoreDialog) {
+        val checkedItems = remember {
+            BooleanArray(io.legado.app.help.storage.BackupConfig.ignoreKeys.size) { index ->
+                io.legado.app.help.storage.BackupConfig.ignoreConfig[
+                    io.legado.app.help.storage.BackupConfig.ignoreKeys[index]
+                ] ?: false
+            }
+        }
+        AlertDialog(
+            onDismissRequest = {
+                io.legado.app.help.storage.BackupConfig.saveIgnoreConfig()
+                showBackupIgnoreDialog = false
+            },
+            title = { Text(stringResource(R.string.restore_ignore)) },
+            text = {
+                Column {
+                    io.legado.app.help.storage.BackupConfig.ignoreTitle.forEachIndexed { index, title ->
+                        TextButton(onClick = {
+                            checkedItems[index] = !checkedItems[index]
+                            io.legado.app.help.storage.BackupConfig.ignoreConfig[
+                                io.legado.app.help.storage.BackupConfig.ignoreKeys[index]
+                            ] = checkedItems[index]
+                        }) {
+                            val isChecked = checkedItems[index]
+                            Text(if (isChecked) "✓ $title" else title)
+                        }
+                    }
+                }
+            },
+            confirmButton = {
+                TextButton(onClick = {
+                    io.legado.app.help.storage.BackupConfig.saveIgnoreConfig()
+                    showBackupIgnoreDialog = false
+                }) {
+                    Text(stringResource(R.string.ok))
+                }
+            },
+            dismissButton = {
+                TextButton(onClick = {
+                    io.legado.app.help.storage.BackupConfig.saveIgnoreConfig()
+                    showBackupIgnoreDialog = false
+                }) {
+                    Text(stringResource(R.string.cancel))
+                }
+            }
+        )
+    }
+
+    if (showLoadingDialog) {
+        AlertDialog(
+            onDismissRequest = {},
+            title = { Text(loadingText) },
+            confirmButton = {},
+            dismissButton = {}
+        )
+    }
+}
+
+@Composable
+fun ConfirmDialog(
+    title: String,
+    text: String,
+    onConfirm: () -> Unit,
+    onDismiss: () -> Unit
+) {
+    AlertDialog(
+        onDismissRequest = onDismiss,
+        title = { Text(title) },
+        text = { Text(text) },
+        confirmButton = {
+            OutlinedButton(onClick = onConfirm) {
+                Text(stringResource(R.string.ok))
+            }
+        },
+        dismissButton = {
+            TextButton(onClick = onDismiss) {
+                Text(stringResource(R.string.cancel))
+            }
+        }
+    )
+}
+
+private fun startBackup(
+    path: String,
+    context: android.content.Context,
+    viewModel: BackupConfigViewModel,
+    onSuccess: () -> Unit,
+    onError: (String) -> Unit
+) {
+    viewModel.backup(path, onSuccess, onError)
+}

+ 74 - 0
app/src/main/java/io/legado/app/ui/config/backupConfig/BackupConfigViewModel.kt

@@ -0,0 +1,74 @@
+package io.legado.app.ui.config.backupConfig
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import io.legado.app.help.AppWebDav
+import io.legado.app.help.storage.Backup
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import splitties.init.appCtx
+
+class BackupConfigViewModel : ViewModel() {
+
+    fun setWebDavAccount(account: String, password: String) {
+        BackupConfig.webDavAccount = account
+        BackupConfig.webDavPassword = password
+    }
+
+    fun getWebDavAccount(): String {
+        return BackupConfig.webDavAccount
+    }
+
+    fun getWebDavPassword(): String {
+        return BackupConfig.webDavPassword
+    }
+
+    suspend fun testWebDav(): Boolean {
+        return withContext(Dispatchers.IO) {
+            try {
+                AppWebDav.testWebDav()
+                true
+            } catch (e: Exception) {
+                false
+            }
+        }
+    }
+
+    fun backup(backupPath: String, onSuccess: () -> Unit, onError: (String) -> Unit) {
+        viewModelScope.launch(Dispatchers.IO) {
+            try {
+                Backup.backupLocked(appCtx, backupPath)
+                withContext(Dispatchers.Main) {
+                    onSuccess()
+                }
+            } catch (e: Exception) {
+                withContext(Dispatchers.Main) {
+                    onError(e.localizedMessage ?: "备份出错")
+                }
+            }
+        }
+    }
+
+    suspend fun getBackupNames(): List<String> {
+        return withContext(Dispatchers.IO) {
+            AppWebDav.getBackupNames()
+        }
+    }
+
+    fun restoreWebDav(name: String, onSuccess: () -> Unit, onError: (String) -> Unit) {
+        viewModelScope.launch(Dispatchers.IO) {
+            try {
+                AppWebDav.restoreWebDav(name)
+                withContext(Dispatchers.Main) {
+                    onSuccess()
+                }
+            } catch (e: Exception) {
+                withContext(Dispatchers.Main) {
+                    onError(e.localizedMessage ?: "恢复出错")
+                }
+            }
+        }
+    }
+
+}

+ 2 - 3
app/src/main/java/io/legado/app/ui/config/themeConfig/BackgroundImageManageSheet.kt

@@ -39,7 +39,6 @@ import org.koin.androidx.compose.koinViewModel
 @OptIn(ExperimentalMaterial3Api::class)
 @Composable
 fun BackgroundImageManageSheet(
-    preferenceKey: String,
     isDarkTheme: Boolean,
     onDismissRequest: () -> Unit,
     viewModel: ThemeConfigViewModel = koinViewModel()
@@ -53,7 +52,7 @@ fun BackgroundImageManageSheet(
                 scope.launch {
                     viewModel.setBackgroundFromUri(
                         uri = it,
-                        preferenceKey = preferenceKey
+                        isDarkTheme = isDarkTheme
                     )
                 }
             }
@@ -118,7 +117,7 @@ fun BackgroundImageManageSheet(
                         )
                     }
                     IconButton(
-                        onClick = { viewModel.removeBackground(preferenceKey) },
+                        onClick = { viewModel.removeBackground(isDarkTheme) },
                         modifier = Modifier
                             .align(Alignment.TopEnd)
                             .padding(8.dp)

+ 200 - 0
app/src/main/java/io/legado/app/ui/config/themeConfig/LauncherIconPickerSheet.kt

@@ -0,0 +1,200 @@
+package io.legado.app.ui.config.themeConfig
+
+import android.content.ComponentName
+import android.widget.ImageView
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.lazy.grid.items
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.viewinterop.AndroidView
+import io.legado.app.R
+import io.legado.app.ui.main.Launcher0
+import io.legado.app.ui.main.Launcher1
+import io.legado.app.ui.main.Launcher2
+import io.legado.app.ui.main.Launcher3
+import io.legado.app.ui.main.Launcher4
+import io.legado.app.ui.main.Launcher5
+import io.legado.app.ui.main.Launcher6
+import io.legado.app.ui.main.LauncherW
+import io.legado.app.ui.widget.components.modalBottomSheet.GlassModalBottomSheet
+import io.legado.app.utils.getCompatDrawable
+import splitties.init.appCtx
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun LauncherIconPickerSheet(
+    selectedValue: String,
+    onDismissRequest: () -> Unit,
+    onValueChange: (String) -> Unit
+) {
+    val context = LocalContext.current
+    val icons = LauncherIcons.list
+
+    GlassModalBottomSheet(
+        onDismissRequest = onDismissRequest
+    ) {
+        Column(
+            modifier = Modifier
+                .fillMaxWidth()
+                .padding(bottom = 24.dp)
+        ) {
+            Text(
+                text = stringResource(R.string.change_icon),
+                style = MaterialTheme.typography.titleMedium,
+                modifier = Modifier.padding(16.dp)
+            )
+
+            LazyVerticalGrid(
+                columns = GridCells.Fixed(3),
+                contentPadding = PaddingValues(horizontal = 16.dp),
+                horizontalArrangement = Arrangement.spacedBy(12.dp),
+                verticalArrangement = Arrangement.spacedBy(12.dp)
+            ) {
+                items(icons, key = { it.value }) { item ->
+
+                    val isSelected = item.value == selectedValue
+                    val drawable = remember(item.resId) {
+                        context.getCompatDrawable(item.resId)
+                    }
+
+                    Box(
+                        modifier = Modifier
+                            .aspectRatio(1f)
+                            .clip(MaterialTheme.shapes.large)
+                            .background(
+                                if (isSelected)
+                                    MaterialTheme.colorScheme.secondaryContainer
+                                else
+                                    MaterialTheme.colorScheme.surfaceContainer
+                            )
+                            .then(
+                                if (isSelected) {
+                                    Modifier.border(
+                                        width = 2.dp,
+                                        color = MaterialTheme.colorScheme.primary,
+                                        shape = MaterialTheme.shapes.large
+                                    )
+                                } else {
+                                    Modifier
+                                }
+                            )
+                            .clickable {
+                                onValueChange(item.value)
+                                onDismissRequest()
+                            }
+                            .padding(16.dp),
+                        contentAlignment = Alignment.Center
+                    ) {
+                        AndroidView(
+                            factory = { ctx ->
+                                ImageView(ctx).apply {
+                                    scaleType = ImageView.ScaleType.FIT_CENTER
+                                }
+                            },
+                            update = { imageView ->
+                                imageView.setImageDrawable(drawable)
+                            },
+                            modifier = Modifier.size(48.dp)
+                        )
+                    }
+                }
+            }
+
+            Spacer(modifier = Modifier.height(16.dp))
+        }
+    }
+}
+
+data class LauncherIconItem(
+    val value: String,
+    val label: String,
+    val resId: Int,
+    val component: ComponentName
+)
+
+object LauncherIcons {
+
+    val list = listOf(
+        LauncherIconItem(
+            value = "ic_launcher",
+            label = "iconMain",
+            resId = R.mipmap.ic_launcher,
+            component = ComponentName(appCtx, LauncherW::class.java)
+        ),
+        LauncherIconItem(
+            value = "launcherw",
+            label = "iconWhite",
+            resId = R.mipmap.launcherw,
+            component = ComponentName(appCtx, LauncherW::class.java)
+        ),
+        LauncherIconItem(
+            value = "launcher0",
+            label = "icon0",
+            resId = R.mipmap.launcher0,
+            component = ComponentName(appCtx, Launcher0::class.java)
+        ),
+        LauncherIconItem(
+            value = "launcher1",
+            label = "icon1",
+            resId = R.mipmap.launcher1,
+            component = ComponentName(appCtx, Launcher1::class.java)
+        ),
+        LauncherIconItem(
+            value = "launcher2",
+            label = "icon2",
+            resId = R.mipmap.launcher2,
+            component = ComponentName(appCtx, Launcher2::class.java)
+        ),
+        LauncherIconItem(
+            value = "launcher3",
+            label = "icon3",
+            resId = R.mipmap.launcher3,
+            component = ComponentName(appCtx, Launcher3::class.java)
+        ),
+        LauncherIconItem(
+            value = "launcher4",
+            label = "icon4",
+            resId = R.mipmap.launcher4,
+            component = ComponentName(appCtx, Launcher4::class.java)
+        ),
+        LauncherIconItem(
+            value = "launcher5",
+            label = "icon5",
+            resId = R.mipmap.launcher5,
+            component = ComponentName(appCtx, Launcher5::class.java)
+        ),
+        LauncherIconItem(
+            value = "launcher6",
+            label = "icon6",
+            resId = R.mipmap.launcher6,
+            component = ComponentName(appCtx, Launcher6::class.java)
+        ),
+    )
+
+    fun find(value: String?): LauncherIconItem? {
+        return list.find { it.value == value }
+    }
+}

+ 31 - 21
app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfig.kt

@@ -1,6 +1,5 @@
 package io.legado.app.ui.config.themeConfig
 
-import android.util.Log
 import io.legado.app.constant.EventBus
 import io.legado.app.constant.PreferKey
 import io.legado.app.ui.config.prefDelegate
@@ -20,6 +19,8 @@ object ThemeConfig {
 
     var appTheme by prefDelegate(PreferKey.appTheme, "0")
 
+    var themeMode by prefDelegate(PreferKey.themeMode, "0")
+
     var isPureBlack by prefDelegate(PreferKey.pureBlack, false)
 
     var bgImageLight by prefDelegate<String?>(PreferKey.bgImage, null) {
@@ -34,28 +35,37 @@ object ThemeConfig {
 
     var bgImageNBlurring by prefDelegate(PreferKey.bgImageNBlurring, 0)
 
-    fun hasImageBg(isDark: Boolean): Boolean {
-        val result = if (isDark) {
-            !bgImageDark.isNullOrBlank()
-        } else {
-            !bgImageLight.isNullOrBlank()
-        }
-
-        Log.d(
-            "MainConfig",
-            "hasImageBg -> isDark=$isDark, " +
-                    "bgImageDark=$bgImageDark, " +
-                    "bgImageLight=$bgImageLight, " +
-                    "result=$result"
-        )
-
-        return result
+    var isPredictiveBackEnabled by prefDelegate(PreferKey.isPredictiveBackEnabled, true)
+
+    var customMode by prefDelegate<String?>(PreferKey.customMode, "tonalSpot")
+
+    var fontScale by prefDelegate(PreferKey.fontScale, 10) {
+        postEvent(EventBus.RECREATE, "")
     }
 
-    /*
-    fun hasImageBg(isDark: Boolean): Boolean {
-        return if (isDark) bgImageDark.isNullOrBlank() else bgImageLight.isNullOrBlank()
+    var cPrimary by prefDelegate(PreferKey.cPrimary, 0) {
+        postEvent(EventBus.RECREATE, "")
     }
-     */
+
+    var launcherIcon by prefDelegate(PreferKey.launcherIcon, "ic_launcher")
+
+    var showDiscovery by prefDelegate(PreferKey.showDiscovery, true)
+
+    var showRss by prefDelegate(PreferKey.showRss, true)
+
+    var showStatusBar by prefDelegate(PreferKey.showStatusBar, true)
+
+    var swipeAnimation by prefDelegate(PreferKey.swipeAnimation, true)
+
+    var showBottomView by prefDelegate(PreferKey.showBottomView, true)
+
+    var tabletInterface by prefDelegate(PreferKey.tabletInterface, "auto")
+
+    var labelVisibilityMode by prefDelegate(PreferKey.labelVisibilityMode, "auto")
+
+    var defaultHomePage by prefDelegate(PreferKey.defaultHomePage, "bookshelf")
+
+    fun hasImageBg(isDark: Boolean): Boolean =
+        !(if (isDark) bgImageDark else bgImageLight).isNullOrBlank()
 
 }

+ 648 - 18
app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigScreen.kt

@@ -1,42 +1,116 @@
 package io.legado.app.ui.config.themeConfig
 
+import android.annotation.SuppressLint
+import android.content.Context
+import android.os.Handler
+import android.os.Looper
+import androidx.compose.animation.core.animateDpAsState
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
 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.lazy.LazyRow
+import androidx.compose.foundation.lazy.items
 import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.BrightnessMedium
+import androidx.compose.material.icons.filled.Check
+import androidx.compose.material.icons.filled.DarkMode
+import androidx.compose.material.icons.filled.LightMode
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.ButtonGroupDefaults
+import androidx.compose.material3.Card
+import androidx.compose.material3.CardDefaults
 import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedButton
 import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.material3.ToggleButton
+import androidx.compose.material3.ToggleButtonDefaults
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableIntStateOf
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringArrayResource
 import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.role
+import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.unit.dp
+import androidx.constraintlayout.compose.ConstraintLayout
+import com.google.android.material.color.DynamicColors
+import com.google.android.material.color.DynamicColorsOptions
 import io.legado.app.R
-import io.legado.app.constant.PreferKey
+import io.legado.app.base.AppContextWrapper
+import io.legado.app.constant.EventBus
+import io.legado.app.help.LauncherIconHelp
+import io.legado.app.help.config.AppConfig
+import io.legado.app.help.config.OldThemeConfig
+import io.legado.app.lib.theme.ThemeStore
+import io.legado.app.lib.theme.primaryColor
+import io.legado.app.ui.theme.ThemeManager
+import io.legado.app.ui.theme.ThemeResolver
 import io.legado.app.ui.widget.components.AppScaffold
 import io.legado.app.ui.widget.components.GlassMediumFlexibleTopAppBar
 import io.legado.app.ui.widget.components.GlassTopAppBarDefaults
 import io.legado.app.ui.widget.components.SplicedColumnGroup
 import io.legado.app.ui.widget.components.button.TopbarNavigationButton
+import io.legado.app.ui.widget.components.dialog.ColorPickerSheet
 import io.legado.app.ui.widget.components.settingItem.ClickableSettingItem
 import io.legado.app.ui.widget.components.settingItem.DropdownListSettingItem
 import io.legado.app.ui.widget.components.settingItem.SliderSettingItem
 import io.legado.app.ui.widget.components.settingItem.SwitchSettingItem
+import io.legado.app.utils.postEvent
+import io.legado.app.utils.restart
+import io.legado.app.utils.toastOnUi
 import org.koin.androidx.compose.koinViewModel
 
-@OptIn(ExperimentalMaterial3Api::class)
+@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
 @Composable
 fun ThemeConfigScreen(
     onBackClick: () -> Unit,
     viewModel: ThemeConfigViewModel = koinViewModel()
 ) {
     val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
-    var manageKey by remember { mutableStateOf<Pair<String, Boolean>?>(null) }
+    var manageKey by remember { mutableStateOf<Boolean?>(null) }
+    val context = LocalContext.current
+
+    var selectedThemeMode by remember { mutableStateOf(ThemeConfig.themeMode) }
+    var selectedTheme by remember { mutableStateOf(ThemeConfig.appTheme) }
+    var showRestartDialog by remember { mutableStateOf(false) }
+    var showColorPicker by remember { mutableStateOf(false) }
+    var showLauncherIconPicker by remember { mutableStateOf(false) }
+
+    val fontScaleValue = remember { mutableFloatStateOf(ThemeConfig.fontScale.toFloat()) }
+    val primaryColorValue = remember { mutableIntStateOf(ThemeConfig.cPrimary) }
 
     AppScaffold(
         modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
@@ -57,24 +131,197 @@ fun ThemeConfigScreen(
                 .verticalScroll(rememberScrollState())
                 .padding(16.dp)
         ) {
+            val isDarkTheme = when (selectedThemeMode) {
+                "1" -> false
+                "2" -> true
+                else -> isSystemInDarkTheme()
+            }
+
+            Column(
+                horizontalAlignment = Alignment.CenterHorizontally,
+                modifier = Modifier.fillMaxWidth()
+            ) {
+                ThemeCard(
+                    context = context,
+                    value = selectedTheme,
+                    isDark = isDarkTheme,
+                    isAmoled = ThemeConfig.isPureBlack,
+                    paletteStyle = ThemeConfig.paletteStyle
+                )
+            }
+
+            Spacer(modifier = Modifier.height(16.dp))
+
+            val themeItems = stringArrayResource(R.array.themes_item)
+            val themeValues = stringArrayResource(R.array.themes_value)
+            val themes = remember(themeItems, themeValues) {
+                themeItems.zip(themeValues).toList()
+            }
+
+            SplicedColumnGroup(title = stringResource(R.string.theme)) {
+                ThemeModeSelector(
+                    selectedMode = selectedThemeMode,
+                    onModeSelected = { mode ->
+                        selectedThemeMode = mode
+                        ThemeConfig.themeMode = mode
+                        OldThemeConfig.applyDayNight(context)
+                    }
+                )
+
+                Spacer(modifier = Modifier.height(16.dp))
+
+                ThemeColorSelector(
+                    context = context,
+                    themes = themes,
+                    selectedTheme = selectedTheme,
+                    isDark = isDarkTheme,
+                    isAmoled = ThemeConfig.isPureBlack,
+                    paletteStyle = ThemeConfig.paletteStyle,
+                    onThemeSelected = { theme ->
+                        if (theme == "13") {
+                            val hasLightBg = !ThemeConfig.bgImageLight.isNullOrEmpty()
+                            val hasDarkBg = !ThemeConfig.bgImageDark.isNullOrEmpty()
+                            if (!hasLightBg || !hasDarkBg) {
+                                context.toastOnUi(R.string.transparent_theme_alarm)
+                                return@ThemeColorSelector
+                            } else {
+                                AppConfig.containerOpacity = 0
+                            }
+                        }
+                        val oldTheme = selectedTheme
+                        selectedTheme = theme
+                        ThemeConfig.appTheme = theme
+                        val isDynamicSwitch = (oldTheme == "12" || theme == "12")
+                        if (isDynamicSwitch) {
+                            showRestartDialog = true
+                        } else {
+                            postEvent(EventBus.RECREATE, "")
+                        }
+                    }
+                )
+            }
+
             SplicedColumnGroup {
                 SwitchSettingItem(
                     title = stringResource(R.string.pure_black),
                     checked = ThemeConfig.isPureBlack,
                     onCheckedChange = { ThemeConfig.isPureBlack = it }
                 )
-                DropdownListSettingItem(
-                    title = stringResource(R.string.palette_style),
-                    selectedValue = ThemeConfig.paletteStyle,
-                    displayEntries = arrayOf(
-                        "柔和", "中性", "鲜艳", "表现力", "彩虹",
-                        "水果拼盘", "单色", "仿真", "内容取色"
-                    ),
-                    entryValues = arrayOf(
-                        "tonalSpot", "neutral", "vibrant", "expressive", "rainbow",
-                        "fruitSalad", "monochrome", "fidelity", "content"
+                ClickableSettingItem(
+                    title = stringResource(R.string.change_icon),
+                    description = stringResource(R.string.change_icon_summary),
+                    onClick = { showLauncherIconPicker = true }
+                )
+                SwitchSettingItem(
+                    title = "预见性返回手势",
+                    description = "启用系统预见性返回手势",
+                    checked = ThemeConfig.isPredictiveBackEnabled,
+                    onCheckedChange = {
+                        ThemeConfig.isPredictiveBackEnabled = it
+                        context.toastOnUi("重启以应用")
+                    }
+                )
+                SliderSettingItem(
+                    title = stringResource(R.string.font_scale),
+                    description = stringResource(
+                        R.string.font_scale_summary,
+                        AppContextWrapper.getFontScale(context)
                     ),
-                    onValueChange = { ThemeConfig.paletteStyle = it }
+                    value = fontScaleValue.value,
+                    defaultValue = 10f,
+                    valueRange = 8f..16f,
+                    steps = 7,
+                    onValueChange = { value ->
+                        fontScaleValue.floatValue = value
+                        ThemeConfig.fontScale = value.toInt()
+                    }
+                )
+            }
+
+            if (selectedTheme == "12") {
+                SplicedColumnGroup(title = "自定义主题") {
+                    ClickableSettingItem(
+                        title = stringResource(R.string.seed_color),
+                        option = if (primaryColorValue.intValue != 0) "#${
+                            Integer.toHexString(
+                                primaryColorValue.intValue
+                            ).uppercase()
+                        }" else "点击选择",
+                        onClick = { showColorPicker = true },
+                        trailingContent = {
+                            if (primaryColorValue.intValue != 0) {
+                                Box(
+                                    modifier = Modifier
+                                        .size(28.dp)
+                                        .clip(CircleShape)
+                                        .background(Color(primaryColorValue.intValue))
+                                        .border(
+                                            1.dp,
+                                            MaterialTheme.colorScheme.outlineVariant,
+                                            CircleShape
+                                        )
+                                )
+                            }
+                        }
+                    )
+                    DropdownListSettingItem(
+                        title = stringResource(R.string.palette_style),
+                        selectedValue = ThemeConfig.paletteStyle,
+                        displayEntries = stringArrayResource(R.array.paletteStyle),
+                        entryValues = stringArrayResource(R.array.paletteStyle_value),
+                        onValueChange = { ThemeConfig.paletteStyle = it }
+                    )
+                }
+            }
+
+            SplicedColumnGroup(title = stringResource(R.string.main_activity)) {
+                SwitchSettingItem(
+                    title = stringResource(R.string.show_discovery),
+                    checked = ThemeConfig.showDiscovery,
+                    onCheckedChange = { ThemeConfig.showDiscovery = it }
+                )
+                SwitchSettingItem(
+                    title = stringResource(R.string.show_rss),
+                    checked = ThemeConfig.showRss,
+                    onCheckedChange = { ThemeConfig.showRss = it }
+                )
+                SwitchSettingItem(
+                    title = stringResource(R.string.show_status),
+                    checked = ThemeConfig.showStatusBar,
+                    onCheckedChange = { ThemeConfig.showStatusBar = it }
+                )
+                //TODO:这个可以不要了,在删掉原来的设置页以后删
+                SwitchSettingItem(
+                    title = stringResource(R.string.show_swipe_animation),
+                    checked = ThemeConfig.swipeAnimation,
+                    onCheckedChange = { ThemeConfig.swipeAnimation = it }
+                )
+                SwitchSettingItem(
+                    title = stringResource(R.string.show_bottom_nav),
+                    description = stringResource(R.string.be_swiped),
+                    checked = ThemeConfig.showBottomView,
+                    onCheckedChange = { ThemeConfig.showBottomView = it }
+                )
+                DropdownListSettingItem(
+                    title = stringResource(R.string.tabletInterface),
+                    selectedValue = ThemeConfig.tabletInterface,
+                    displayEntries = stringArrayResource(R.array.tabletInterface),
+                    entryValues = stringArrayResource(R.array.tabletInterface_value),
+                    onValueChange = { ThemeConfig.tabletInterface = it }
+                )
+                DropdownListSettingItem(
+                    title = stringResource(R.string.nav_label_mode),
+                    selectedValue = ThemeConfig.labelVisibilityMode,
+                    displayEntries = stringArrayResource(R.array.label_vis_mode),
+                    entryValues = stringArrayResource(R.array.label_vis_mode_value),
+                    onValueChange = { ThemeConfig.labelVisibilityMode = it }
+                )
+                DropdownListSettingItem(
+                    title = stringResource(R.string.default_home_page),
+                    selectedValue = ThemeConfig.defaultHomePage,
+                    displayEntries = stringArrayResource(R.array.default_home_page),
+                    entryValues = stringArrayResource(R.array.default_home_page_value),
+                    onValueChange = { ThemeConfig.defaultHomePage = it }
                 )
             }
 
@@ -117,7 +364,7 @@ fun ThemeConfigScreen(
                     description = if (hasLightBg) stringResource(R.string.click_to_delete) else stringResource(
                         R.string.select_image
                     ),
-                    onClick = { manageKey = Pair(PreferKey.bgImage, false) }
+                    onClick = { manageKey = false }
                 )
 
                 if (hasLightBg) {
@@ -141,7 +388,7 @@ fun ThemeConfigScreen(
                     description = if (hasDarkBg) stringResource(R.string.click_to_delete) else stringResource(
                         R.string.select_image
                     ),
-                    onClick = { manageKey = Pair(PreferKey.bgImageN, true) }
+                    onClick = { manageKey = true }
                 )
 
                 if (hasDarkBg) {
@@ -160,11 +407,394 @@ fun ThemeConfigScreen(
         }
     }
 
-    manageKey?.let { (key, isDark) ->
+    if (showRestartDialog) {
+        AlertDialog(
+            onDismissRequest = { showRestartDialog = false },
+            title = { Text(stringResource(R.string.restart_required_message)) },
+            confirmButton = {
+                OutlinedButton(onClick = {
+                    showRestartDialog = false
+                    Handler(Looper.getMainLooper()).postDelayed({
+                        context.restart()
+                    }, 100)
+                }) {
+                    Text(stringResource(R.string.ok))
+                }
+            },
+            dismissButton = {
+                TextButton(onClick = {
+                    showRestartDialog = false
+                    context.toastOnUi(R.string.restart_later_message)
+                }) {
+                    Text(stringResource(R.string.cancel))
+                }
+            }
+        )
+    }
+
+    manageKey?.let { isDark ->
         BackgroundImageManageSheet(
-            preferenceKey = key,
             isDarkTheme = isDark,
             onDismissRequest = { manageKey = null }
         )
     }
+
+    if (showColorPicker) {
+        ColorPickerSheet(
+            initialColor = primaryColorValue.value,
+            onDismissRequest = { showColorPicker = false },
+            onColorSelected = { color ->
+                primaryColorValue.value = color
+                ThemeConfig.cPrimary = color
+                ThemeStore.editTheme(context)
+                    .primaryColor(color)
+                    .apply()
+                DynamicColors.applyToActivitiesIfAvailable(
+                    context.applicationContext as android.app.Application,
+                    DynamicColorsOptions.Builder()
+                        .setContentBasedSource(context.primaryColor)
+                        .build()
+                )
+            }
+        )
+    }
+
+    if (showLauncherIconPicker) {
+        LauncherIconPickerSheet(
+            selectedValue = ThemeConfig.launcherIcon,
+            onDismissRequest = { showLauncherIconPicker = false },
+            onValueChange = {
+                ThemeConfig.launcherIcon = it
+                LauncherIconHelp.changeIcon(it)
+            }
+        )
+    }
+}
+
+@OptIn(ExperimentalMaterial3ExpressiveApi::class)
+@Composable
+fun ThemeModeSelector(
+    selectedMode: String,
+    onModeSelected: (String) -> Unit
+) {
+    val modes = listOf(
+        Triple("0", stringResource(R.string.flow_sys), Icons.Filled.BrightnessMedium),
+        Triple("1", stringResource(R.string.light_mode), Icons.Filled.LightMode),
+        Triple("2", stringResource(R.string.dark_mode), Icons.Filled.DarkMode)
+    )
+
+    val selectedIndex = modes.indexOfFirst { it.first == selectedMode }
+        .coerceAtLeast(0)
+
+    Row(
+        modifier = Modifier
+            .fillMaxWidth(),
+        horizontalArrangement = Arrangement.spacedBy(
+            ButtonGroupDefaults.ConnectedSpaceBetween
+        )
+    ) {
+        val modifiers = listOf(Modifier.weight(1.2f), Modifier.weight(1f), Modifier.weight(1f))
+
+        modes.forEachIndexed { index, (value, label, icon) ->
+
+            ToggleButton(
+                checked = selectedIndex == index,
+                onCheckedChange = { onModeSelected(value) },
+                modifier = modifiers[index]
+                    .semantics { role = Role.RadioButton },
+
+                shapes = when (index) {
+                    0 -> ButtonGroupDefaults.connectedLeadingButtonShapes()
+                    modes.lastIndex -> ButtonGroupDefaults.connectedTrailingButtonShapes()
+                    else -> ButtonGroupDefaults.connectedMiddleButtonShapes()
+                }
+            ) {
+                Icon(
+                    imageVector = icon,
+                    contentDescription = null
+                )
+
+                Spacer(Modifier.size(ToggleButtonDefaults.IconSpacing))
+
+                Text(text = label)
+            }
+        }
+    }
+}
+
+@Composable
+fun ThemeColorSelector(
+    context: Context,
+    themes: List<Pair<String, String>>,
+    selectedTheme: String,
+    isDark: Boolean,
+    isAmoled: Boolean,
+    paletteStyle: String?,
+    onThemeSelected: (String) -> Unit
+) {
+    LazyRow(
+        horizontalArrangement = Arrangement.spacedBy(16.dp),
+        modifier = Modifier.fillMaxWidth()
+    ) {
+        items(themes) { (label, value) ->
+            ThemeColorButton(
+                context = context,
+                label = label,
+                value = value,
+                isSelected = selectedTheme == value,
+                isDark = isDark,
+                isAmoled = isAmoled,
+                paletteStyle = paletteStyle,
+                onClick = { onThemeSelected(value) }
+            )
+        }
+    }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun ThemeColorButton(
+    context: Context,
+    label: String,
+    value: String,
+    isSelected: Boolean,
+    isDark: Boolean,
+    isAmoled: Boolean,
+    paletteStyle: String?,
+    onClick: () -> Unit
+) {
+    val colors = getThemeColorPalette(context, value, isDark, isAmoled, paletteStyle)
+    val borderWidth by animateDpAsState(
+        targetValue = if (isSelected) 2.dp else 0.dp,
+        label = "borderWidth"
+    )
+
+    Column(
+        horizontalAlignment = Alignment.CenterHorizontally,
+    ) {
+        Card(
+            onClick = onClick,
+            modifier = Modifier.size(64.dp),
+            shape = RoundedCornerShape(16.dp),
+            border = if (isSelected) BorderStroke(
+                borderWidth,
+                MaterialTheme.colorScheme.primary
+            ) else null,
+            colors = CardDefaults.cardColors(containerColor = colors.surfaceContainer)
+        ) {
+            Box(
+                modifier = Modifier.fillMaxSize(),
+                contentAlignment = Alignment.Center
+            ) {
+                Box(
+                    modifier = Modifier.size(48.dp)
+                ) {
+                    Canvas(
+                        modifier = Modifier.fillMaxSize()
+                    ) {
+                        drawArc(
+                            color = colors.secondary,
+                            startAngle = -90f,
+                            sweepAngle = 180f,
+                            useCenter = true,
+                            size = size
+                        )
+
+                        drawArc(
+                            color = colors.tertiary,
+                            startAngle = 90f,
+                            sweepAngle = 180f,
+                            useCenter = true,
+                            size = size
+                        )
+                    }
+
+                    Box(
+                        modifier = Modifier
+                            .size(24.dp)
+                            .clip(CircleShape)
+                            .background(colors.primary)
+                            .align(Alignment.Center)
+                    )
+                }
+
+                if (isSelected) {
+                    Box(
+                        modifier = Modifier
+                            .size(40.dp)
+                            .clip(RoundedCornerShape(12.dp)),
+                        contentAlignment = Alignment.Center
+                    ) {
+                        Icon(
+                            imageVector = Icons.Default.Check,
+                            contentDescription = null,
+                            tint = MaterialTheme.colorScheme.onPrimary,
+                            modifier = Modifier.size(24.dp)
+                        )
+                    }
+                }
+            }
+        }
+
+        Spacer(modifier = Modifier.height(12.dp))
+
+        Text(
+            text = label,
+            style = MaterialTheme.typography.labelSmall,
+            color = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface
+        )
+    }
+}
+
+@Composable
+fun ThemeCard(
+    context: Context,
+    value: String,
+    isDark: Boolean,
+    isAmoled: Boolean,
+    paletteStyle: String?
+) {
+    val colors = getThemeColors(context, value, isDark, isAmoled, paletteStyle)
+
+    Column(
+        horizontalAlignment = Alignment.CenterHorizontally
+    ) {
+        Card(
+            modifier = Modifier
+                .width(128.dp)
+                .height(256.dp),
+            shape = MaterialTheme.shapes.large,
+            border = BorderStroke(1.dp, colors.primary),
+            colors = CardDefaults.cardColors(containerColor = colors.surface)
+        ) {
+            ConstraintLayout(
+                modifier = Modifier.fillMaxSize()
+            ) {
+                val (colorTop, colorBook, colorBottom) = createRefs()
+
+                Box(
+                    modifier = Modifier
+                        .size(width = 48.dp, height = 16.dp)
+                        .constrainAs(colorTop) {
+                            top.linkTo(parent.top, margin = 12.dp)
+                            start.linkTo(parent.start, margin = 12.dp)
+                        }
+                        .clip(RoundedCornerShape(8.dp))
+                        .background(colors.onSurfaceVariant)
+                )
+
+                Box(
+                    modifier = Modifier
+                        .size(width = 56.dp, height = 80.dp)
+                        .constrainAs(colorBook) {
+                            top.linkTo(colorTop.bottom, margin = 8.dp)
+                            start.linkTo(colorTop.start)
+                        }
+                        .clip(RoundedCornerShape(8.dp))
+                        .background(colors.secondaryContainer)
+                ) {
+                    Box(
+                        modifier = Modifier
+                            .align(Alignment.TopEnd)
+                            .padding(4.dp)
+                            .size(width = 16.dp, height = 12.dp)
+                            .clip(RoundedCornerShape(4.dp))
+                            .background(colors.secondary)
+                    )
+                }
+
+                Box(
+                    modifier = Modifier
+                        .fillMaxWidth()
+                        .height(40.dp)
+                        .constrainAs(colorBottom) {
+                            bottom.linkTo(parent.bottom)
+                            start.linkTo(parent.start, margin = 4.dp)
+                            end.linkTo(parent.end, margin = 4.dp)
+                        }
+                        .background(colors.surfaceContainer)
+                ) {
+                    Box(
+                        modifier = Modifier
+                            .align(Alignment.Center)
+                            .size(20.dp)
+                            .clip(CircleShape)
+                            .background(colors.primary)
+                    )
+                }
+            }
+        }
+    }
+}
+
+data class ThemeColorPalette(
+    val primary: Color,
+    val secondary: Color,
+    val tertiary: Color,
+    val surfaceContainer: Color
+)
+
+data class ThemeColors(
+    val primary: Color,
+    val secondary: Color,
+    val tertiary: Color,
+    val surface: Color,
+    val surfaceContainer: Color,
+    val secondaryContainer: Color,
+    val onSurface: Color,
+    val onSurfaceVariant: Color
+)
+
+@SuppressLint("ResourceType")
+private fun getThemeColorPalette(
+    context: Context,
+    value: String,
+    isDark: Boolean,
+    isAmoled: Boolean,
+    paletteStyle: String?
+): ThemeColorPalette {
+    val appThemeMode = ThemeResolver.resolveThemeMode(value)
+    val colorScheme = ThemeManager.getColorScheme(
+        context = context,
+        mode = appThemeMode,
+        darkTheme = isDark,
+        isAmoled = isAmoled,
+        paletteStyle = paletteStyle
+    )
+
+    return ThemeColorPalette(
+        primary = colorScheme.primary,
+        secondary = colorScheme.secondaryContainer,
+        tertiary = colorScheme.tertiaryContainer,
+        surfaceContainer = colorScheme.surfaceContainer
+    )
+}
+
+@SuppressLint("ResourceType")
+private fun getThemeColors(
+    context: Context,
+    value: String,
+    isDark: Boolean,
+    isAmoled: Boolean,
+    paletteStyle: String?
+): ThemeColors {
+    val appThemeMode = ThemeResolver.resolveThemeMode(value)
+    val colorScheme = ThemeManager.getColorScheme(
+        context = context,
+        mode = appThemeMode,
+        darkTheme = isDark,
+        isAmoled = isAmoled,
+        paletteStyle = paletteStyle
+    )
+
+    return ThemeColors(
+        primary = colorScheme.primary,
+        secondary = colorScheme.secondary,
+        tertiary = colorScheme.tertiary,
+        surface = colorScheme.surface,
+        surfaceContainer = colorScheme.surfaceContainer,
+        secondaryContainer = colorScheme.secondaryContainer,
+        onSurface = colorScheme.onSurface,
+        onSurfaceVariant = colorScheme.onSurfaceVariant
+    )
 }

+ 14 - 12
app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigViewModel.kt

@@ -21,7 +21,7 @@ class ThemeConfigViewModel : ViewModel() {
      */
     suspend fun setBackgroundFromUri(
         uri: Uri,
-        preferenceKey: String
+        isDarkTheme: Boolean
     ) = withContext(Dispatchers.IO) {
         kotlin.runCatching {
             val fileDoc = FileDoc.fromUri(uri, false)
@@ -33,6 +33,7 @@ class ThemeConfigViewModel : ViewModel() {
             }
             val fileName = "$md5.$suffix"
 
+            val preferenceKey = if (isDarkTheme) PreferKey.bgImageN else PreferKey.bgImage
             val folder = File(appCtx.externalFiles, preferenceKey)
             val file = File(folder, fileName)
 
@@ -45,24 +46,24 @@ class ThemeConfigViewModel : ViewModel() {
                 }
             }
 
-            updateBackgroundPath(preferenceKey, file.absolutePath)
+            updateBackgroundPath(isDarkTheme, file.absolutePath)
         }.onFailure {
             it.printStackTrace()
         }
     }
 
-    fun removeBackground(preferenceKey: String) {
-        updateBackgroundPath(preferenceKey, null)
+    fun removeBackground(isDarkTheme: Boolean) {
+        updateBackgroundPath(isDarkTheme, null)
     }
 
     /**
      * 更新背景路径并清理旧文件
      */
-    private fun updateBackgroundPath(preferenceKey: String, newPath: String?) {
-        val oldPath = when (preferenceKey) {
-            PreferKey.bgImage -> ThemeConfig.bgImageLight
-            PreferKey.bgImageN -> ThemeConfig.bgImageDark
-            else -> null
+    private fun updateBackgroundPath(isDarkTheme: Boolean, newPath: String?) {
+        val oldPath = if (isDarkTheme) {
+            ThemeConfig.bgImageDark
+        } else {
+            ThemeConfig.bgImageLight
         }
 
         if (oldPath != newPath && oldPath != null) {
@@ -72,9 +73,10 @@ class ThemeConfigViewModel : ViewModel() {
             }
         }
 
-        when (preferenceKey) {
-            PreferKey.bgImage -> ThemeConfig.bgImageLight = newPath
-            PreferKey.bgImageN -> ThemeConfig.bgImageDark = newPath
+        if (isDarkTheme) {
+            ThemeConfig.bgImageDark = newPath
+        } else {
+            ThemeConfig.bgImageLight = newPath
         }
     }
 }

+ 72 - 61
app/src/main/java/io/legado/app/ui/main/bookshelf/BookItem.kt

@@ -198,76 +198,87 @@ fun BookshelfItem(
 @Composable
 fun BookGroupCover(
     books: List<Book>,
+    coverPath: String? = null,
     modifier: Modifier = Modifier
 ) {
     Box(
         modifier = modifier
             .aspectRatio(5f / 7f)
             .clip(RoundedCornerShape(4.dp))
-            .background(MaterialTheme.colorScheme.surfaceVariant)
+            .background(MaterialTheme.colorScheme.surfaceContainer)
     ) {
-        Column(modifier = Modifier.fillMaxSize()) {
-            Row(modifier = Modifier.weight(1f)) {
-                Box(
-                    modifier = Modifier
-                        .weight(1f)
-                        .fillMaxHeight()
-                        .padding(1.dp)
-                ) {
-                    books.getOrNull(0)?.let {
-                        BookCover(
-                            name = it.name,
-                            author = it.author,
-                            path = it.getDisplayCover(),
-                            modifier = Modifier.fillMaxSize()
-                        )
+        if (coverPath != null) {
+            BookCover(
+                name = "",
+                author = "",
+                path = coverPath,
+                modifier = Modifier.fillMaxSize(),
+                ignoreUseDefaultCover = true
+            )
+        } else {
+            Column(modifier = Modifier.fillMaxSize()) {
+                Row(modifier = Modifier.weight(1f)) {
+                    Box(
+                        modifier = Modifier
+                            .weight(1f)
+                            .fillMaxHeight()
+                            .padding(1.dp)
+                    ) {
+                        books.getOrNull(0)?.let {
+                            BookCover(
+                                name = it.name,
+                                author = it.author,
+                                path = it.getDisplayCover(),
+                                modifier = Modifier.fillMaxSize()
+                            )
+                        }
                     }
-                }
-                Box(
-                    modifier = Modifier
-                        .weight(1f)
-                        .fillMaxHeight()
-                        .padding(1.dp)
-                ) {
-                    books.getOrNull(1)?.let {
-                        BookCover(
-                            name = it.name,
-                            author = it.author,
-                            path = it.getDisplayCover(),
-                            modifier = Modifier.fillMaxSize()
-                        )
+                    Box(
+                        modifier = Modifier
+                            .weight(1f)
+                            .fillMaxHeight()
+                            .padding(1.dp)
+                    ) {
+                        books.getOrNull(1)?.let {
+                            BookCover(
+                                name = it.name,
+                                author = it.author,
+                                path = it.getDisplayCover(),
+                                modifier = Modifier.fillMaxSize()
+                            )
+                        }
                     }
                 }
-            }
-            Row(modifier = Modifier.weight(1f)) {
-                Box(
-                    modifier = Modifier
-                        .weight(1f)
-                        .fillMaxHeight()
-                        .padding(1.dp)
-                ) {
-                    books.getOrNull(2)?.let {
-                        BookCover(
-                            name = it.name,
-                            author = it.author,
-                            path = it.getDisplayCover(),
-                            modifier = Modifier.fillMaxSize()
-                        )
+                Row(modifier = Modifier.weight(1f)) {
+                    Box(
+                        modifier = Modifier
+                            .weight(1f)
+                            .fillMaxHeight()
+                            .padding(1.dp)
+                    ) {
+                        books.getOrNull(2)?.let {
+                            BookCover(
+                                name = it.name,
+                                author = it.author,
+                                path = it.getDisplayCover(),
+                                modifier = Modifier.fillMaxSize()
+                            )
+                        }
                     }
-                }
-                Box(
-                    modifier = Modifier
-                        .weight(1f)
-                        .fillMaxHeight()
-                        .padding(1.dp)
-                ) {
-                    books.getOrNull(3)?.let {
-                        BookCover(
-                            name = it.name,
-                            author = it.author,
-                            path = it.getDisplayCover(),
-                            modifier = Modifier.fillMaxSize()
-                        )
+                    Box(
+                        modifier = Modifier
+                            .weight(1f)
+                            .fillMaxHeight()
+                            .padding(1.dp)
+                    ) {
+                        books.getOrNull(3)?.let {
+                            BookCover(
+                                name = it.name,
+                                author = it.author,
+                                path = it.getDisplayCover(),
+                                modifier = Modifier.fillMaxSize()
+                            )
+                        }
                     }
                 }
             }
@@ -292,7 +303,7 @@ fun BookGroupItemGrid(
         isGrid = true,
         gridStyle = gridStyle,
         isCompact = false,
-        cover = { BookGroupCover(books = previewBooks, modifier = it) },
+        cover = { BookGroupCover(books = previewBooks, coverPath = group.cover, modifier = it) },
         title = group.groupName,
         modifier = modifier,
         titleSmallFont = titleSmallFont,
@@ -321,7 +332,7 @@ fun BookGroupItemList(
         isGrid = false,
         gridStyle = 0,
         isCompact = isCompact,
-        cover = { BookGroupCover(books = previewBooks, modifier = it) },
+        cover = { BookGroupCover(books = previewBooks, coverPath = group.cover, modifier = it) },
         title = group.groupName,
         titleSmallFont = titleSmallFont,
         titleCenter = titleCenter,

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

@@ -5,6 +5,7 @@ import android.content.res.Configuration
 import androidx.activity.compose.BackHandler
 import androidx.activity.compose.rememberLauncherForActivityResult
 import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.animation.AnimatedContent
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
@@ -162,6 +163,17 @@ fun BookshelfScreen(
         pageCount = { uiState.groups.size }
     )
 
+    LaunchedEffect(uiState.groups) {
+        if (uiState.groups.isNotEmpty()) {
+            val savedGroupId = BookshelfConfig.saveTabPosition
+            val savedGroupIndex = uiState.groups.indexOfFirst { it.groupId == savedGroupId }
+            if (savedGroupIndex >= 0 && savedGroupIndex != pagerState.currentPage) {
+                viewModel.changeGroup(savedGroupId)
+                pagerState.scrollToPage(savedGroupIndex)
+            }
+        }
+    }
+
     LaunchedEffect(pagerState) {
         snapshotFlow { pagerState.currentPage }
             .distinctUntilChanged()
@@ -348,75 +360,84 @@ fun BookshelfScreen(
                     enabled = pullToRefreshEnabled
                 )
         ) {
-            if (bookGroupStyle == 2 && isInFolderRoot) {
-                FastScrollLazyVerticalGrid(
-                    columns = GridCells.Fixed(bookshelfLayoutGrid.coerceAtLeast(1)),
-                    modifier = Modifier.fillMaxSize(),
-                    contentPadding = PaddingValues(
-                        top = paddingValues.calculateTopPadding(),
-                        bottom = 120.dp,
-                        start = if (bookshelfLayoutMode != 0) 12.dp else 0.dp,
-                        end = if (bookshelfLayoutMode != 0) 12.dp else 0.dp
-                    ),
-                    verticalArrangement = Arrangement.spacedBy(if (bookshelfLayoutMode != 0) 8.dp else 0.dp),
-                    horizontalArrangement = Arrangement.spacedBy(if (bookshelfLayoutMode != 0) 8.dp else 0.dp),
-                    showFastScroll = BookshelfConfig.showBookshelfFastScroller
-                ) {
-                    itemsIndexed(uiState.groups, key = { _, it -> it.groupId }) { index, group ->
-                        if (bookshelfLayoutMode == 0) {
-                            BookGroupItemList(
-                                group = group,
-                                previewBooks = uiState.groupPreviews[group.groupId] ?: emptyList(),
-                                isCompact = BookshelfConfig.bookshelfLayoutCompact,
-                                titleSmallFont = BookshelfConfig.bookshelfTitleSmallFont,
-                                titleCenter = BookshelfConfig.bookshelfTitleCenter,
-                                titleMaxLines = BookshelfConfig.bookshelfTitleMaxLines,
-                                onClick = {
-                                    scope.launch { pagerState.scrollToPage(index) }
-                                    isInFolderRoot = false
-                                },
-                                onLongClick = { showGroupManageSheet = true }
-                            )
-                        } else {
-                            BookGroupItemGrid(
-                                group = group,
-                                previewBooks = uiState.groupPreviews[group.groupId] ?: emptyList(),
-                                gridStyle = BookshelfConfig.bookshelfGridLayout,
-                                titleSmallFont = BookshelfConfig.bookshelfTitleSmallFont,
-                                titleCenter = BookshelfConfig.bookshelfTitleCenter,
-                                titleMaxLines = BookshelfConfig.bookshelfTitleMaxLines,
-                                coverShadow = BookshelfConfig.bookshelfCoverShadow,
-                                onClick = {
-                                    scope.launch { pagerState.scrollToPage(index) }
-                                    isInFolderRoot = false
-                                },
-                                onLongClick = { showGroupManageSheet = true }
-                            )
+            AnimatedContent(
+                targetState = isInFolderRoot,
+                label = "FolderTransition"
+            ) { isRoot ->
+                if (bookGroupStyle == 2 && isRoot) {
+                    FastScrollLazyVerticalGrid(
+                        columns = GridCells.Fixed(bookshelfLayoutGrid.coerceAtLeast(1)),
+                        modifier = Modifier.fillMaxSize(),
+                        contentPadding = PaddingValues(
+                            top = paddingValues.calculateTopPadding(),
+                            bottom = 120.dp,
+                            start = if (bookshelfLayoutMode != 0) 12.dp else 0.dp,
+                            end = if (bookshelfLayoutMode != 0) 12.dp else 0.dp
+                        ),
+                        verticalArrangement = Arrangement.spacedBy(if (bookshelfLayoutMode != 0) 8.dp else 0.dp),
+                        horizontalArrangement = Arrangement.spacedBy(if (bookshelfLayoutMode != 0) 8.dp else 0.dp),
+                        showFastScroll = BookshelfConfig.showBookshelfFastScroller
+                    ) {
+                        itemsIndexed(
+                            uiState.groups,
+                            key = { _, it -> it.groupId }) { index, group ->
+                            if (bookshelfLayoutMode == 0) {
+                                BookGroupItemList(
+                                    group = group,
+                                    previewBooks = uiState.groupPreviews[group.groupId]
+                                        ?: emptyList(),
+                                    isCompact = BookshelfConfig.bookshelfLayoutCompact,
+                                    titleSmallFont = BookshelfConfig.bookshelfTitleSmallFont,
+                                    titleCenter = BookshelfConfig.bookshelfTitleCenter,
+                                    titleMaxLines = BookshelfConfig.bookshelfTitleMaxLines,
+                                    onClick = {
+                                        scope.launch { pagerState.scrollToPage(index) }
+                                        isInFolderRoot = false
+                                    },
+                                    onLongClick = { showGroupManageSheet = true }
+                                )
+                            } else {
+                                BookGroupItemGrid(
+                                    group = group,
+                                    previewBooks = uiState.groupPreviews[group.groupId]
+                                        ?: emptyList(),
+                                    gridStyle = BookshelfConfig.bookshelfGridLayout,
+                                    titleSmallFont = BookshelfConfig.bookshelfTitleSmallFont,
+                                    titleCenter = BookshelfConfig.bookshelfTitleCenter,
+                                    titleMaxLines = BookshelfConfig.bookshelfTitleMaxLines,
+                                    coverShadow = BookshelfConfig.bookshelfCoverShadow,
+                                    onClick = {
+                                        scope.launch { pagerState.scrollToPage(index) }
+                                        isInFolderRoot = false
+                                    },
+                                    onLongClick = { showGroupManageSheet = true }
+                                )
+                            }
                         }
                     }
-                }
-            } else {
-                HorizontalPager(
-                    state = pagerState,
-                    modifier = Modifier.fillMaxSize(),
-                    beyondViewportPageCount = 3,
-                    key = { if (it < uiState.groups.size) uiState.groups[it].groupId else it }
-                ) { pageIndex ->
-                    val group = uiState.groups.getOrNull(pageIndex)
-                    if (group != null) {
-                        val booksFlow = remember(group.groupId) {
-                            viewModel.getBooksFlow(group.groupId)
+                } else {
+                    HorizontalPager(
+                        state = pagerState,
+                        modifier = Modifier.fillMaxSize(),
+                        beyondViewportPageCount = 3,
+                        key = { if (it < uiState.groups.size) uiState.groups[it].groupId else it }
+                    ) { pageIndex ->
+                        val group = uiState.groups.getOrNull(pageIndex)
+                        if (group != null) {
+                            val booksFlow = remember(group.groupId) {
+                                viewModel.getBooksFlow(group.groupId)
+                            }
+                            val books by booksFlow.collectAsState(emptyList())
+                            BookshelfPage(
+                                paddingValues = paddingValues,
+                                books = books,
+                                uiState = uiState,
+                                bookshelfLayoutMode = bookshelfLayoutMode,
+                                bookshelfLayoutGrid = bookshelfLayoutGrid,
+                                onBookClick = onBookClick,
+                                onBookLongClick = onBookLongClick
+                            )
                         }
-                        val books by booksFlow.collectAsState(emptyList())
-                        BookshelfPage(
-                            paddingValues = paddingValues,
-                            books = books,
-                            uiState = uiState,
-                            bookshelfLayoutMode = bookshelfLayoutMode,
-                            bookshelfLayoutGrid = bookshelfLayoutGrid,
-                            onBookClick = onBookClick,
-                            onBookLongClick = onBookLongClick
-                        )
                     }
                 }
             }

+ 10 - 3
app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt

@@ -138,11 +138,18 @@ class BookshelfViewModel(
         if (BookshelfConfig.bookGroupStyle in 2..3) {
             groups.associate { group ->
                 val groupBooks = if (group.groupId == BookGroup.IdAll) {
-                    allBooks.take(4)
+                    allBooks
                 } else {
-                    allBooks.filter { (it.group and group.groupId) != 0L }.take(4)
+                    allBooks.filter { (it.group and group.groupId) != 0L }
                 }
-                group.groupId to groupBooks
+                val sortedBooks = sortBooks(groupBooks, group)
+                val booksWithCover = sortedBooks.filter { it.getDisplayCover() != null }
+                val result = if (booksWithCover.size >= 4) {
+                    booksWithCover.take(4)
+                } else {
+                    (booksWithCover + sortedBooks.filter { it.getDisplayCover() == null }).take(4)
+                }
+                group.groupId to result
             }
         } else {
             emptyMap()

+ 58 - 6
app/src/main/java/io/legado/app/ui/widget/components/AppScaffold.kt

@@ -1,39 +1,91 @@
 package io.legado.app.ui.widget.components
 
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.RowScope
 import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.FabPosition
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Scaffold
 import androidx.compose.material3.ScaffoldDefaults
+import androidx.compose.material3.TopAppBarColors
+import androidx.compose.material3.TopAppBarScrollBehavior
 import androidx.compose.material3.contentColorFor
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
+import dev.chrisbanes.haze.HazeState
+import io.legado.app.ui.config.themeConfig.ThemeConfig
+import io.legado.app.ui.theme.responsiveHazeEffect
+import io.legado.app.ui.theme.responsiveHazeSource
 
+@OptIn(ExperimentalMaterial3Api::class)
 @Composable
 fun AppScaffold(
     modifier: Modifier = Modifier,
+    title: (@Composable () -> Unit)? = null,
+    subtitle: (@Composable () -> Unit)? = null,
+    scrollBehavior: TopAppBarScrollBehavior? = null,
+    navigationIcon: @Composable () -> Unit = {},
+    actions: @Composable RowScope.() -> Unit = {},
+    colors: TopAppBarColors = GlassTopAppBarDefaults.glassColors(),
     topBar: @Composable () -> Unit = {},
     bottomBar: @Composable () -> Unit = {},
     snackbarHost: @Composable () -> Unit = {},
     floatingActionButton: @Composable () -> Unit = {},
     floatingActionButtonPosition: FabPosition = FabPosition.End,
-    containerColor: Color = Color.Transparent,
     contentColor: Color = contentColorFor(MaterialTheme.colorScheme.background),
     contentWindowInsets: WindowInsets = ScaffoldDefaults.contentWindowInsets,
     content: @Composable (PaddingValues) -> Unit
 ) {
+    val isDark = isSystemInDarkTheme()
+    val hasImageBg = ThemeConfig.hasImageBg(isDark)
+    val hazeState = remember { HazeState() }
+
+    val containerColor = if (hasImageBg) {
+        Color.Transparent
+    } else {
+        MaterialTheme.colorScheme.background
+    }
+
+    val finalTopBar = @Composable {
+        Box(modifier = Modifier.responsiveHazeEffect(state = hazeState)) {
+            if (title != null) {
+                GlassMediumFlexibleTopAppBar(
+                    title = title,
+                    subtitle = subtitle,
+                    scrollBehavior = scrollBehavior,
+                    navigationIcon = navigationIcon,
+                    actions = actions,
+                    colors = colors
+                )
+            } else {
+                topBar()
+            }
+        }
+    }
+
     Scaffold(
         modifier = modifier,
-        topBar = topBar,
+        topBar = finalTopBar,
         bottomBar = bottomBar,
         snackbarHost = snackbarHost,
         floatingActionButton = floatingActionButton,
         floatingActionButtonPosition = floatingActionButtonPosition,
         containerColor = containerColor,
         contentColor = contentColor,
-        contentWindowInsets = contentWindowInsets,
-        content = content
-    )
-}
+        contentWindowInsets = contentWindowInsets
+    ) { paddingValues ->
+        Box(
+            modifier = Modifier
+                .fillMaxSize()
+                .responsiveHazeSource(hazeState)
+        ) {
+            content(paddingValues)
+        }
+    }
+}

+ 1 - 4
app/src/main/java/io/legado/app/ui/widget/components/GlassMediumFlexibleTopAppBar.kt

@@ -75,10 +75,7 @@ object GlassTopAppBarDefaults {
 
         return TopAppBarDefaults.topAppBarColors(
             containerColor = containerColor,
-            scrolledContainerColor = scrolledContainerColor,
-            navigationIconContentColor = MaterialTheme.colorScheme.onSurface,
-            titleContentColor = MaterialTheme.colorScheme.onSurface,
-            actionIconContentColor = MaterialTheme.colorScheme.onSurfaceVariant
+            scrolledContainerColor = scrolledContainerColor
         )
     }
 

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

@@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.ColumnScope
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.Card
 import androidx.compose.material3.CardDefaults
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Text
@@ -14,7 +15,6 @@ import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.unit.dp
-import io.legado.app.ui.widget.components.card.GlassCard
 
 /**
  * Settings Group Container by https://github.com/wxxsfxyzm/InstallerX-Revived
@@ -29,17 +29,17 @@ fun SplicedColumnGroup(
     title: String = "",
     content: @Composable ColumnScope.() -> Unit,
 ) {
-    Column(modifier = modifier.padding(top = 4.dp, bottom = 12.dp)) {
+    Column(modifier = modifier.padding(top = 8.dp, bottom = 16.dp)) {
         if (title.isNotEmpty()) {
             Text(
                 text = title,
                 style = MaterialTheme.typography.labelLarge,
                 color = MaterialTheme.colorScheme.primary,
-                modifier = Modifier.padding(start = 16.dp, bottom = 8.dp, top = 8.dp)
+                modifier = Modifier.padding(start = 16.dp, bottom = 8.dp)
             )
         }
 
-        GlassCard(
+        Card(
             shape = RoundedCornerShape(16.dp),
             colors = CardDefaults.cardColors(containerColor = Color.Transparent),
             modifier = Modifier.fillMaxWidth()

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

@@ -26,11 +26,18 @@ fun GlassCard(
 ) {
     val containerAlpha = ThemeConfig.containerOpacity / 100f
 
-    val finalColors = colors ?: CardDefaults.cardColors(
-        containerColor = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = containerAlpha),
+    val baseColors = colors ?: CardDefaults.cardColors(
+        containerColor = MaterialTheme.colorScheme.secondaryContainer,
         contentColor = MaterialTheme.colorScheme.onSecondaryContainer
     )
 
+    val finalColors = CardDefaults.cardColors(
+        containerColor = baseColors.containerColor.copy(alpha = containerAlpha),
+        contentColor = baseColors.contentColor,
+        disabledContainerColor = baseColors.disabledContainerColor.copy(alpha = containerAlpha),
+        disabledContentColor = baseColors.disabledContentColor
+    )
+
     if (onClick != null) {
         Card(
             onClick = onClick,

+ 13 - 5
app/src/main/java/io/legado/app/ui/widget/components/cover/BookCover.kt

@@ -54,17 +54,25 @@ fun BookCover(
     badgeText: String? = null,
     showBadgeDot: Boolean = false,
     sourceOrigin: String? = null,
-    onLoadFinish: (() -> Unit)? = null
+    onLoadFinish: (() -> Unit)? = null,
+    ignoreUseDefaultCover: Boolean = false
 ) {
     val context = LocalContext.current
     val isNight = isSystemInDarkTheme()
 
-    val useDefault = CoverConfig.useDefaultCover
+    val useDefault = !ignoreUseDefaultCover && CoverConfig.useDefaultCover
     val finalPath = if (useDefault) null else path
 
-    // 获取随机封面路径(字符串),而非直接获取 Drawable,避免主线程解码大图
-    val randomPath = remember(name, author, path, isNight) {
-        BookCover.getRandomDefaultPath(seed = name ?: author ?: path ?: "")
+    // 获取随机封面路径(字符串),增加配置项作为 Key,确保设置更改后立即刷新
+    val randomPath = remember(
+        name, author, path, isNight,
+        CoverConfig.defaultCover,
+        CoverConfig.defaultCoverDark
+    ) {
+        BookCover.getRandomDefaultPath(
+            seed = name ?: author ?: path ?: "",
+            isNight = isNight
+        )
     }
 
     // 检查是否有自定义随机封面设置

+ 37 - 41
app/src/main/java/io/legado/app/ui/widget/components/dialog/ColorPickerSheet.kt

@@ -95,7 +95,7 @@ fun ColorPickerSheet(
             HsvColorPicker(
                 modifier = Modifier
                     .fillMaxWidth()
-                    .height(250.dp)
+                    .height(240.dp)
                     .padding(10.dp),
                 controller = controller,
                 initialColor = Color(initialColor),
@@ -122,34 +122,6 @@ fun ColorPickerSheet(
                 controller = controller,
             )
 
-            Spacer(modifier = Modifier.height(16.dp))
-
-            Row(
-                verticalAlignment = Alignment.CenterVertically,
-                modifier = Modifier
-                    .fillMaxWidth()
-                    .padding(horizontal = 10.dp)
-            ) {
-                Box(
-                    modifier = Modifier
-                        .size(40.dp)
-                        .clip(RoundedCornerShape(8.dp))
-                        .background(currentColor)
-                        .border(
-                            1.dp,
-                            MaterialTheme.colorScheme.outlineVariant,
-                            RoundedCornerShape(8.dp)
-                        )
-                )
-                Spacer(modifier = Modifier.width(12.dp))
-                Text(
-                    text = "#${Integer.toHexString(currentColor.toArgb()).uppercase()}",
-                    style = MaterialTheme.typography.bodyLarge
-                )
-            }
-
-            Spacer(modifier = Modifier.height(16.dp))
-
             LazyVerticalGrid(
                 columns = GridCells.Adaptive(36.dp),
                 verticalArrangement = Arrangement.spacedBy(8.dp),
@@ -176,21 +148,45 @@ fun ColorPickerSheet(
                 }
             }
 
-            Spacer(modifier = Modifier.height(24.dp))
+            Spacer(modifier = Modifier.height(16.dp))
 
             Row(
-                modifier = Modifier.fillMaxWidth(),
-                horizontalArrangement = Arrangement.End
+                verticalAlignment = Alignment.CenterVertically,
+                modifier = Modifier
+                    .fillMaxWidth()
+                    .padding(horizontal = 10.dp)
             ) {
-                TextButton(onClick = onDismissRequest) {
-                    Text(stringResource(R.string.cancel))
-                }
-                Spacer(modifier = Modifier.width(8.dp))
-                Button(onClick = {
-                    onColorSelected(currentColor.toArgb())
-                    onDismissRequest()
-                }) {
-                    Text(stringResource(R.string.ok))
+                Box(
+                    modifier = Modifier
+                        .size(40.dp)
+                        .clip(RoundedCornerShape(8.dp))
+                        .background(currentColor)
+                        .border(
+                            1.dp,
+                            MaterialTheme.colorScheme.outlineVariant,
+                            RoundedCornerShape(8.dp)
+                        )
+                )
+                Spacer(modifier = Modifier.width(12.dp))
+                Text(
+                    text = "#${Integer.toHexString(currentColor.toArgb()).uppercase()}",
+                    style = MaterialTheme.typography.bodyLarge
+                )
+
+                Row(
+                    modifier = Modifier.fillMaxWidth(),
+                    horizontalArrangement = Arrangement.End
+                ) {
+                    TextButton(onClick = onDismissRequest) {
+                        Text(stringResource(R.string.cancel))
+                    }
+                    Spacer(modifier = Modifier.width(8.dp))
+                    Button(onClick = {
+                        onColorSelected(currentColor.toArgb())
+                        onDismissRequest()
+                    }) {
+                        Text(stringResource(R.string.ok))
+                    }
                 }
             }
         }

+ 0 - 1
app/src/main/java/io/legado/app/ui/widget/components/importComponents/ImportComponents.kt

@@ -145,7 +145,6 @@ fun <T> BatchImportDialog(
             modifier = Modifier
                 .fillMaxWidth()
                 .heightIn(max = LocalConfiguration.current.screenHeightDp.dp * 0.8f),
-            containerColor = Color.Transparent,
             topBar = {
                 CenterAlignedTopAppBar(
                     title = {

+ 15 - 23
app/src/main/java/io/legado/app/ui/widget/components/list/ListScaffold.kt

@@ -35,9 +35,6 @@ import androidx.compose.ui.Modifier
 import androidx.compose.ui.input.nestedscroll.nestedScroll
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.zIndex
-import dev.chrisbanes.haze.HazeState
-import io.legado.app.ui.theme.responsiveHazeEffect
-import io.legado.app.ui.theme.responsiveHazeSource
 import io.legado.app.ui.widget.components.AppScaffold
 import io.legado.app.ui.widget.components.GlassTopAppBarDefaults
 import io.legado.app.ui.widget.components.SelectionActions
@@ -84,7 +81,6 @@ fun <T> ListScaffold(
     content: @Composable (PaddingValues) -> Unit
 ) {
     val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
-    val hazeState = remember { HazeState() }
 
     AppScaffold(
         modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
@@ -96,29 +92,25 @@ fun <T> ListScaffold(
             )
         },
         topBar = {
-            Box(modifier = Modifier.responsiveHazeEffect(state = hazeState)) {
-                DynamicTopAppBar(
-                    title = title,
-                    subtitle = subtitle,
-                    state = state,
-                    scrollBehavior = scrollBehavior,
-                    onBackClick = onBackClick,
-                    onSearchToggle = onSearchToggle,
-                    onSearchQueryChange = onSearchQueryChange,
-                    searchPlaceholder = searchPlaceholder,
-                    onClearSelection = { selectionActions?.onSelectInvert?.invoke() },
-                    topBarActions = topBarActions,
-                    dropDownMenuContent = dropDownMenuContent,
-                    bottomContent = bottomContent
-                )
-            }
+            DynamicTopAppBar(
+                title = title,
+                subtitle = subtitle,
+                state = state,
+                scrollBehavior = scrollBehavior,
+                onBackClick = onBackClick,
+                onSearchToggle = onSearchToggle,
+                onSearchQueryChange = onSearchQueryChange,
+                searchPlaceholder = searchPlaceholder,
+                onClearSelection = { selectionActions?.onSelectInvert?.invoke() },
+                topBarActions = topBarActions,
+                dropDownMenuContent = dropDownMenuContent,
+                bottomContent = bottomContent
+            )
         },
         floatingActionButton = floatingActionButton
     ) { paddingValues ->
         Box(
-            modifier = Modifier
-                .fillMaxSize()
-                .responsiveHazeSource(hazeState)
+            modifier = Modifier.fillMaxSize()
         ) {
             content(paddingValues)
 

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

@@ -50,9 +50,8 @@ fun SliderSettingItem(
 
     SettingItem(
         title = title,
-        description = description,
         color = color ?: MaterialTheme.colorScheme.surfaceContainerLow,
-        option = value.toInt().toString(),
+        option = description,
         expanded = expanded,
         onExpandChange = { expanded = it },
         expandContent = {

+ 15 - 11
app/src/main/java/io/legado/app/ui/widget/image/CoverImageView.kt

@@ -23,6 +23,7 @@ import io.legado.app.help.config.AppConfig
 import io.legado.app.help.glide.ImageLoader
 import io.legado.app.help.glide.OkHttpModelLoader
 import io.legado.app.model.BookCover
+import io.legado.app.ui.config.coverConfig.CoverConfig
 import io.legado.app.utils.spToPx
 import io.legado.app.utils.textHeight
 import io.legado.app.utils.themeColor
@@ -46,9 +47,9 @@ class CoverImageView @JvmOverloads constructor(
     private var author: String? = null
     private var nameHeight = 0f
     private var authorHeight = 0f
-    private val isNightTheme = AppConfig.isNightTheme
-    val colorKey = if (isNightTheme) BookCover.coverTextColorN else BookCover.coverTextColor
-    val shadowKey = if (isNightTheme) BookCover.coverShadowColorN else BookCover.coverShadowColor
+    private val isNightTheme get() = AppConfig.isNightTheme
+    private val colorKey get() = if (isNightTheme) CoverConfig.coverTextColorN else CoverConfig.coverTextColor
+    private val shadowKey get() = if (isNightTheme) CoverConfig.coverShadowColorN else CoverConfig.coverShadowColor
     private val namePaint by lazy {
         val textPaint = TextPaint()
         textPaint.typeface = Typeface.DEFAULT_BOLD
@@ -123,13 +124,14 @@ class CoverImageView @JvmOverloads constructor(
     private fun drawNameAuthor(canvas: Canvas) {
         var startX = width * 0.2f
         var startY = viewHeight * 0.2f
-        if (BookCover.drawBookName) {
+        val showName = if (isNightTheme) CoverConfig.coverShowNameN else CoverConfig.coverShowName
+        if (showName) {
             name?.toStringArray()?.let { nameChars ->
                 val textSize = viewWidth / 8
                 namePaint.textSize = textSize
                 namePaint.strokeWidth = textSize / 8
 
-                if (BookCover.drawBookShadow) {
+                if (CoverConfig.coverShowShadow) {
                     namePaint.setShadowLayer(
                         4f,
                         0f,
@@ -141,12 +143,12 @@ class CoverImageView @JvmOverloads constructor(
                 }
 
                 nameChars.forEach { char ->
-                    if (BookCover.drawBookStroke) {
+                    if (CoverConfig.coverShowStroke) {
                         namePaint.color = Color.WHITE
                         namePaint.style = Paint.Style.STROKE
                         canvas.drawText(char, startX, startY, namePaint)
                     }
-                    if (BookCover.coverDefaultColor)
+                    if (CoverConfig.coverDefaultColor)
                         namePaint.color = context.themeColor(com.google.android.material.R.attr.colorSecondary)
                     else
                         namePaint.color = colorKey
@@ -164,14 +166,16 @@ class CoverImageView @JvmOverloads constructor(
             }
         }
 
-        if (BookCover.drawBookAuthor) {
+        val showAuthor =
+            if (isNightTheme) CoverConfig.coverShowAuthorN else CoverConfig.coverShowAuthor
+        if (showAuthor) {
             author?.toStringArray()?.let { author ->
                 authorPaint.textSize = viewWidth / 10
                 authorPaint.strokeWidth = authorPaint.textSize / 5
                 startX = width * 0.8f
                 startY = viewHeight * 0.95f - author.size * authorPaint.textHeight
                 startY = maxOf(startY, viewHeight * 0.2f)
-                if (BookCover.drawBookShadow) {
+                if (CoverConfig.coverShowShadow) {
                     authorPaint.setShadowLayer(
                         4f,
                         0f,
@@ -183,12 +187,12 @@ class CoverImageView @JvmOverloads constructor(
                 }
 
                 author.forEach {
-                    if (BookCover.drawBookStroke) {
+                    if (CoverConfig.coverShowStroke) {
                         authorPaint.color = Color.WHITE
                         authorPaint.style = Paint.Style.STROKE
                         canvas.drawText(it, startX, startY, authorPaint)
                     }
-                    if (BookCover.coverDefaultColor)
+                    if (CoverConfig.coverDefaultColor)
                         authorPaint.color = context.themeColor(com.google.android.material.R.attr.colorSecondary)
                     else
                         authorPaint.color = colorKey