[优化] 新增了新的外观设置、备份与恢复设置,现在可以在高级、测试处体验。
[修复] 封面状态不同步的问题、文件夹封面和书籍封面获取的问题、容器透明度不生效的问题等
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -74,7 +74,7 @@ fun <T> prefDelegate(
|
||||
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
|
||||
if (_value.value != value) {
|
||||
when (value) {
|
||||
is String -> appCtx.putPrefString(key, value)
|
||||
is String? -> appCtx.putPrefString(key, value)
|
||||
is Int -> appCtx.putPrefInt(key, value)
|
||||
is Boolean -> appCtx.putPrefBoolean(key, value)
|
||||
is Long -> appCtx.putPrefLong(key, value)
|
||||
|
||||
@@ -17,6 +17,7 @@ import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import io.legado.app.R
|
||||
import io.legado.app.base.BaseComposeActivity
|
||||
import io.legado.app.ui.config.backupConfig.BackupConfigScreen
|
||||
import io.legado.app.ui.config.coverConfig.CoverConfigScreen
|
||||
import io.legado.app.ui.config.otherConfig.OtherConfigScreen
|
||||
import io.legado.app.ui.config.readConfig.ReadConfigScreen
|
||||
@@ -46,6 +47,9 @@ class TestConfigActivity : BaseComposeActivity() {
|
||||
@Serializable
|
||||
object ThemeConfigRoute
|
||||
|
||||
@Serializable
|
||||
object BackupConfigRoute
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val navController = rememberNavController()
|
||||
@@ -60,7 +64,8 @@ class TestConfigActivity : BaseComposeActivity() {
|
||||
onNavigateToOther = { navController.navigate(OtherConfigRoute) },
|
||||
onNavigateToRead = { navController.navigate(ReadConfigRoute) },
|
||||
onNavigateToCover = { navController.navigate(CoverConfigRoute) },
|
||||
onNavigateToTheme = { navController.navigate(ThemeConfigRoute) }
|
||||
onNavigateToTheme = { navController.navigate(ThemeConfigRoute) },
|
||||
onNavigateToBackup = { navController.navigate(BackupConfigRoute) }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -77,7 +82,13 @@ class TestConfigActivity : BaseComposeActivity() {
|
||||
}
|
||||
|
||||
composable<ThemeConfigRoute> {
|
||||
ThemeConfigScreen(onBackClick = { navController.popBackStack() })
|
||||
ThemeConfigScreen(
|
||||
onBackClick = { navController.popBackStack() }
|
||||
)
|
||||
}
|
||||
|
||||
composable<BackupConfigRoute> {
|
||||
BackupConfigScreen(onBackClick = { navController.popBackStack() })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -89,7 +100,8 @@ class TestConfigActivity : BaseComposeActivity() {
|
||||
onNavigateToOther: () -> Unit,
|
||||
onNavigateToRead: () -> Unit,
|
||||
onNavigateToCover: () -> Unit,
|
||||
onNavigateToTheme: () -> Unit
|
||||
onNavigateToTheme: () -> Unit,
|
||||
onNavigateToBackup: () -> Unit
|
||||
) {
|
||||
val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
|
||||
|
||||
@@ -129,6 +141,10 @@ class TestConfigActivity : BaseComposeActivity() {
|
||||
title = stringResource(R.string.cover_config),
|
||||
onClick = onNavigateToCover
|
||||
)
|
||||
ClickableSettingItem(
|
||||
title = stringResource(R.string.backup_restore),
|
||||
onClick = onNavigateToBackup
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
@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)
|
||||
|
||||
@@ -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
|
||||
|
||||
import android.util.Log
|
||||
import io.legado.app.constant.EventBus
|
||||
import io.legado.app.constant.PreferKey
|
||||
import io.legado.app.ui.config.prefDelegate
|
||||
@@ -20,6 +19,8 @@ object ThemeConfig {
|
||||
|
||||
var appTheme by prefDelegate(PreferKey.appTheme, "0")
|
||||
|
||||
var themeMode by prefDelegate(PreferKey.themeMode, "0")
|
||||
|
||||
var isPureBlack by prefDelegate(PreferKey.pureBlack, false)
|
||||
|
||||
var bgImageLight by prefDelegate<String?>(PreferKey.bgImage, null) {
|
||||
@@ -34,28 +35,37 @@ object ThemeConfig {
|
||||
|
||||
var bgImageNBlurring by prefDelegate(PreferKey.bgImageNBlurring, 0)
|
||||
|
||||
fun hasImageBg(isDark: Boolean): Boolean {
|
||||
val result = if (isDark) {
|
||||
!bgImageDark.isNullOrBlank()
|
||||
} else {
|
||||
!bgImageLight.isNullOrBlank()
|
||||
}
|
||||
var isPredictiveBackEnabled by prefDelegate(PreferKey.isPredictiveBackEnabled, true)
|
||||
|
||||
Log.d(
|
||||
"MainConfig",
|
||||
"hasImageBg -> isDark=$isDark, " +
|
||||
"bgImageDark=$bgImageDark, " +
|
||||
"bgImageLight=$bgImageLight, " +
|
||||
"result=$result"
|
||||
)
|
||||
var customMode by prefDelegate<String?>(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()
|
||||
|
||||
}
|
||||
@@ -1,42 +1,116 @@
|
||||
package io.legado.app.ui.config.themeConfig
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.BrightnessMedium
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.DarkMode
|
||||
import androidx.compose.material.icons.filled.LightMode
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.ButtonGroupDefaults
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.ToggleButton
|
||||
import androidx.compose.material3.ToggleButtonDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringArrayResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.semantics.role
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.constraintlayout.compose.ConstraintLayout
|
||||
import com.google.android.material.color.DynamicColors
|
||||
import com.google.android.material.color.DynamicColorsOptions
|
||||
import io.legado.app.R
|
||||
import io.legado.app.constant.PreferKey
|
||||
import io.legado.app.base.AppContextWrapper
|
||||
import io.legado.app.constant.EventBus
|
||||
import io.legado.app.help.LauncherIconHelp
|
||||
import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.help.config.OldThemeConfig
|
||||
import io.legado.app.lib.theme.ThemeStore
|
||||
import io.legado.app.lib.theme.primaryColor
|
||||
import io.legado.app.ui.theme.ThemeManager
|
||||
import io.legado.app.ui.theme.ThemeResolver
|
||||
import io.legado.app.ui.widget.components.AppScaffold
|
||||
import io.legado.app.ui.widget.components.GlassMediumFlexibleTopAppBar
|
||||
import io.legado.app.ui.widget.components.GlassTopAppBarDefaults
|
||||
import io.legado.app.ui.widget.components.SplicedColumnGroup
|
||||
import io.legado.app.ui.widget.components.button.TopbarNavigationButton
|
||||
import io.legado.app.ui.widget.components.dialog.ColorPickerSheet
|
||||
import io.legado.app.ui.widget.components.settingItem.ClickableSettingItem
|
||||
import io.legado.app.ui.widget.components.settingItem.DropdownListSettingItem
|
||||
import io.legado.app.ui.widget.components.settingItem.SliderSettingItem
|
||||
import io.legado.app.ui.widget.components.settingItem.SwitchSettingItem
|
||||
import io.legado.app.utils.postEvent
|
||||
import io.legado.app.utils.restart
|
||||
import io.legado.app.utils.toastOnUi
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun ThemeConfigScreen(
|
||||
onBackClick: () -> Unit,
|
||||
viewModel: ThemeConfigViewModel = koinViewModel()
|
||||
) {
|
||||
val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
|
||||
var manageKey by remember { mutableStateOf<Pair<String, Boolean>?>(null) }
|
||||
var manageKey by remember { mutableStateOf<Boolean?>(null) }
|
||||
val context = LocalContext.current
|
||||
|
||||
var selectedThemeMode by remember { mutableStateOf(ThemeConfig.themeMode) }
|
||||
var selectedTheme by remember { mutableStateOf(ThemeConfig.appTheme) }
|
||||
var showRestartDialog by remember { mutableStateOf(false) }
|
||||
var showColorPicker by remember { mutableStateOf(false) }
|
||||
var showLauncherIconPicker by remember { mutableStateOf(false) }
|
||||
|
||||
val fontScaleValue = remember { mutableFloatStateOf(ThemeConfig.fontScale.toFloat()) }
|
||||
val primaryColorValue = remember { mutableIntStateOf(ThemeConfig.cPrimary) }
|
||||
|
||||
AppScaffold(
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
@@ -57,24 +131,197 @@ fun ThemeConfigScreen(
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(16.dp)
|
||||
) {
|
||||
val isDarkTheme = when (selectedThemeMode) {
|
||||
"1" -> false
|
||||
"2" -> true
|
||||
else -> isSystemInDarkTheme()
|
||||
}
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
ThemeCard(
|
||||
context = context,
|
||||
value = selectedTheme,
|
||||
isDark = isDarkTheme,
|
||||
isAmoled = ThemeConfig.isPureBlack,
|
||||
paletteStyle = ThemeConfig.paletteStyle
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
val themeItems = stringArrayResource(R.array.themes_item)
|
||||
val themeValues = stringArrayResource(R.array.themes_value)
|
||||
val themes = remember(themeItems, themeValues) {
|
||||
themeItems.zip(themeValues).toList()
|
||||
}
|
||||
|
||||
SplicedColumnGroup(title = stringResource(R.string.theme)) {
|
||||
ThemeModeSelector(
|
||||
selectedMode = selectedThemeMode,
|
||||
onModeSelected = { mode ->
|
||||
selectedThemeMode = mode
|
||||
ThemeConfig.themeMode = mode
|
||||
OldThemeConfig.applyDayNight(context)
|
||||
}
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
ThemeColorSelector(
|
||||
context = context,
|
||||
themes = themes,
|
||||
selectedTheme = selectedTheme,
|
||||
isDark = isDarkTheme,
|
||||
isAmoled = ThemeConfig.isPureBlack,
|
||||
paletteStyle = ThemeConfig.paletteStyle,
|
||||
onThemeSelected = { theme ->
|
||||
if (theme == "13") {
|
||||
val hasLightBg = !ThemeConfig.bgImageLight.isNullOrEmpty()
|
||||
val hasDarkBg = !ThemeConfig.bgImageDark.isNullOrEmpty()
|
||||
if (!hasLightBg || !hasDarkBg) {
|
||||
context.toastOnUi(R.string.transparent_theme_alarm)
|
||||
return@ThemeColorSelector
|
||||
} else {
|
||||
AppConfig.containerOpacity = 0
|
||||
}
|
||||
}
|
||||
val oldTheme = selectedTheme
|
||||
selectedTheme = theme
|
||||
ThemeConfig.appTheme = theme
|
||||
val isDynamicSwitch = (oldTheme == "12" || theme == "12")
|
||||
if (isDynamicSwitch) {
|
||||
showRestartDialog = true
|
||||
} else {
|
||||
postEvent(EventBus.RECREATE, "")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
SplicedColumnGroup {
|
||||
SwitchSettingItem(
|
||||
title = stringResource(R.string.pure_black),
|
||||
checked = ThemeConfig.isPureBlack,
|
||||
onCheckedChange = { ThemeConfig.isPureBlack = it }
|
||||
)
|
||||
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<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(
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -198,76 +198,87 @@ fun BookshelfItem(
|
||||
@Composable
|
||||
fun BookGroupCover(
|
||||
books: List<Book>,
|
||||
coverPath: String? = null,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.aspectRatio(5f / 7f)
|
||||
.clip(RoundedCornerShape(4.dp))
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant)
|
||||
.background(MaterialTheme.colorScheme.surfaceContainer)
|
||||
) {
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
Row(modifier = Modifier.weight(1f)) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxHeight()
|
||||
.padding(1.dp)
|
||||
) {
|
||||
books.getOrNull(0)?.let {
|
||||
BookCover(
|
||||
name = it.name,
|
||||
author = it.author,
|
||||
path = it.getDisplayCover(),
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
if (coverPath != null) {
|
||||
BookCover(
|
||||
name = "",
|
||||
author = "",
|
||||
path = coverPath,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
ignoreUseDefaultCover = true
|
||||
)
|
||||
} else {
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
Row(modifier = Modifier.weight(1f)) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxHeight()
|
||||
.padding(1.dp)
|
||||
) {
|
||||
books.getOrNull(0)?.let {
|
||||
BookCover(
|
||||
name = it.name,
|
||||
author = it.author,
|
||||
path = it.getDisplayCover(),
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxHeight()
|
||||
.padding(1.dp)
|
||||
) {
|
||||
books.getOrNull(1)?.let {
|
||||
BookCover(
|
||||
name = it.name,
|
||||
author = it.author,
|
||||
path = it.getDisplayCover(),
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxHeight()
|
||||
.padding(1.dp)
|
||||
) {
|
||||
books.getOrNull(1)?.let {
|
||||
BookCover(
|
||||
name = it.name,
|
||||
author = it.author,
|
||||
path = it.getDisplayCover(),
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
Row(modifier = Modifier.weight(1f)) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxHeight()
|
||||
.padding(1.dp)
|
||||
) {
|
||||
books.getOrNull(2)?.let {
|
||||
BookCover(
|
||||
name = it.name,
|
||||
author = it.author,
|
||||
path = it.getDisplayCover(),
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Row(modifier = Modifier.weight(1f)) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxHeight()
|
||||
.padding(1.dp)
|
||||
) {
|
||||
books.getOrNull(2)?.let {
|
||||
BookCover(
|
||||
name = it.name,
|
||||
author = it.author,
|
||||
path = it.getDisplayCover(),
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxHeight()
|
||||
.padding(1.dp)
|
||||
) {
|
||||
books.getOrNull(3)?.let {
|
||||
BookCover(
|
||||
name = it.name,
|
||||
author = it.author,
|
||||
path = it.getDisplayCover(),
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.fillMaxHeight()
|
||||
.padding(1.dp)
|
||||
) {
|
||||
books.getOrNull(3)?.let {
|
||||
BookCover(
|
||||
name = it.name,
|
||||
author = it.author,
|
||||
path = it.getDisplayCover(),
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -292,7 +303,7 @@ fun BookGroupItemGrid(
|
||||
isGrid = true,
|
||||
gridStyle = gridStyle,
|
||||
isCompact = false,
|
||||
cover = { BookGroupCover(books = previewBooks, modifier = it) },
|
||||
cover = { BookGroupCover(books = previewBooks, coverPath = group.cover, modifier = it) },
|
||||
title = group.groupName,
|
||||
modifier = modifier,
|
||||
titleSmallFont = titleSmallFont,
|
||||
@@ -321,7 +332,7 @@ fun BookGroupItemList(
|
||||
isGrid = false,
|
||||
gridStyle = 0,
|
||||
isCompact = isCompact,
|
||||
cover = { BookGroupCover(books = previewBooks, modifier = it) },
|
||||
cover = { BookGroupCover(books = previewBooks, coverPath = group.cover, modifier = it) },
|
||||
title = group.groupName,
|
||||
titleSmallFont = titleSmallFont,
|
||||
titleCenter = titleCenter,
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -1,39 +1,91 @@
|
||||
package io.legado.app.ui.widget.components
|
||||
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FabPosition
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.ScaffoldDefaults
|
||||
import androidx.compose.material3.TopAppBarColors
|
||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
import androidx.compose.material3.contentColorFor
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import dev.chrisbanes.haze.HazeState
|
||||
import io.legado.app.ui.config.themeConfig.ThemeConfig
|
||||
import io.legado.app.ui.theme.responsiveHazeEffect
|
||||
import io.legado.app.ui.theme.responsiveHazeSource
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun AppScaffold(
|
||||
modifier: Modifier = Modifier,
|
||||
title: (@Composable () -> Unit)? = null,
|
||||
subtitle: (@Composable () -> Unit)? = null,
|
||||
scrollBehavior: TopAppBarScrollBehavior? = null,
|
||||
navigationIcon: @Composable () -> Unit = {},
|
||||
actions: @Composable RowScope.() -> Unit = {},
|
||||
colors: TopAppBarColors = GlassTopAppBarDefaults.glassColors(),
|
||||
topBar: @Composable () -> Unit = {},
|
||||
bottomBar: @Composable () -> Unit = {},
|
||||
snackbarHost: @Composable () -> Unit = {},
|
||||
floatingActionButton: @Composable () -> Unit = {},
|
||||
floatingActionButtonPosition: FabPosition = FabPosition.End,
|
||||
containerColor: Color = Color.Transparent,
|
||||
contentColor: Color = contentColorFor(MaterialTheme.colorScheme.background),
|
||||
contentWindowInsets: WindowInsets = ScaffoldDefaults.contentWindowInsets,
|
||||
content: @Composable (PaddingValues) -> Unit
|
||||
) {
|
||||
val isDark = isSystemInDarkTheme()
|
||||
val hasImageBg = ThemeConfig.hasImageBg(isDark)
|
||||
val hazeState = remember { HazeState() }
|
||||
|
||||
val containerColor = if (hasImageBg) {
|
||||
Color.Transparent
|
||||
} else {
|
||||
MaterialTheme.colorScheme.background
|
||||
}
|
||||
|
||||
val finalTopBar = @Composable {
|
||||
Box(modifier = Modifier.responsiveHazeEffect(state = hazeState)) {
|
||||
if (title != null) {
|
||||
GlassMediumFlexibleTopAppBar(
|
||||
title = title,
|
||||
subtitle = subtitle,
|
||||
scrollBehavior = scrollBehavior,
|
||||
navigationIcon = navigationIcon,
|
||||
actions = actions,
|
||||
colors = colors
|
||||
)
|
||||
} else {
|
||||
topBar()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
topBar = topBar,
|
||||
topBar = finalTopBar,
|
||||
bottomBar = bottomBar,
|
||||
snackbarHost = snackbarHost,
|
||||
floatingActionButton = floatingActionButton,
|
||||
floatingActionButtonPosition = floatingActionButtonPosition,
|
||||
containerColor = containerColor,
|
||||
contentColor = contentColor,
|
||||
contentWindowInsets = contentWindowInsets,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
contentWindowInsets = contentWindowInsets
|
||||
) { paddingValues ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.responsiveHazeSource(hazeState)
|
||||
) {
|
||||
content(paddingValues)
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-4
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
// 检查是否有自定义随机封面设置
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-1
@@ -145,7 +145,6 @@ fun <T> BatchImportDialog(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(max = LocalConfiguration.current.screenHeightDp.dp * 0.8f),
|
||||
containerColor = Color.Transparent,
|
||||
topBar = {
|
||||
CenterAlignedTopAppBar(
|
||||
title = {
|
||||
|
||||
@@ -35,9 +35,6 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.zIndex
|
||||
import dev.chrisbanes.haze.HazeState
|
||||
import io.legado.app.ui.theme.responsiveHazeEffect
|
||||
import io.legado.app.ui.theme.responsiveHazeSource
|
||||
import io.legado.app.ui.widget.components.AppScaffold
|
||||
import io.legado.app.ui.widget.components.GlassTopAppBarDefaults
|
||||
import io.legado.app.ui.widget.components.SelectionActions
|
||||
@@ -84,7 +81,6 @@ fun <T> ListScaffold(
|
||||
content: @Composable (PaddingValues) -> Unit
|
||||
) {
|
||||
val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
|
||||
val hazeState = remember { HazeState() }
|
||||
|
||||
AppScaffold(
|
||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
@@ -96,29 +92,25 @@ fun <T> ListScaffold(
|
||||
)
|
||||
},
|
||||
topBar = {
|
||||
Box(modifier = Modifier.responsiveHazeEffect(state = hazeState)) {
|
||||
DynamicTopAppBar(
|
||||
title = title,
|
||||
subtitle = subtitle,
|
||||
state = state,
|
||||
scrollBehavior = scrollBehavior,
|
||||
onBackClick = onBackClick,
|
||||
onSearchToggle = onSearchToggle,
|
||||
onSearchQueryChange = onSearchQueryChange,
|
||||
searchPlaceholder = searchPlaceholder,
|
||||
onClearSelection = { selectionActions?.onSelectInvert?.invoke() },
|
||||
topBarActions = topBarActions,
|
||||
dropDownMenuContent = dropDownMenuContent,
|
||||
bottomContent = bottomContent
|
||||
)
|
||||
}
|
||||
DynamicTopAppBar(
|
||||
title = title,
|
||||
subtitle = subtitle,
|
||||
state = state,
|
||||
scrollBehavior = scrollBehavior,
|
||||
onBackClick = onBackClick,
|
||||
onSearchToggle = onSearchToggle,
|
||||
onSearchQueryChange = onSearchQueryChange,
|
||||
searchPlaceholder = searchPlaceholder,
|
||||
onClearSelection = { selectionActions?.onSelectInvert?.invoke() },
|
||||
topBarActions = topBarActions,
|
||||
dropDownMenuContent = dropDownMenuContent,
|
||||
bottomContent = bottomContent
|
||||
)
|
||||
},
|
||||
floatingActionButton = floatingActionButton
|
||||
) { paddingValues ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.responsiveHazeSource(hazeState)
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
content(paddingValues)
|
||||
|
||||
|
||||
+1
-2
@@ -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 = {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user