diff --git a/app/src/main/java/io/legado/app/di/appModule.kt b/app/src/main/java/io/legado/app/di/appModule.kt index 0fc235929..2f9ab4d95 100644 --- a/app/src/main/java/io/legado/app/di/appModule.kt +++ b/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) diff --git a/app/src/main/java/io/legado/app/model/BookCover.kt b/app/src/main/java/io/legado/app/model/BookCover.kt index f0d33dc1b..4a6a980a9 100644 --- a/app/src/main/java/io/legado/app/model/BookCover.kt +++ b/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() - } + 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() } - @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) + if (paths.isNullOrEmpty()) { + return appCtx.resources.getDrawable(R.drawable.image_cover_default, null) + } - val key = if (isNightTheme) PreferKey.defaultCoverDark else PreferKey.defaultCover - val paths = appCtx.getPrefString(key)?.split(",")?.filter { it.isNotBlank() } - - if (paths.isNullOrEmpty()) { - defaultDrawable = appCtx.resources.getDrawable(R.drawable.image_cover_default, null) - return + val randomPath = paths[Random.nextInt(paths.size)] + 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 diff --git a/app/src/main/java/io/legado/app/ui/config/PrefDelegate.kt b/app/src/main/java/io/legado/app/ui/config/PrefDelegate.kt index bfb5145b9..167fd886d 100644 --- a/app/src/main/java/io/legado/app/ui/config/PrefDelegate.kt +++ b/app/src/main/java/io/legado/app/ui/config/PrefDelegate.kt @@ -74,7 +74,7 @@ fun 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) diff --git a/app/src/main/java/io/legado/app/ui/config/TestConfigActivity.kt b/app/src/main/java/io/legado/app/ui/config/TestConfigActivity.kt index 49435c230..8d1642680 100644 --- a/app/src/main/java/io/legado/app/ui/config/TestConfigActivity.kt +++ b/app/src/main/java/io/legado/app/ui/config/TestConfigActivity.kt @@ -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 { - ThemeConfigScreen(onBackClick = { navController.popBackStack() }) + ThemeConfigScreen( + onBackClick = { navController.popBackStack() } + ) + } + + composable { + 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 + ) } } } diff --git a/app/src/main/java/io/legado/app/ui/config/backupConfig/BackupConfig.kt b/app/src/main/java/io/legado/app/ui/config/backupConfig/BackupConfig.kt new file mode 100644 index 000000000..d4a42d957 --- /dev/null +++ b/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( + PreferKey.backupPath, + null + ) + +} diff --git a/app/src/main/java/io/legado/app/ui/config/backupConfig/BackupConfigScreen.kt b/app/src/main/java/io/legado/app/ui/config/backupConfig/BackupConfigScreen.kt new file mode 100644 index 000000000..ac34f94a4 --- /dev/null +++ b/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>(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) +} diff --git a/app/src/main/java/io/legado/app/ui/config/backupConfig/BackupConfigViewModel.kt b/app/src/main/java/io/legado/app/ui/config/backupConfig/BackupConfigViewModel.kt new file mode 100644 index 000000000..9a260ed41 --- /dev/null +++ b/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 { + 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 ?: "恢复出错") + } + } + } + } + +} diff --git a/app/src/main/java/io/legado/app/ui/config/themeConfig/BackgroundImageManageSheet.kt b/app/src/main/java/io/legado/app/ui/config/themeConfig/BackgroundImageManageSheet.kt index bcf0e4cec..f10ebfe91 100644 --- a/app/src/main/java/io/legado/app/ui/config/themeConfig/BackgroundImageManageSheet.kt +++ b/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) diff --git a/app/src/main/java/io/legado/app/ui/config/themeConfig/LauncherIconPickerSheet.kt b/app/src/main/java/io/legado/app/ui/config/themeConfig/LauncherIconPickerSheet.kt new file mode 100644 index 000000000..26e1ab766 --- /dev/null +++ b/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 } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfig.kt b/app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfig.kt index 750bbcfed..c9436dbb2 100644 --- a/app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfig.kt +++ b/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(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() - } + var isPredictiveBackEnabled by prefDelegate(PreferKey.isPredictiveBackEnabled, true) - Log.d( - "MainConfig", - "hasImageBg -> isDark=$isDark, " + - "bgImageDark=$bgImageDark, " + - "bgImageLight=$bgImageLight, " + - "result=$result" - ) + var customMode by prefDelegate(PreferKey.customMode, "tonalSpot") - return result + 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() } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigScreen.kt b/app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigScreen.kt index bbb2dc2e7..a8747b89e 100644 --- a/app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigScreen.kt +++ b/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?>(null) } + var manageKey by remember { mutableStateOf(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 } ) + 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) + ), + 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.palette_style), - selectedValue = ThemeConfig.paletteStyle, - displayEntries = arrayOf( - "柔和", "中性", "鲜艳", "表现力", "彩虹", - "水果拼盘", "单色", "仿真", "内容取色" - ), - entryValues = arrayOf( - "tonalSpot", "neutral", "vibrant", "expressive", "rainbow", - "fruitSalad", "monochrome", "fidelity", "content" - ), - onValueChange = { ThemeConfig.paletteStyle = it } + 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>, + 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 + ) } diff --git a/app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigViewModel.kt b/app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigViewModel.kt index 7ee6b4aa1..32aa996cc 100644 --- a/app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigViewModel.kt +++ b/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 } } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookItem.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookItem.kt index 2469cc5dc..7e9696cb8 100644 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookItem.kt +++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookItem.kt @@ -198,76 +198,87 @@ fun BookshelfItem( @Composable fun BookGroupCover( books: List, + 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, diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt index a278ba3ee..aeee948af 100644 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt +++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt @@ -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 - ) } } } diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt index 0e19716ea..c1c27a9c0 100644 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt @@ -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() diff --git a/app/src/main/java/io/legado/app/ui/widget/components/AppScaffold.kt b/app/src/main/java/io/legado/app/ui/widget/components/AppScaffold.kt index 20deec8e1..696556ded 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/AppScaffold.kt +++ b/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) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/components/GlassMediumFlexibleTopAppBar.kt b/app/src/main/java/io/legado/app/ui/widget/components/GlassMediumFlexibleTopAppBar.kt index 5fe9c3ccb..28a90d991 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/GlassMediumFlexibleTopAppBar.kt +++ b/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 ) } diff --git a/app/src/main/java/io/legado/app/ui/widget/components/SplicedColumnGroup.kt b/app/src/main/java/io/legado/app/ui/widget/components/SplicedColumnGroup.kt index 72ea9d303..c6cb1912e 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/SplicedColumnGroup.kt +++ b/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() diff --git a/app/src/main/java/io/legado/app/ui/widget/components/card/GlassCard.kt b/app/src/main/java/io/legado/app/ui/widget/components/card/GlassCard.kt index b76ecefc9..43de6eedf 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/card/GlassCard.kt +++ b/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, diff --git a/app/src/main/java/io/legado/app/ui/widget/components/cover/BookCover.kt b/app/src/main/java/io/legado/app/ui/widget/components/cover/BookCover.kt index 07d755e9e..fd0772934 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/cover/BookCover.kt +++ b/app/src/main/java/io/legado/app/ui/widget/components/cover/BookCover.kt @@ -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 + ) } // 检查是否有自定义随机封面设置 diff --git a/app/src/main/java/io/legado/app/ui/widget/components/dialog/ColorPickerSheet.kt b/app/src/main/java/io/legado/app/ui/widget/components/dialog/ColorPickerSheet.kt index 54ec573f7..0c124e227 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/dialog/ColorPickerSheet.kt +++ b/app/src/main/java/io/legado/app/ui/widget/components/dialog/ColorPickerSheet.kt @@ -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)) + } } } } diff --git a/app/src/main/java/io/legado/app/ui/widget/components/importComponents/ImportComponents.kt b/app/src/main/java/io/legado/app/ui/widget/components/importComponents/ImportComponents.kt index b5ac6c033..18caaf8f1 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/importComponents/ImportComponents.kt +++ b/app/src/main/java/io/legado/app/ui/widget/components/importComponents/ImportComponents.kt @@ -145,7 +145,6 @@ fun BatchImportDialog( modifier = Modifier .fillMaxWidth() .heightIn(max = LocalConfiguration.current.screenHeightDp.dp * 0.8f), - containerColor = Color.Transparent, topBar = { CenterAlignedTopAppBar( title = { diff --git a/app/src/main/java/io/legado/app/ui/widget/components/list/ListScaffold.kt b/app/src/main/java/io/legado/app/ui/widget/components/list/ListScaffold.kt index 23d7ca88a..de0e53644 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/list/ListScaffold.kt +++ b/app/src/main/java/io/legado/app/ui/widget/components/list/ListScaffold.kt @@ -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 ListScaffold( content: @Composable (PaddingValues) -> Unit ) { val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior() - val hazeState = remember { HazeState() } AppScaffold( modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), @@ -96,29 +92,25 @@ fun 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) diff --git a/app/src/main/java/io/legado/app/ui/widget/components/settingItem/SliderSettingItem.kt b/app/src/main/java/io/legado/app/ui/widget/components/settingItem/SliderSettingItem.kt index f38e6dbf2..d1f3a5b39 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/settingItem/SliderSettingItem.kt +++ b/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 = { diff --git a/app/src/main/java/io/legado/app/ui/widget/image/CoverImageView.kt b/app/src/main/java/io/legado/app/ui/widget/image/CoverImageView.kt index bf43c5e6d..516918ce9 100644 --- a/app/src/main/java/io/legado/app/ui/widget/image/CoverImageView.kt +++ b/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