[优化] 新增了新的外观设置、备份与恢复设置,现在可以在高级、测试处体验。

[修复] 封面状态不同步的问题、文件夹封面和书籍封面获取的问题、容器透明度不生效的问题等
This commit is contained in:
HapeLee committed 2026-03-23 01:36:31 +08:00
1 parent 1e41ce5ded
commit 36649527ed
25 files changed
+1848 -196

No files matched your search

@@ -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.searchContent.SearchContentViewModel
import io.legado.app.ui.book.toc.TocViewModel import io.legado.app.ui.book.toc.TocViewModel
import io.legado.app.ui.book.toc.rule.TxtTocRuleViewModel 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.coverConfig.CoverConfigViewModel
import io.legado.app.ui.config.otherConfig.OtherConfigViewModel import io.legado.app.ui.config.otherConfig.OtherConfigViewModel
import io.legado.app.ui.config.readConfig.ReadConfigViewModel import io.legado.app.ui.config.readConfig.ReadConfigViewModel
@@ -94,6 +95,7 @@ val appModule = module {
viewModelOf(::ReadConfigViewModel) viewModelOf(::ReadConfigViewModel)
viewModelOf(::CoverConfigViewModel) viewModelOf(::CoverConfigViewModel)
viewModelOf(::ThemeConfigViewModel) viewModelOf(::ThemeConfigViewModel)
viewModelOf(::BackupConfigViewModel)
viewModelOf(::TocViewModel) viewModelOf(::TocViewModel)
viewModelOf(::RemoteBookViewModel) viewModelOf(::RemoteBookViewModel)
viewModelOf(::BookInfoViewModel) viewModelOf(::BookInfoViewModel)
@@ -3,7 +3,6 @@ package io.legado.app.model
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.Color
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import androidx.annotation.Keep import androidx.annotation.Keep
import androidx.core.graphics.drawable.toDrawable 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.BitmapUtils
import io.legado.app.utils.GSON import io.legado.app.utils.GSON
import io.legado.app.utils.fromJsonObject 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 io.legado.app.utils.getPrefString
import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.currentCoroutineContext
import splitties.init.appCtx import splitties.init.appCtx
@@ -48,76 +45,32 @@ object BookCover {
private const val coverRuleConfigKey = "legadoCoverRuleConfig" private const val coverRuleConfigKey = "legadoCoverRuleConfig"
const val configFileName = "coverRule.json" 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") @SuppressLint("UseCompatLoadingForDrawables")
fun upDefaultCover() { get() {
val isNightTheme = AppConfig.isNightTheme 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 key = if (isNightTheme) PreferKey.defaultCoverDark else PreferKey.defaultCover val key = if (isNightTheme) PreferKey.defaultCoverDark else PreferKey.defaultCover
val paths = appCtx.getPrefString(key)?.split(",")?.filter { it.isNotBlank() } val paths = appCtx.getPrefString(key)?.split(",")?.filter { it.isNotBlank() }
if (paths.isNullOrEmpty()) { if (paths.isNullOrEmpty()) {
defaultDrawable = appCtx.resources.getDrawable(R.drawable.image_cover_default, null) return appCtx.resources.getDrawable(R.drawable.image_cover_default, null)
return
} }
val randomPath = paths[Random.nextInt(paths.size)] val randomPath = paths[Random.nextInt(paths.size)]
defaultDrawable = kotlin.runCatching { return kotlin.runCatching {
BitmapUtils.decodeBitmap(randomPath, 600, 900)!!.toDrawable(appCtx.resources) BitmapUtils.decodeBitmap(randomPath, 600, 900)!!.toDrawable(appCtx.resources)
}.getOrDefault(appCtx.resources.getDrawable(R.drawable.image_cover_default, null)) }.getOrDefault(appCtx.resources.getDrawable(R.drawable.image_cover_default, null))
} }
fun getRandomDefaultPath(seed: Any? = null): String? { // 兼容旧代码,空实现
val isNightTheme = AppConfig.isNightTheme fun upDefaultCover() {}
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() } val paths = appCtx.getPrefString(key)?.split(",")?.filter { it.isNotBlank() }
if (paths.isNullOrEmpty()) return null if (paths.isNullOrEmpty()) return null
val random = if (seed != null) Random(seed.hashCode()) else Random val random = if (seed != null) Random(seed.hashCode()) else Random
@@ -74,7 +74,7 @@ fun <T> prefDelegate(
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
if (_value.value != value) { if (_value.value != value) {
when (value) { when (value) {
is String -> appCtx.putPrefString(key, value) is String? -> appCtx.putPrefString(key, value)
is Int -> appCtx.putPrefInt(key, value) is Int -> appCtx.putPrefInt(key, value)
is Boolean -> appCtx.putPrefBoolean(key, value) is Boolean -> appCtx.putPrefBoolean(key, value)
is Long -> appCtx.putPrefLong(key, value) is Long -> appCtx.putPrefLong(key, value)
@@ -17,6 +17,7 @@ import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseComposeActivity 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.coverConfig.CoverConfigScreen
import io.legado.app.ui.config.otherConfig.OtherConfigScreen import io.legado.app.ui.config.otherConfig.OtherConfigScreen
import io.legado.app.ui.config.readConfig.ReadConfigScreen import io.legado.app.ui.config.readConfig.ReadConfigScreen
@@ -46,6 +47,9 @@ class TestConfigActivity : BaseComposeActivity() {
@Serializable @Serializable
object ThemeConfigRoute object ThemeConfigRoute
@Serializable
object BackupConfigRoute
@Composable @Composable
override fun Content() { override fun Content() {
val navController = rememberNavController() val navController = rememberNavController()
@@ -60,7 +64,8 @@ class TestConfigActivity : BaseComposeActivity() {
onNavigateToOther = { navController.navigate(OtherConfigRoute) }, onNavigateToOther = { navController.navigate(OtherConfigRoute) },
onNavigateToRead = { navController.navigate(ReadConfigRoute) }, onNavigateToRead = { navController.navigate(ReadConfigRoute) },
onNavigateToCover = { navController.navigate(CoverConfigRoute) }, 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> { 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, onNavigateToOther: () -> Unit,
onNavigateToRead: () -> Unit, onNavigateToRead: () -> Unit,
onNavigateToCover: () -> Unit, onNavigateToCover: () -> Unit,
onNavigateToTheme: () -> Unit onNavigateToTheme: () -> Unit,
onNavigateToBackup: () -> Unit
) { ) {
val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior() val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
@@ -129,6 +141,10 @@ class TestConfigActivity : BaseComposeActivity() {
title = stringResource(R.string.cover_config), title = stringResource(R.string.cover_config),
onClick = onNavigateToCover onClick = onNavigateToCover
) )
ClickableSettingItem(
title = stringResource(R.string.backup_restore),
onClick = onNavigateToBackup
)
} }
} }
} }
@@ -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
)
}
@@ -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)
}
@@ -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 ?: "恢复出错")
}
}
}
}
}
@@ -39,7 +39,6 @@ import org.koin.androidx.compose.koinViewModel
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun BackgroundImageManageSheet( fun BackgroundImageManageSheet(
preferenceKey: String,
isDarkTheme: Boolean, isDarkTheme: Boolean,
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
viewModel: ThemeConfigViewModel = koinViewModel() viewModel: ThemeConfigViewModel = koinViewModel()
@@ -53,7 +52,7 @@ fun BackgroundImageManageSheet(
scope.launch { scope.launch {
viewModel.setBackgroundFromUri( viewModel.setBackgroundFromUri(
uri = it, uri = it,
preferenceKey = preferenceKey isDarkTheme = isDarkTheme
) )
} }
} }
@@ -118,7 +117,7 @@ fun BackgroundImageManageSheet(
) )
} }
IconButton( IconButton(
onClick = { viewModel.removeBackground(preferenceKey) }, onClick = { viewModel.removeBackground(isDarkTheme) },
modifier = Modifier modifier = Modifier
.align(Alignment.TopEnd) .align(Alignment.TopEnd)
.padding(8.dp) .padding(8.dp)
@@ -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 }
}
}
@@ -1,6 +1,5 @@
package io.legado.app.ui.config.themeConfig package io.legado.app.ui.config.themeConfig
import android.util.Log
import io.legado.app.constant.EventBus import io.legado.app.constant.EventBus
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.ui.config.prefDelegate import io.legado.app.ui.config.prefDelegate
@@ -20,6 +19,8 @@ object ThemeConfig {
var appTheme by prefDelegate(PreferKey.appTheme, "0") var appTheme by prefDelegate(PreferKey.appTheme, "0")
var themeMode by prefDelegate(PreferKey.themeMode, "0")
var isPureBlack by prefDelegate(PreferKey.pureBlack, false) var isPureBlack by prefDelegate(PreferKey.pureBlack, false)
var bgImageLight by prefDelegate<String?>(PreferKey.bgImage, null) { var bgImageLight by prefDelegate<String?>(PreferKey.bgImage, null) {
@@ -34,28 +35,37 @@ object ThemeConfig {
var bgImageNBlurring by prefDelegate(PreferKey.bgImageNBlurring, 0) var bgImageNBlurring by prefDelegate(PreferKey.bgImageNBlurring, 0)
fun hasImageBg(isDark: Boolean): Boolean { var isPredictiveBackEnabled by prefDelegate(PreferKey.isPredictiveBackEnabled, true)
val result = if (isDark) {
!bgImageDark.isNullOrBlank() var customMode by prefDelegate<String?>(PreferKey.customMode, "tonalSpot")
} else {
!bgImageLight.isNullOrBlank() var fontScale by prefDelegate(PreferKey.fontScale, 10) {
postEvent(EventBus.RECREATE, "")
} }
Log.d( var cPrimary by prefDelegate(PreferKey.cPrimary, 0) {
"MainConfig", postEvent(EventBus.RECREATE, "")
"hasImageBg -> isDark=$isDark, " +
"bgImageDark=$bgImageDark, " +
"bgImageLight=$bgImageLight, " +
"result=$result"
)
return result
} }
/* var launcherIcon by prefDelegate(PreferKey.launcherIcon, "ic_launcher")
fun hasImageBg(isDark: Boolean): Boolean {
return if (isDark) bgImageDark.isNullOrBlank() else bgImageLight.isNullOrBlank() 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()
} }
@@ -1,42 +1,116 @@
package io.legado.app.ui.config.themeConfig 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.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize 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.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.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll 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.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.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.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.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.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.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.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.AppScaffold
import io.legado.app.ui.widget.components.GlassMediumFlexibleTopAppBar import io.legado.app.ui.widget.components.GlassMediumFlexibleTopAppBar
import io.legado.app.ui.widget.components.GlassTopAppBarDefaults import io.legado.app.ui.widget.components.GlassTopAppBarDefaults
import io.legado.app.ui.widget.components.SplicedColumnGroup import io.legado.app.ui.widget.components.SplicedColumnGroup
import io.legado.app.ui.widget.components.button.TopbarNavigationButton 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.ClickableSettingItem
import io.legado.app.ui.widget.components.settingItem.DropdownListSettingItem 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.SliderSettingItem
import io.legado.app.ui.widget.components.settingItem.SwitchSettingItem 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 import org.koin.androidx.compose.koinViewModel
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@Composable @Composable
fun ThemeConfigScreen( fun ThemeConfigScreen(
onBackClick: () -> Unit, onBackClick: () -> Unit,
viewModel: ThemeConfigViewModel = koinViewModel() viewModel: ThemeConfigViewModel = koinViewModel()
) { ) {
val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior() 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( AppScaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
@@ -57,26 +131,199 @@ fun ThemeConfigScreen(
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
.padding(16.dp) .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 { SplicedColumnGroup {
SwitchSettingItem( SwitchSettingItem(
title = stringResource(R.string.pure_black), title = stringResource(R.string.pure_black),
checked = ThemeConfig.isPureBlack, checked = ThemeConfig.isPureBlack,
onCheckedChange = { ThemeConfig.isPureBlack = it } 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( DropdownListSettingItem(
title = stringResource(R.string.palette_style), title = stringResource(R.string.palette_style),
selectedValue = ThemeConfig.paletteStyle, selectedValue = ThemeConfig.paletteStyle,
displayEntries = arrayOf( displayEntries = stringArrayResource(R.array.paletteStyle),
"柔和", "中性", "鲜艳", "表现力", "彩虹", entryValues = stringArrayResource(R.array.paletteStyle_value),
"水果拼盘", "单色", "仿真", "内容取色"
),
entryValues = arrayOf(
"tonalSpot", "neutral", "vibrant", "expressive", "rainbow",
"fruitSalad", "monochrome", "fidelity", "content"
),
onValueChange = { ThemeConfig.paletteStyle = it } 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 }
)
}
SplicedColumnGroup(title = "Compose 相关") { SplicedColumnGroup(title = "Compose 相关") {
SwitchSettingItem( SwitchSettingItem(
@@ -117,7 +364,7 @@ fun ThemeConfigScreen(
description = if (hasLightBg) stringResource(R.string.click_to_delete) else stringResource( description = if (hasLightBg) stringResource(R.string.click_to_delete) else stringResource(
R.string.select_image R.string.select_image
), ),
onClick = { manageKey = Pair(PreferKey.bgImage, false) } onClick = { manageKey = false }
) )
if (hasLightBg) { if (hasLightBg) {
@@ -141,7 +388,7 @@ fun ThemeConfigScreen(
description = if (hasDarkBg) stringResource(R.string.click_to_delete) else stringResource( description = if (hasDarkBg) stringResource(R.string.click_to_delete) else stringResource(
R.string.select_image R.string.select_image
), ),
onClick = { manageKey = Pair(PreferKey.bgImageN, true) } onClick = { manageKey = true }
) )
if (hasDarkBg) { 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( BackgroundImageManageSheet(
preferenceKey = key,
isDarkTheme = isDark, isDarkTheme = isDark,
onDismissRequest = { manageKey = null } 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
)
} }
@@ -21,7 +21,7 @@ class ThemeConfigViewModel : ViewModel() {
*/ */
suspend fun setBackgroundFromUri( suspend fun setBackgroundFromUri(
uri: Uri, uri: Uri,
preferenceKey: String isDarkTheme: Boolean
) = withContext(Dispatchers.IO) { ) = withContext(Dispatchers.IO) {
kotlin.runCatching { kotlin.runCatching {
val fileDoc = FileDoc.fromUri(uri, false) val fileDoc = FileDoc.fromUri(uri, false)
@@ -33,6 +33,7 @@ class ThemeConfigViewModel : ViewModel() {
} }
val fileName = "$md5.$suffix" val fileName = "$md5.$suffix"
val preferenceKey = if (isDarkTheme) PreferKey.bgImageN else PreferKey.bgImage
val folder = File(appCtx.externalFiles, preferenceKey) val folder = File(appCtx.externalFiles, preferenceKey)
val file = File(folder, fileName) val file = File(folder, fileName)
@@ -45,24 +46,24 @@ class ThemeConfigViewModel : ViewModel() {
} }
} }
updateBackgroundPath(preferenceKey, file.absolutePath) updateBackgroundPath(isDarkTheme, file.absolutePath)
}.onFailure { }.onFailure {
it.printStackTrace() it.printStackTrace()
} }
} }
fun removeBackground(preferenceKey: String) { fun removeBackground(isDarkTheme: Boolean) {
updateBackgroundPath(preferenceKey, null) updateBackgroundPath(isDarkTheme, null)
} }
/** /**
* 更新背景路径并清理旧文件 * 更新背景路径并清理旧文件
*/ */
private fun updateBackgroundPath(preferenceKey: String, newPath: String?) { private fun updateBackgroundPath(isDarkTheme: Boolean, newPath: String?) {
val oldPath = when (preferenceKey) { val oldPath = if (isDarkTheme) {
PreferKey.bgImage -> ThemeConfig.bgImageLight ThemeConfig.bgImageDark
PreferKey.bgImageN -> ThemeConfig.bgImageDark } else {
else -> null ThemeConfig.bgImageLight
} }
if (oldPath != newPath && oldPath != null) { if (oldPath != newPath && oldPath != null) {
@@ -72,9 +73,10 @@ class ThemeConfigViewModel : ViewModel() {
} }
} }
when (preferenceKey) { if (isDarkTheme) {
PreferKey.bgImage -> ThemeConfig.bgImageLight = newPath ThemeConfig.bgImageDark = newPath
PreferKey.bgImageN -> ThemeConfig.bgImageDark = newPath } else {
ThemeConfig.bgImageLight = newPath
} }
} }
} }
@@ -198,14 +198,24 @@ fun BookshelfItem(
@Composable @Composable
fun BookGroupCover( fun BookGroupCover(
books: List<Book>, books: List<Book>,
coverPath: String? = null,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
Box( Box(
modifier = modifier modifier = modifier
.aspectRatio(5f / 7f) .aspectRatio(5f / 7f)
.clip(RoundedCornerShape(4.dp)) .clip(RoundedCornerShape(4.dp))
.background(MaterialTheme.colorScheme.surfaceVariant) .background(MaterialTheme.colorScheme.surfaceContainer)
) { ) {
if (coverPath != null) {
BookCover(
name = "",
author = "",
path = coverPath,
modifier = Modifier.fillMaxSize(),
ignoreUseDefaultCover = true
)
} else {
Column(modifier = Modifier.fillMaxSize()) { Column(modifier = Modifier.fillMaxSize()) {
Row(modifier = Modifier.weight(1f)) { Row(modifier = Modifier.weight(1f)) {
Box( Box(
@@ -273,6 +283,7 @@ fun BookGroupCover(
} }
} }
} }
}
} }
@Composable @Composable
@@ -292,7 +303,7 @@ fun BookGroupItemGrid(
isGrid = true, isGrid = true,
gridStyle = gridStyle, gridStyle = gridStyle,
isCompact = false, isCompact = false,
cover = { BookGroupCover(books = previewBooks, modifier = it) }, cover = { BookGroupCover(books = previewBooks, coverPath = group.cover, modifier = it) },
title = group.groupName, title = group.groupName,
modifier = modifier, modifier = modifier,
titleSmallFont = titleSmallFont, titleSmallFont = titleSmallFont,
@@ -321,7 +332,7 @@ fun BookGroupItemList(
isGrid = false, isGrid = false,
gridStyle = 0, gridStyle = 0,
isCompact = isCompact, isCompact = isCompact,
cover = { BookGroupCover(books = previewBooks, modifier = it) }, cover = { BookGroupCover(books = previewBooks, coverPath = group.cover, modifier = it) },
title = group.groupName, title = group.groupName,
titleSmallFont = titleSmallFont, titleSmallFont = titleSmallFont,
titleCenter = titleCenter, titleCenter = titleCenter,
@@ -5,6 +5,7 @@ import android.content.res.Configuration
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@@ -162,6 +163,17 @@ fun BookshelfScreen(
pageCount = { uiState.groups.size } 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) { LaunchedEffect(pagerState) {
snapshotFlow { pagerState.currentPage } snapshotFlow { pagerState.currentPage }
.distinctUntilChanged() .distinctUntilChanged()
@@ -348,7 +360,11 @@ fun BookshelfScreen(
enabled = pullToRefreshEnabled enabled = pullToRefreshEnabled
) )
) { ) {
if (bookGroupStyle == 2 && isInFolderRoot) { AnimatedContent(
targetState = isInFolderRoot,
label = "FolderTransition"
) { isRoot ->
if (bookGroupStyle == 2 && isRoot) {
FastScrollLazyVerticalGrid( FastScrollLazyVerticalGrid(
columns = GridCells.Fixed(bookshelfLayoutGrid.coerceAtLeast(1)), columns = GridCells.Fixed(bookshelfLayoutGrid.coerceAtLeast(1)),
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
@@ -362,11 +378,14 @@ fun BookshelfScreen(
horizontalArrangement = Arrangement.spacedBy(if (bookshelfLayoutMode != 0) 8.dp else 0.dp), horizontalArrangement = Arrangement.spacedBy(if (bookshelfLayoutMode != 0) 8.dp else 0.dp),
showFastScroll = BookshelfConfig.showBookshelfFastScroller showFastScroll = BookshelfConfig.showBookshelfFastScroller
) { ) {
itemsIndexed(uiState.groups, key = { _, it -> it.groupId }) { index, group -> itemsIndexed(
uiState.groups,
key = { _, it -> it.groupId }) { index, group ->
if (bookshelfLayoutMode == 0) { if (bookshelfLayoutMode == 0) {
BookGroupItemList( BookGroupItemList(
group = group, group = group,
previewBooks = uiState.groupPreviews[group.groupId] ?: emptyList(), previewBooks = uiState.groupPreviews[group.groupId]
?: emptyList(),
isCompact = BookshelfConfig.bookshelfLayoutCompact, isCompact = BookshelfConfig.bookshelfLayoutCompact,
titleSmallFont = BookshelfConfig.bookshelfTitleSmallFont, titleSmallFont = BookshelfConfig.bookshelfTitleSmallFont,
titleCenter = BookshelfConfig.bookshelfTitleCenter, titleCenter = BookshelfConfig.bookshelfTitleCenter,
@@ -380,7 +399,8 @@ fun BookshelfScreen(
} else { } else {
BookGroupItemGrid( BookGroupItemGrid(
group = group, group = group,
previewBooks = uiState.groupPreviews[group.groupId] ?: emptyList(), previewBooks = uiState.groupPreviews[group.groupId]
?: emptyList(),
gridStyle = BookshelfConfig.bookshelfGridLayout, gridStyle = BookshelfConfig.bookshelfGridLayout,
titleSmallFont = BookshelfConfig.bookshelfTitleSmallFont, titleSmallFont = BookshelfConfig.bookshelfTitleSmallFont,
titleCenter = BookshelfConfig.bookshelfTitleCenter, titleCenter = BookshelfConfig.bookshelfTitleCenter,
@@ -420,6 +440,7 @@ fun BookshelfScreen(
} }
} }
} }
}
PullToRefreshDefaults.LoadingIndicator( PullToRefreshDefaults.LoadingIndicator(
state = pullToRefreshState, state = pullToRefreshState,
@@ -138,11 +138,18 @@ class BookshelfViewModel(
if (BookshelfConfig.bookGroupStyle in 2..3) { if (BookshelfConfig.bookGroupStyle in 2..3) {
groups.associate { group -> groups.associate { group ->
val groupBooks = if (group.groupId == BookGroup.IdAll) { val groupBooks = if (group.groupId == BookGroup.IdAll) {
allBooks.take(4) allBooks
} else { } 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 { } else {
emptyMap() emptyMap()
@@ -1,39 +1,91 @@
package io.legado.app.ui.widget.components 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.PaddingValues
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.WindowInsets 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.FabPosition
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.ScaffoldDefaults import androidx.compose.material3.ScaffoldDefaults
import androidx.compose.material3.TopAppBarColors
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.material3.contentColorFor import androidx.compose.material3.contentColorFor
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color 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 @Composable
fun AppScaffold( fun AppScaffold(
modifier: Modifier = Modifier, 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 = {}, topBar: @Composable () -> Unit = {},
bottomBar: @Composable () -> Unit = {}, bottomBar: @Composable () -> Unit = {},
snackbarHost: @Composable () -> Unit = {}, snackbarHost: @Composable () -> Unit = {},
floatingActionButton: @Composable () -> Unit = {}, floatingActionButton: @Composable () -> Unit = {},
floatingActionButtonPosition: FabPosition = FabPosition.End, floatingActionButtonPosition: FabPosition = FabPosition.End,
containerColor: Color = Color.Transparent,
contentColor: Color = contentColorFor(MaterialTheme.colorScheme.background), contentColor: Color = contentColorFor(MaterialTheme.colorScheme.background),
contentWindowInsets: WindowInsets = ScaffoldDefaults.contentWindowInsets, contentWindowInsets: WindowInsets = ScaffoldDefaults.contentWindowInsets,
content: @Composable (PaddingValues) -> Unit 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( Scaffold(
modifier = modifier, modifier = modifier,
topBar = topBar, topBar = finalTopBar,
bottomBar = bottomBar, bottomBar = bottomBar,
snackbarHost = snackbarHost, snackbarHost = snackbarHost,
floatingActionButton = floatingActionButton, floatingActionButton = floatingActionButton,
floatingActionButtonPosition = floatingActionButtonPosition, floatingActionButtonPosition = floatingActionButtonPosition,
containerColor = containerColor, containerColor = containerColor,
contentColor = contentColor, contentColor = contentColor,
contentWindowInsets = contentWindowInsets, contentWindowInsets = contentWindowInsets
content = content ) { paddingValues ->
) Box(
modifier = Modifier
.fillMaxSize()
.responsiveHazeSource(hazeState)
) {
content(paddingValues)
}
}
} }
@@ -75,10 +75,7 @@ object GlassTopAppBarDefaults {
return TopAppBarDefaults.topAppBarColors( return TopAppBarDefaults.topAppBarColors(
containerColor = containerColor, containerColor = containerColor,
scrolledContainerColor = scrolledContainerColor, scrolledContainerColor = scrolledContainerColor
navigationIconContentColor = MaterialTheme.colorScheme.onSurface,
titleContentColor = MaterialTheme.colorScheme.onSurface,
actionIconContentColor = MaterialTheme.colorScheme.onSurfaceVariant
) )
} }
@@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
@@ -14,7 +15,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp 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 * Settings Group Container by https://github.com/wxxsfxyzm/InstallerX-Revived
@@ -29,17 +29,17 @@ fun SplicedColumnGroup(
title: String = "", title: String = "",
content: @Composable ColumnScope.() -> Unit, 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()) { if (title.isNotEmpty()) {
Text( Text(
text = title, text = title,
style = MaterialTheme.typography.labelLarge, style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.primary, 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), shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = Color.Transparent), colors = CardDefaults.cardColors(containerColor = Color.Transparent),
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
@@ -26,11 +26,18 @@ fun GlassCard(
) { ) {
val containerAlpha = ThemeConfig.containerOpacity / 100f val containerAlpha = ThemeConfig.containerOpacity / 100f
val finalColors = colors ?: CardDefaults.cardColors( val baseColors = colors ?: CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = containerAlpha), containerColor = MaterialTheme.colorScheme.secondaryContainer,
contentColor = MaterialTheme.colorScheme.onSecondaryContainer 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) { if (onClick != null) {
Card( Card(
onClick = onClick, onClick = onClick,
@@ -54,17 +54,25 @@ fun BookCover(
badgeText: String? = null, badgeText: String? = null,
showBadgeDot: Boolean = false, showBadgeDot: Boolean = false,
sourceOrigin: String? = null, sourceOrigin: String? = null,
onLoadFinish: (() -> Unit)? = null onLoadFinish: (() -> Unit)? = null,
ignoreUseDefaultCover: Boolean = false
) { ) {
val context = LocalContext.current val context = LocalContext.current
val isNight = isSystemInDarkTheme() val isNight = isSystemInDarkTheme()
val useDefault = CoverConfig.useDefaultCover val useDefault = !ignoreUseDefaultCover && CoverConfig.useDefaultCover
val finalPath = if (useDefault) null else path val finalPath = if (useDefault) null else path
// 获取随机封面路径(字符串),而非直接获取 Drawable,避免主线程解码大图 // 获取随机封面路径(字符串),增加配置项作为 Key,确保设置更改后立即刷新
val randomPath = remember(name, author, path, isNight) { val randomPath = remember(
BookCover.getRandomDefaultPath(seed = name ?: author ?: path ?: "") name, author, path, isNight,
CoverConfig.defaultCover,
CoverConfig.defaultCoverDark
) {
BookCover.getRandomDefaultPath(
seed = name ?: author ?: path ?: "",
isNight = isNight
)
} }
// 检查是否有自定义随机封面设置 // 检查是否有自定义随机封面设置
@@ -95,7 +95,7 @@ fun ColorPickerSheet(
HsvColorPicker( HsvColorPicker(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(250.dp) .height(240.dp)
.padding(10.dp), .padding(10.dp),
controller = controller, controller = controller,
initialColor = Color(initialColor), initialColor = Color(initialColor),
@@ -122,34 +122,6 @@ fun ColorPickerSheet(
controller = controller, 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( LazyVerticalGrid(
columns = GridCells.Adaptive(36.dp), columns = GridCells.Adaptive(36.dp),
verticalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp),
@@ -176,7 +148,30 @@ fun ColorPickerSheet(
} }
} }
Spacer(modifier = Modifier.height(24.dp)) 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
)
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@@ -195,4 +190,5 @@ fun ColorPickerSheet(
} }
} }
} }
}
} }
@@ -145,7 +145,6 @@ fun <T> BatchImportDialog(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.heightIn(max = LocalConfiguration.current.screenHeightDp.dp * 0.8f), .heightIn(max = LocalConfiguration.current.screenHeightDp.dp * 0.8f),
containerColor = Color.Transparent,
topBar = { topBar = {
CenterAlignedTopAppBar( CenterAlignedTopAppBar(
title = { title = {
@@ -35,9 +35,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex 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.AppScaffold
import io.legado.app.ui.widget.components.GlassTopAppBarDefaults import io.legado.app.ui.widget.components.GlassTopAppBarDefaults
import io.legado.app.ui.widget.components.SelectionActions import io.legado.app.ui.widget.components.SelectionActions
@@ -84,7 +81,6 @@ fun <T> ListScaffold(
content: @Composable (PaddingValues) -> Unit content: @Composable (PaddingValues) -> Unit
) { ) {
val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior() val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
val hazeState = remember { HazeState() }
AppScaffold( AppScaffold(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
@@ -96,7 +92,6 @@ fun <T> ListScaffold(
) )
}, },
topBar = { topBar = {
Box(modifier = Modifier.responsiveHazeEffect(state = hazeState)) {
DynamicTopAppBar( DynamicTopAppBar(
title = title, title = title,
subtitle = subtitle, subtitle = subtitle,
@@ -111,14 +106,11 @@ fun <T> ListScaffold(
dropDownMenuContent = dropDownMenuContent, dropDownMenuContent = dropDownMenuContent,
bottomContent = bottomContent bottomContent = bottomContent
) )
}
}, },
floatingActionButton = floatingActionButton floatingActionButton = floatingActionButton
) { paddingValues -> ) { paddingValues ->
Box( Box(
modifier = Modifier modifier = Modifier.fillMaxSize()
.fillMaxSize()
.responsiveHazeSource(hazeState)
) { ) {
content(paddingValues) content(paddingValues)
@@ -50,9 +50,8 @@ fun SliderSettingItem(
SettingItem( SettingItem(
title = title, title = title,
description = description,
color = color ?: MaterialTheme.colorScheme.surfaceContainerLow, color = color ?: MaterialTheme.colorScheme.surfaceContainerLow,
option = value.toInt().toString(), option = description,
expanded = expanded, expanded = expanded,
onExpandChange = { expanded = it }, onExpandChange = { expanded = it },
expandContent = { expandContent = {
@@ -23,6 +23,7 @@ import io.legado.app.help.config.AppConfig
import io.legado.app.help.glide.ImageLoader import io.legado.app.help.glide.ImageLoader
import io.legado.app.help.glide.OkHttpModelLoader import io.legado.app.help.glide.OkHttpModelLoader
import io.legado.app.model.BookCover 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.spToPx
import io.legado.app.utils.textHeight import io.legado.app.utils.textHeight
import io.legado.app.utils.themeColor import io.legado.app.utils.themeColor
@@ -46,9 +47,9 @@ class CoverImageView @JvmOverloads constructor(
private var author: String? = null private var author: String? = null
private var nameHeight = 0f private var nameHeight = 0f
private var authorHeight = 0f private var authorHeight = 0f
private val isNightTheme = AppConfig.isNightTheme private val isNightTheme get() = AppConfig.isNightTheme
val colorKey = if (isNightTheme) BookCover.coverTextColorN else BookCover.coverTextColor private val colorKey get() = if (isNightTheme) CoverConfig.coverTextColorN else CoverConfig.coverTextColor
val shadowKey = if (isNightTheme) BookCover.coverShadowColorN else BookCover.coverShadowColor private val shadowKey get() = if (isNightTheme) CoverConfig.coverShadowColorN else CoverConfig.coverShadowColor
private val namePaint by lazy { private val namePaint by lazy {
val textPaint = TextPaint() val textPaint = TextPaint()
textPaint.typeface = Typeface.DEFAULT_BOLD textPaint.typeface = Typeface.DEFAULT_BOLD
@@ -123,13 +124,14 @@ class CoverImageView @JvmOverloads constructor(
private fun drawNameAuthor(canvas: Canvas) { private fun drawNameAuthor(canvas: Canvas) {
var startX = width * 0.2f var startX = width * 0.2f
var startY = viewHeight * 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 -> name?.toStringArray()?.let { nameChars ->
val textSize = viewWidth / 8 val textSize = viewWidth / 8
namePaint.textSize = textSize namePaint.textSize = textSize
namePaint.strokeWidth = textSize / 8 namePaint.strokeWidth = textSize / 8
if (BookCover.drawBookShadow) { if (CoverConfig.coverShowShadow) {
namePaint.setShadowLayer( namePaint.setShadowLayer(
4f, 4f,
0f, 0f,
@@ -141,12 +143,12 @@ class CoverImageView @JvmOverloads constructor(
} }
nameChars.forEach { char -> nameChars.forEach { char ->
if (BookCover.drawBookStroke) { if (CoverConfig.coverShowStroke) {
namePaint.color = Color.WHITE namePaint.color = Color.WHITE
namePaint.style = Paint.Style.STROKE namePaint.style = Paint.Style.STROKE
canvas.drawText(char, startX, startY, namePaint) canvas.drawText(char, startX, startY, namePaint)
} }
if (BookCover.coverDefaultColor) if (CoverConfig.coverDefaultColor)
namePaint.color = context.themeColor(com.google.android.material.R.attr.colorSecondary) namePaint.color = context.themeColor(com.google.android.material.R.attr.colorSecondary)
else else
namePaint.color = colorKey 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 -> author?.toStringArray()?.let { author ->
authorPaint.textSize = viewWidth / 10 authorPaint.textSize = viewWidth / 10
authorPaint.strokeWidth = authorPaint.textSize / 5 authorPaint.strokeWidth = authorPaint.textSize / 5
startX = width * 0.8f startX = width * 0.8f
startY = viewHeight * 0.95f - author.size * authorPaint.textHeight startY = viewHeight * 0.95f - author.size * authorPaint.textHeight
startY = maxOf(startY, viewHeight * 0.2f) startY = maxOf(startY, viewHeight * 0.2f)
if (BookCover.drawBookShadow) { if (CoverConfig.coverShowShadow) {
authorPaint.setShadowLayer( authorPaint.setShadowLayer(
4f, 4f,
0f, 0f,
@@ -183,12 +187,12 @@ class CoverImageView @JvmOverloads constructor(
} }
author.forEach { author.forEach {
if (BookCover.drawBookStroke) { if (CoverConfig.coverShowStroke) {
authorPaint.color = Color.WHITE authorPaint.color = Color.WHITE
authorPaint.style = Paint.Style.STROKE authorPaint.style = Paint.Style.STROKE
canvas.drawText(it, startX, startY, authorPaint) canvas.drawText(it, startX, startY, authorPaint)
} }
if (BookCover.coverDefaultColor) if (CoverConfig.coverDefaultColor)
authorPaint.color = context.themeColor(com.google.android.material.R.attr.colorSecondary) authorPaint.color = context.themeColor(com.google.android.material.R.attr.colorSecondary)
else else
authorPaint.color = colorKey authorPaint.color = colorKey