[优化] 新增了新的外观设置、备份与恢复设置,现在可以在高级、测试处体验。
[修复] 封面状态不同步的问题、文件夹封面和书籍封面获取的问题、容器透明度不生效的问题等
This commit is contained in:
25 files changed
+1848
-196
No files matched your search
@@ -28,6 +28,7 @@ import io.legado.app.ui.book.readRecord.ReadRecordViewModel
|
|||||||
import io.legado.app.ui.book.searchContent.SearchContentViewModel
|
import io.legado.app.ui.book.searchContent.SearchContentViewModel
|
||||||
import io.legado.app.ui.book.toc.TocViewModel
|
import io.legado.app.ui.book.toc.TocViewModel
|
||||||
import io.legado.app.ui.book.toc.rule.TxtTocRuleViewModel
|
import io.legado.app.ui.book.toc.rule.TxtTocRuleViewModel
|
||||||
|
import io.legado.app.ui.config.backupConfig.BackupConfigViewModel
|
||||||
import io.legado.app.ui.config.coverConfig.CoverConfigViewModel
|
import io.legado.app.ui.config.coverConfig.CoverConfigViewModel
|
||||||
import io.legado.app.ui.config.otherConfig.OtherConfigViewModel
|
import io.legado.app.ui.config.otherConfig.OtherConfigViewModel
|
||||||
import io.legado.app.ui.config.readConfig.ReadConfigViewModel
|
import io.legado.app.ui.config.readConfig.ReadConfigViewModel
|
||||||
@@ -94,6 +95,7 @@ val appModule = module {
|
|||||||
viewModelOf(::ReadConfigViewModel)
|
viewModelOf(::ReadConfigViewModel)
|
||||||
viewModelOf(::CoverConfigViewModel)
|
viewModelOf(::CoverConfigViewModel)
|
||||||
viewModelOf(::ThemeConfigViewModel)
|
viewModelOf(::ThemeConfigViewModel)
|
||||||
|
viewModelOf(::BackupConfigViewModel)
|
||||||
viewModelOf(::TocViewModel)
|
viewModelOf(::TocViewModel)
|
||||||
viewModelOf(::RemoteBookViewModel)
|
viewModelOf(::RemoteBookViewModel)
|
||||||
viewModelOf(::BookInfoViewModel)
|
viewModelOf(::BookInfoViewModel)
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package io.legado.app.model
|
|||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.Color
|
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import androidx.annotation.Keep
|
import androidx.annotation.Keep
|
||||||
import androidx.core.graphics.drawable.toDrawable
|
import androidx.core.graphics.drawable.toDrawable
|
||||||
@@ -35,8 +34,6 @@ import io.legado.app.model.analyzeRule.AnalyzeUrl
|
|||||||
import io.legado.app.utils.BitmapUtils
|
import io.legado.app.utils.BitmapUtils
|
||||||
import io.legado.app.utils.GSON
|
import io.legado.app.utils.GSON
|
||||||
import io.legado.app.utils.fromJsonObject
|
import io.legado.app.utils.fromJsonObject
|
||||||
import io.legado.app.utils.getPrefBoolean
|
|
||||||
import io.legado.app.utils.getPrefInt
|
|
||||||
import io.legado.app.utils.getPrefString
|
import io.legado.app.utils.getPrefString
|
||||||
import kotlinx.coroutines.currentCoroutineContext
|
import kotlinx.coroutines.currentCoroutineContext
|
||||||
import splitties.init.appCtx
|
import splitties.init.appCtx
|
||||||
@@ -48,76 +45,32 @@ object BookCover {
|
|||||||
|
|
||||||
private const val coverRuleConfigKey = "legadoCoverRuleConfig"
|
private const val coverRuleConfigKey = "legadoCoverRuleConfig"
|
||||||
const val configFileName = "coverRule.json"
|
const val configFileName = "coverRule.json"
|
||||||
var drawBookName = true
|
|
||||||
private set
|
|
||||||
var drawBookAuthor = true
|
|
||||||
private set
|
|
||||||
var drawBookShadow = true
|
|
||||||
private set
|
|
||||||
var drawBookStroke = true
|
|
||||||
private set
|
|
||||||
var coverTextColor: Int = Color.WHITE
|
|
||||||
private set
|
|
||||||
var coverShadowColor: Int = Color.WHITE
|
|
||||||
private set
|
|
||||||
var coverTextColorN: Int = Color.WHITE
|
|
||||||
private set
|
|
||||||
var coverShadowColorN: Int = Color.WHITE
|
|
||||||
private set
|
|
||||||
var coverDefaultColor = true
|
|
||||||
private set
|
|
||||||
lateinit var defaultDrawable: Drawable
|
|
||||||
private set
|
|
||||||
|
|
||||||
init {
|
|
||||||
upDefaultCover()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
val defaultDrawable: Drawable
|
||||||
@SuppressLint("UseCompatLoadingForDrawables")
|
@SuppressLint("UseCompatLoadingForDrawables")
|
||||||
fun upDefaultCover() {
|
get() {
|
||||||
val isNightTheme = AppConfig.isNightTheme
|
val isNightTheme = AppConfig.isNightTheme
|
||||||
drawBookName = if (isNightTheme) {
|
|
||||||
appCtx.getPrefBoolean(PreferKey.coverShowNameN, true)
|
|
||||||
} else {
|
|
||||||
appCtx.getPrefBoolean(PreferKey.coverShowName, true)
|
|
||||||
}
|
|
||||||
drawBookAuthor = if (isNightTheme) {
|
|
||||||
appCtx.getPrefBoolean(PreferKey.coverShowAuthorN, true)
|
|
||||||
} else {
|
|
||||||
appCtx.getPrefBoolean(PreferKey.coverShowAuthor, true)
|
|
||||||
}
|
|
||||||
drawBookShadow =
|
|
||||||
appCtx.getPrefBoolean(PreferKey.coverShowShadow, false)
|
|
||||||
drawBookStroke =
|
|
||||||
appCtx.getPrefBoolean(PreferKey.coverShowStroke, true)
|
|
||||||
coverDefaultColor =
|
|
||||||
appCtx.getPrefBoolean(PreferKey.coverDefaultColor, true)
|
|
||||||
coverTextColor =
|
|
||||||
appCtx.getPrefInt(PreferKey.coverTextColor, Color.WHITE)
|
|
||||||
coverTextColorN =
|
|
||||||
appCtx.getPrefInt(PreferKey.coverTextColorN, Color.BLACK)
|
|
||||||
coverShadowColor =
|
|
||||||
appCtx.getPrefInt(PreferKey.coverShadowColor, Color.WHITE)
|
|
||||||
coverShadowColorN =
|
|
||||||
appCtx.getPrefInt(PreferKey.coverShadowColorN, Color.BLACK)
|
|
||||||
|
|
||||||
val key = if (isNightTheme) PreferKey.defaultCoverDark else PreferKey.defaultCover
|
val key = if (isNightTheme) PreferKey.defaultCoverDark else PreferKey.defaultCover
|
||||||
val paths = appCtx.getPrefString(key)?.split(",")?.filter { it.isNotBlank() }
|
val paths = appCtx.getPrefString(key)?.split(",")?.filter { it.isNotBlank() }
|
||||||
|
|
||||||
if (paths.isNullOrEmpty()) {
|
if (paths.isNullOrEmpty()) {
|
||||||
defaultDrawable = appCtx.resources.getDrawable(R.drawable.image_cover_default, null)
|
return appCtx.resources.getDrawable(R.drawable.image_cover_default, null)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val randomPath = paths[Random.nextInt(paths.size)]
|
val randomPath = paths[Random.nextInt(paths.size)]
|
||||||
defaultDrawable = kotlin.runCatching {
|
return kotlin.runCatching {
|
||||||
BitmapUtils.decodeBitmap(randomPath, 600, 900)!!.toDrawable(appCtx.resources)
|
BitmapUtils.decodeBitmap(randomPath, 600, 900)!!.toDrawable(appCtx.resources)
|
||||||
}.getOrDefault(appCtx.resources.getDrawable(R.drawable.image_cover_default, null))
|
}.getOrDefault(appCtx.resources.getDrawable(R.drawable.image_cover_default, null))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getRandomDefaultPath(seed: Any? = null): String? {
|
// 兼容旧代码,空实现
|
||||||
val isNightTheme = AppConfig.isNightTheme
|
fun upDefaultCover() {}
|
||||||
val key = if (isNightTheme) PreferKey.defaultCoverDark else PreferKey.defaultCover
|
|
||||||
|
fun getRandomDefaultPath(
|
||||||
|
seed: Any? = null,
|
||||||
|
isNight: Boolean = AppConfig.isNightTheme
|
||||||
|
): String? {
|
||||||
|
val key = if (isNight) PreferKey.defaultCoverDark else PreferKey.defaultCover
|
||||||
val paths = appCtx.getPrefString(key)?.split(",")?.filter { it.isNotBlank() }
|
val paths = appCtx.getPrefString(key)?.split(",")?.filter { it.isNotBlank() }
|
||||||
if (paths.isNullOrEmpty()) return null
|
if (paths.isNullOrEmpty()) return null
|
||||||
val random = if (seed != null) Random(seed.hashCode()) else Random
|
val random = if (seed != null) Random(seed.hashCode()) else Random
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ fun <T> prefDelegate(
|
|||||||
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
|
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
|
||||||
if (_value.value != value) {
|
if (_value.value != value) {
|
||||||
when (value) {
|
when (value) {
|
||||||
is String -> appCtx.putPrefString(key, value)
|
is String? -> appCtx.putPrefString(key, value)
|
||||||
is Int -> appCtx.putPrefInt(key, value)
|
is Int -> appCtx.putPrefInt(key, value)
|
||||||
is Boolean -> appCtx.putPrefBoolean(key, value)
|
is Boolean -> appCtx.putPrefBoolean(key, value)
|
||||||
is Long -> appCtx.putPrefLong(key, value)
|
is Long -> appCtx.putPrefLong(key, value)
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import androidx.navigation.compose.composable
|
|||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
import io.legado.app.base.BaseComposeActivity
|
import io.legado.app.base.BaseComposeActivity
|
||||||
|
import io.legado.app.ui.config.backupConfig.BackupConfigScreen
|
||||||
import io.legado.app.ui.config.coverConfig.CoverConfigScreen
|
import io.legado.app.ui.config.coverConfig.CoverConfigScreen
|
||||||
import io.legado.app.ui.config.otherConfig.OtherConfigScreen
|
import io.legado.app.ui.config.otherConfig.OtherConfigScreen
|
||||||
import io.legado.app.ui.config.readConfig.ReadConfigScreen
|
import io.legado.app.ui.config.readConfig.ReadConfigScreen
|
||||||
@@ -46,6 +47,9 @@ class TestConfigActivity : BaseComposeActivity() {
|
|||||||
@Serializable
|
@Serializable
|
||||||
object ThemeConfigRoute
|
object ThemeConfigRoute
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
object BackupConfigRoute
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun Content() {
|
override fun Content() {
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
@@ -60,7 +64,8 @@ class TestConfigActivity : BaseComposeActivity() {
|
|||||||
onNavigateToOther = { navController.navigate(OtherConfigRoute) },
|
onNavigateToOther = { navController.navigate(OtherConfigRoute) },
|
||||||
onNavigateToRead = { navController.navigate(ReadConfigRoute) },
|
onNavigateToRead = { navController.navigate(ReadConfigRoute) },
|
||||||
onNavigateToCover = { navController.navigate(CoverConfigRoute) },
|
onNavigateToCover = { navController.navigate(CoverConfigRoute) },
|
||||||
onNavigateToTheme = { navController.navigate(ThemeConfigRoute) }
|
onNavigateToTheme = { navController.navigate(ThemeConfigRoute) },
|
||||||
|
onNavigateToBackup = { navController.navigate(BackupConfigRoute) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,7 +82,13 @@ class TestConfigActivity : BaseComposeActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
composable<ThemeConfigRoute> {
|
composable<ThemeConfigRoute> {
|
||||||
ThemeConfigScreen(onBackClick = { navController.popBackStack() })
|
ThemeConfigScreen(
|
||||||
|
onBackClick = { navController.popBackStack() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
composable<BackupConfigRoute> {
|
||||||
|
BackupConfigScreen(onBackClick = { navController.popBackStack() })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -89,7 +100,8 @@ class TestConfigActivity : BaseComposeActivity() {
|
|||||||
onNavigateToOther: () -> Unit,
|
onNavigateToOther: () -> Unit,
|
||||||
onNavigateToRead: () -> Unit,
|
onNavigateToRead: () -> Unit,
|
||||||
onNavigateToCover: () -> Unit,
|
onNavigateToCover: () -> Unit,
|
||||||
onNavigateToTheme: () -> Unit
|
onNavigateToTheme: () -> Unit,
|
||||||
|
onNavigateToBackup: () -> Unit
|
||||||
) {
|
) {
|
||||||
val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
|
val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
|
||||||
|
|
||||||
@@ -129,6 +141,10 @@ class TestConfigActivity : BaseComposeActivity() {
|
|||||||
title = stringResource(R.string.cover_config),
|
title = stringResource(R.string.cover_config),
|
||||||
onClick = onNavigateToCover
|
onClick = onNavigateToCover
|
||||||
)
|
)
|
||||||
|
ClickableSettingItem(
|
||||||
|
title = stringResource(R.string.backup_restore),
|
||||||
|
onClick = onNavigateToBackup
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package io.legado.app.ui.config.backupConfig
|
||||||
|
|
||||||
|
import io.legado.app.constant.PreferKey
|
||||||
|
import io.legado.app.ui.config.prefDelegate
|
||||||
|
|
||||||
|
object BackupConfig {
|
||||||
|
|
||||||
|
var webDavUrl by prefDelegate(
|
||||||
|
PreferKey.webDavUrl,
|
||||||
|
""
|
||||||
|
)
|
||||||
|
|
||||||
|
var webDavAccount by prefDelegate(
|
||||||
|
PreferKey.webDavAccount,
|
||||||
|
""
|
||||||
|
)
|
||||||
|
|
||||||
|
var webDavPassword by prefDelegate(
|
||||||
|
PreferKey.webDavPassword,
|
||||||
|
""
|
||||||
|
)
|
||||||
|
|
||||||
|
var webDavDir by prefDelegate(
|
||||||
|
PreferKey.webDavDir,
|
||||||
|
"legado"
|
||||||
|
)
|
||||||
|
|
||||||
|
var webDavDeviceName by prefDelegate(
|
||||||
|
PreferKey.webDavDeviceName,
|
||||||
|
""
|
||||||
|
)
|
||||||
|
|
||||||
|
var syncBookProgress by prefDelegate(
|
||||||
|
PreferKey.syncBookProgress,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
|
||||||
|
var syncBookProgressPlus by prefDelegate(
|
||||||
|
PreferKey.syncBookProgressPlus,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
|
||||||
|
var autoCheckNewBackup by prefDelegate(
|
||||||
|
PreferKey.autoCheckNewBackup,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
|
||||||
|
var onlyLatestBackup by prefDelegate(
|
||||||
|
PreferKey.onlyLatestBackup,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
|
||||||
|
var backupPath by prefDelegate<String?>(
|
||||||
|
PreferKey.backupPath,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,615 @@
|
|||||||
|
package io.legado.app.ui.config.backupConfig
|
||||||
|
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Visibility
|
||||||
|
import androidx.compose.material.icons.filled.VisibilityOff
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.OutlinedButton
|
||||||
|
import androidx.compose.material3.SnackbarHost
|
||||||
|
import androidx.compose.material3.SnackbarHostState
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.material3.TextField
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.legado.app.R
|
||||||
|
import io.legado.app.help.storage.Restore
|
||||||
|
import io.legado.app.lib.permission.Permissions
|
||||||
|
import io.legado.app.lib.permission.PermissionsCompat
|
||||||
|
import io.legado.app.ui.widget.components.AppScaffold
|
||||||
|
import io.legado.app.ui.widget.components.GlassMediumFlexibleTopAppBar
|
||||||
|
import io.legado.app.ui.widget.components.GlassTopAppBarDefaults
|
||||||
|
import io.legado.app.ui.widget.components.SplicedColumnGroup
|
||||||
|
import io.legado.app.ui.widget.components.button.TopbarNavigationButton
|
||||||
|
import io.legado.app.ui.widget.components.filePicker.FilePickerSheet
|
||||||
|
import io.legado.app.ui.widget.components.settingItem.ClickableSettingItem
|
||||||
|
import io.legado.app.ui.widget.components.settingItem.InputSettingItem
|
||||||
|
import io.legado.app.ui.widget.components.settingItem.SwitchSettingItem
|
||||||
|
import io.legado.app.utils.isContentScheme
|
||||||
|
import io.legado.app.utils.takePersistablePermissionSafely
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.koin.androidx.compose.koinViewModel
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun BackupConfigScreen(
|
||||||
|
onBackClick: () -> Unit,
|
||||||
|
viewModel: BackupConfigViewModel = koinViewModel()
|
||||||
|
) {
|
||||||
|
val context by rememberUpdatedState(LocalContext.current)
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
|
||||||
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
|
|
||||||
|
var showWebDavAuthDialog by remember { mutableStateOf(false) }
|
||||||
|
var showBackupIgnoreDialog by remember { mutableStateOf(false) }
|
||||||
|
var showConfirmDialog by remember { mutableStateOf(false) }
|
||||||
|
var confirmDialogTitle by remember { mutableStateOf("") }
|
||||||
|
var confirmDialogText by remember { mutableStateOf("") }
|
||||||
|
var onConfirmAction by remember { mutableStateOf<(() -> Unit)?>(null) }
|
||||||
|
|
||||||
|
var tempAccount by remember { mutableStateOf("") }
|
||||||
|
var tempPassword by remember { mutableStateOf("") }
|
||||||
|
var passwordVisible by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
var showBackupFilePicker by remember { mutableStateOf(false) }
|
||||||
|
var showRestoreFilePicker by remember { mutableStateOf(false) }
|
||||||
|
var showRestoreSheet by remember { mutableStateOf(false) }
|
||||||
|
var backupNames by remember { mutableStateOf<List<String>>(emptyList()) }
|
||||||
|
|
||||||
|
var showLoadingDialog by remember { mutableStateOf(false) }
|
||||||
|
var loadingText by remember { mutableStateOf("") }
|
||||||
|
|
||||||
|
val selectBackupPathLauncher = rememberLauncherForActivityResult(
|
||||||
|
contract = ActivityResultContracts.OpenDocumentTree()
|
||||||
|
) { uri ->
|
||||||
|
uri?.let {
|
||||||
|
it.takePersistablePermissionSafely(context)
|
||||||
|
if (it.isContentScheme()) {
|
||||||
|
BackupConfig.backupPath = it.toString()
|
||||||
|
} else {
|
||||||
|
BackupConfig.backupPath = it.path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val backupAndSelectLauncher = rememberLauncherForActivityResult(
|
||||||
|
contract = ActivityResultContracts.OpenDocumentTree()
|
||||||
|
) { uri ->
|
||||||
|
uri?.let {
|
||||||
|
it.takePersistablePermissionSafely(context)
|
||||||
|
val path = if (it.isContentScheme()) {
|
||||||
|
it.toString()
|
||||||
|
} else {
|
||||||
|
it.path ?: ""
|
||||||
|
}
|
||||||
|
BackupConfig.backupPath = path
|
||||||
|
if (path.isNotEmpty()) {
|
||||||
|
startBackup(path, context, viewModel, {
|
||||||
|
showLoadingDialog = false
|
||||||
|
scope.launch {
|
||||||
|
snackbarHostState.showSnackbar(context.getString(R.string.backup_success))
|
||||||
|
}
|
||||||
|
}, { error ->
|
||||||
|
showLoadingDialog = false
|
||||||
|
scope.launch {
|
||||||
|
snackbarHostState.showSnackbar(
|
||||||
|
context.getString(
|
||||||
|
R.string.backup_fail,
|
||||||
|
error
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val restoreFileLauncher = rememberLauncherForActivityResult(
|
||||||
|
contract = ActivityResultContracts.OpenDocument()
|
||||||
|
) { uri ->
|
||||||
|
uri?.let {
|
||||||
|
showLoadingDialog = true
|
||||||
|
loadingText = "恢复中…"
|
||||||
|
scope.launch {
|
||||||
|
try {
|
||||||
|
Restore.restore(context, uri)
|
||||||
|
showLoadingDialog = false
|
||||||
|
snackbarHostState.showSnackbar("恢复成功")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
showLoadingDialog = false
|
||||||
|
snackbarHostState.showSnackbar("恢复出错: ${e.localizedMessage}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val importOldLauncher = rememberLauncherForActivityResult(
|
||||||
|
contract = ActivityResultContracts.OpenDocument()
|
||||||
|
) { uri ->
|
||||||
|
uri?.let {
|
||||||
|
io.legado.app.help.storage.ImportOldData.importUri(context, uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AppScaffold(
|
||||||
|
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
|
snackbarHost = {
|
||||||
|
SnackbarHost(
|
||||||
|
hostState = snackbarHostState
|
||||||
|
)
|
||||||
|
},
|
||||||
|
topBar = {
|
||||||
|
GlassMediumFlexibleTopAppBar(
|
||||||
|
title = {
|
||||||
|
Text(stringResource(R.string.backup_restore))
|
||||||
|
},
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
navigationIcon = {
|
||||||
|
TopbarNavigationButton(onClick = onBackClick)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { paddingValues ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(paddingValues)
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(16.dp)
|
||||||
|
) {
|
||||||
|
SplicedColumnGroup(title = stringResource(R.string.web_dav_set)) {
|
||||||
|
InputSettingItem(
|
||||||
|
title = stringResource(R.string.web_dav_url),
|
||||||
|
value = BackupConfig.webDavUrl,
|
||||||
|
defaultValue = "",
|
||||||
|
onConfirm = { BackupConfig.webDavUrl = it }
|
||||||
|
)
|
||||||
|
|
||||||
|
ClickableSettingItem(
|
||||||
|
title = stringResource(R.string.web_dav_account),
|
||||||
|
description = "设置 WebDAV 账号和密码",
|
||||||
|
onClick = {
|
||||||
|
tempAccount = viewModel.getWebDavAccount()
|
||||||
|
tempPassword = viewModel.getWebDavPassword()
|
||||||
|
showWebDavAuthDialog = true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
InputSettingItem(
|
||||||
|
title = stringResource(R.string.sub_dir),
|
||||||
|
value = BackupConfig.webDavDir,
|
||||||
|
defaultValue = "legado",
|
||||||
|
onConfirm = { BackupConfig.webDavDir = it }
|
||||||
|
)
|
||||||
|
|
||||||
|
InputSettingItem(
|
||||||
|
title = stringResource(R.string.webdav_device_name),
|
||||||
|
value = BackupConfig.webDavDeviceName,
|
||||||
|
defaultValue = "",
|
||||||
|
onConfirm = { BackupConfig.webDavDeviceName = it }
|
||||||
|
)
|
||||||
|
|
||||||
|
ClickableSettingItem(
|
||||||
|
title = "测试 WebDav 配置",
|
||||||
|
description = "测试 WebDav 服务工作状态",
|
||||||
|
onClick = {
|
||||||
|
scope.launch {
|
||||||
|
showLoadingDialog = true
|
||||||
|
loadingText = "测试中…"
|
||||||
|
val success = viewModel.testWebDav()
|
||||||
|
showLoadingDialog = false
|
||||||
|
if (success) {
|
||||||
|
snackbarHostState.showSnackbar("WebDav 配置正确")
|
||||||
|
} else {
|
||||||
|
snackbarHostState.showSnackbar("WebDav 配置错误")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
SwitchSettingItem(
|
||||||
|
title = stringResource(R.string.sync_book_progress_t),
|
||||||
|
description = stringResource(R.string.sync_book_progress_s),
|
||||||
|
checked = BackupConfig.syncBookProgress,
|
||||||
|
onCheckedChange = {
|
||||||
|
BackupConfig.syncBookProgress = it
|
||||||
|
if (!it) {
|
||||||
|
BackupConfig.syncBookProgressPlus = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (BackupConfig.syncBookProgress) {
|
||||||
|
SwitchSettingItem(
|
||||||
|
title = stringResource(R.string.sync_book_progress_plus_t),
|
||||||
|
description = stringResource(R.string.sync_book_progress_plus_s),
|
||||||
|
checked = BackupConfig.syncBookProgressPlus,
|
||||||
|
onCheckedChange = { BackupConfig.syncBookProgressPlus = it }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
SwitchSettingItem(
|
||||||
|
title = stringResource(R.string.auto_check_new_backup_t),
|
||||||
|
description = stringResource(R.string.auto_check_new_backup_s),
|
||||||
|
checked = BackupConfig.autoCheckNewBackup,
|
||||||
|
onCheckedChange = { BackupConfig.autoCheckNewBackup = it }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
SplicedColumnGroup(title = stringResource(R.string.backup_restore)) {
|
||||||
|
ClickableSettingItem(
|
||||||
|
title = stringResource(R.string.backup_path),
|
||||||
|
description = BackupConfig.backupPath
|
||||||
|
?: stringResource(R.string.select_backup_path),
|
||||||
|
onClick = { showBackupFilePicker = true }
|
||||||
|
)
|
||||||
|
|
||||||
|
val backupText = stringResource(R.string.backup)
|
||||||
|
|
||||||
|
ClickableSettingItem(
|
||||||
|
title = stringResource(R.string.backup),
|
||||||
|
description = stringResource(R.string.backup_summary),
|
||||||
|
onClick = {
|
||||||
|
val backupPath = BackupConfig.backupPath
|
||||||
|
if (backupPath.isNullOrEmpty()) {
|
||||||
|
showRestoreFilePicker = true
|
||||||
|
} else {
|
||||||
|
confirmDialogTitle = backupText
|
||||||
|
confirmDialogText = "确定要备份吗?"
|
||||||
|
onConfirmAction = {
|
||||||
|
if (backupPath.isContentScheme()) {
|
||||||
|
startBackup(backupPath, context, viewModel, {
|
||||||
|
showLoadingDialog = false
|
||||||
|
scope.launch {
|
||||||
|
snackbarHostState.showSnackbar(context.getString(R.string.backup_success))
|
||||||
|
}
|
||||||
|
}, { error ->
|
||||||
|
showLoadingDialog = false
|
||||||
|
scope.launch {
|
||||||
|
snackbarHostState.showSnackbar(
|
||||||
|
context.getString(
|
||||||
|
R.string.backup_fail,
|
||||||
|
error
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
PermissionsCompat.Builder()
|
||||||
|
.addPermissions(*Permissions.Group.STORAGE)
|
||||||
|
.rationale(R.string.tip_perm_request_storage)
|
||||||
|
.onGranted {
|
||||||
|
startBackup(backupPath, context, viewModel, {
|
||||||
|
showLoadingDialog = false
|
||||||
|
scope.launch {
|
||||||
|
snackbarHostState.showSnackbar(
|
||||||
|
context.getString(
|
||||||
|
R.string.backup_success
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}, { error ->
|
||||||
|
showLoadingDialog = false
|
||||||
|
scope.launch {
|
||||||
|
snackbarHostState.showSnackbar(
|
||||||
|
context.getString(
|
||||||
|
R.string.backup_fail,
|
||||||
|
error
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
.request()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
showConfirmDialog = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
ClickableSettingItem(
|
||||||
|
title = stringResource(R.string.restore),
|
||||||
|
description = stringResource(R.string.restore_summary),
|
||||||
|
onClick = {
|
||||||
|
scope.launch {
|
||||||
|
showLoadingDialog = true
|
||||||
|
loadingText = "加载中"
|
||||||
|
try {
|
||||||
|
val names = viewModel.getBackupNames()
|
||||||
|
backupNames = names
|
||||||
|
showRestoreSheet = true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
confirmDialogTitle = "恢复"
|
||||||
|
confirmDialogText =
|
||||||
|
"WebDavError\n${e.localizedMessage}\n将从本地备份恢复。"
|
||||||
|
onConfirmAction = {
|
||||||
|
restoreFileLauncher.launch(arrayOf("application/zip"))
|
||||||
|
}
|
||||||
|
showConfirmDialog = true
|
||||||
|
} finally {
|
||||||
|
showLoadingDialog = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
ClickableSettingItem(
|
||||||
|
title = stringResource(R.string.restore_ignore),
|
||||||
|
description = stringResource(R.string.restore_ignore_summary),
|
||||||
|
onClick = { showBackupIgnoreDialog = true }
|
||||||
|
)
|
||||||
|
|
||||||
|
ClickableSettingItem(
|
||||||
|
title = stringResource(R.string.menu_import_old_version),
|
||||||
|
description = stringResource(R.string.import_old_summary),
|
||||||
|
onClick = {
|
||||||
|
importOldLauncher.launch(arrayOf("*/*"))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
SwitchSettingItem(
|
||||||
|
title = stringResource(R.string.only_latest_backup_t),
|
||||||
|
description = stringResource(R.string.only_latest_backup_s),
|
||||||
|
checked = BackupConfig.onlyLatestBackup,
|
||||||
|
onCheckedChange = { BackupConfig.onlyLatestBackup = it }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showWebDavAuthDialog) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { showWebDavAuthDialog = false },
|
||||||
|
title = { Text(stringResource(R.string.web_dav_account)) },
|
||||||
|
text = {
|
||||||
|
Column {
|
||||||
|
TextField(
|
||||||
|
value = tempAccount,
|
||||||
|
onValueChange = { tempAccount = it },
|
||||||
|
label = { Text("账号") }
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.padding(8.dp))
|
||||||
|
TextField(
|
||||||
|
value = tempPassword,
|
||||||
|
onValueChange = { tempPassword = it },
|
||||||
|
label = { Text("密码") },
|
||||||
|
visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
|
||||||
|
trailingIcon = {
|
||||||
|
val image = if (passwordVisible)
|
||||||
|
Icons.Filled.Visibility
|
||||||
|
else Icons.Filled.VisibilityOff
|
||||||
|
|
||||||
|
val description = if (passwordVisible) "隐藏密码" else "显示密码"
|
||||||
|
|
||||||
|
IconButton(onClick = { passwordVisible = !passwordVisible }) {
|
||||||
|
Icon(imageVector = image, description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
viewModel.setWebDavAccount(tempAccount, tempPassword)
|
||||||
|
showWebDavAuthDialog = false
|
||||||
|
scope.launch {
|
||||||
|
showLoadingDialog = true
|
||||||
|
loadingText = "测试中…"
|
||||||
|
val success = viewModel.testWebDav()
|
||||||
|
showLoadingDialog = false
|
||||||
|
if (success) {
|
||||||
|
snackbarHostState.showSnackbar("WebDav 配置正确")
|
||||||
|
} else {
|
||||||
|
snackbarHostState.showSnackbar("WebDav 配置错误")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Text(stringResource(R.string.ok))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = { showWebDavAuthDialog = false }) {
|
||||||
|
Text(stringResource(R.string.cancel))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showConfirmDialog) {
|
||||||
|
ConfirmDialog(
|
||||||
|
title = confirmDialogTitle,
|
||||||
|
text = confirmDialogText,
|
||||||
|
onConfirm = {
|
||||||
|
onConfirmAction?.invoke()
|
||||||
|
showConfirmDialog = false
|
||||||
|
},
|
||||||
|
onDismiss = { showConfirmDialog = false }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showBackupFilePicker) {
|
||||||
|
FilePickerSheet(
|
||||||
|
onDismissRequest = { showBackupFilePicker = false },
|
||||||
|
onSelectSysDir = {
|
||||||
|
showBackupFilePicker = false
|
||||||
|
try {
|
||||||
|
selectBackupPathLauncher.launch(null)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showRestoreFilePicker) {
|
||||||
|
FilePickerSheet(
|
||||||
|
onDismissRequest = { showRestoreFilePicker = false },
|
||||||
|
onSelectSysDir = {
|
||||||
|
showRestoreFilePicker = false
|
||||||
|
try {
|
||||||
|
backupAndSelectLauncher.launch(null)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showRestoreSheet && backupNames.isNotEmpty()) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { showRestoreSheet = false },
|
||||||
|
title = { Text(stringResource(R.string.select_restore_file)) },
|
||||||
|
text = {
|
||||||
|
Column {
|
||||||
|
backupNames.forEach { name ->
|
||||||
|
TextButton(onClick = {
|
||||||
|
showRestoreSheet = false
|
||||||
|
showLoadingDialog = true
|
||||||
|
loadingText = "恢复中…"
|
||||||
|
viewModel.restoreWebDav(
|
||||||
|
name,
|
||||||
|
{
|
||||||
|
showLoadingDialog = false
|
||||||
|
scope.launch {
|
||||||
|
snackbarHostState.showSnackbar("恢复成功")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ error ->
|
||||||
|
showLoadingDialog = false
|
||||||
|
scope.launch {
|
||||||
|
snackbarHostState.showSnackbar("WebDav恢复出错\n$error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}) {
|
||||||
|
Text(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = { showRestoreSheet = false }) {
|
||||||
|
Text(stringResource(R.string.cancel))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showBackupIgnoreDialog) {
|
||||||
|
val checkedItems = remember {
|
||||||
|
BooleanArray(io.legado.app.help.storage.BackupConfig.ignoreKeys.size) { index ->
|
||||||
|
io.legado.app.help.storage.BackupConfig.ignoreConfig[
|
||||||
|
io.legado.app.help.storage.BackupConfig.ignoreKeys[index]
|
||||||
|
] ?: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = {
|
||||||
|
io.legado.app.help.storage.BackupConfig.saveIgnoreConfig()
|
||||||
|
showBackupIgnoreDialog = false
|
||||||
|
},
|
||||||
|
title = { Text(stringResource(R.string.restore_ignore)) },
|
||||||
|
text = {
|
||||||
|
Column {
|
||||||
|
io.legado.app.help.storage.BackupConfig.ignoreTitle.forEachIndexed { index, title ->
|
||||||
|
TextButton(onClick = {
|
||||||
|
checkedItems[index] = !checkedItems[index]
|
||||||
|
io.legado.app.help.storage.BackupConfig.ignoreConfig[
|
||||||
|
io.legado.app.help.storage.BackupConfig.ignoreKeys[index]
|
||||||
|
] = checkedItems[index]
|
||||||
|
}) {
|
||||||
|
val isChecked = checkedItems[index]
|
||||||
|
Text(if (isChecked) "✓ $title" else title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
io.legado.app.help.storage.BackupConfig.saveIgnoreConfig()
|
||||||
|
showBackupIgnoreDialog = false
|
||||||
|
}) {
|
||||||
|
Text(stringResource(R.string.ok))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
io.legado.app.help.storage.BackupConfig.saveIgnoreConfig()
|
||||||
|
showBackupIgnoreDialog = false
|
||||||
|
}) {
|
||||||
|
Text(stringResource(R.string.cancel))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showLoadingDialog) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = {},
|
||||||
|
title = { Text(loadingText) },
|
||||||
|
confirmButton = {},
|
||||||
|
dismissButton = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ConfirmDialog(
|
||||||
|
title: String,
|
||||||
|
text: String,
|
||||||
|
onConfirm: () -> Unit,
|
||||||
|
onDismiss: () -> Unit
|
||||||
|
) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
title = { Text(title) },
|
||||||
|
text = { Text(text) },
|
||||||
|
confirmButton = {
|
||||||
|
OutlinedButton(onClick = onConfirm) {
|
||||||
|
Text(stringResource(R.string.ok))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = onDismiss) {
|
||||||
|
Text(stringResource(R.string.cancel))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startBackup(
|
||||||
|
path: String,
|
||||||
|
context: android.content.Context,
|
||||||
|
viewModel: BackupConfigViewModel,
|
||||||
|
onSuccess: () -> Unit,
|
||||||
|
onError: (String) -> Unit
|
||||||
|
) {
|
||||||
|
viewModel.backup(path, onSuccess, onError)
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package io.legado.app.ui.config.backupConfig
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import io.legado.app.help.AppWebDav
|
||||||
|
import io.legado.app.help.storage.Backup
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import splitties.init.appCtx
|
||||||
|
|
||||||
|
class BackupConfigViewModel : ViewModel() {
|
||||||
|
|
||||||
|
fun setWebDavAccount(account: String, password: String) {
|
||||||
|
BackupConfig.webDavAccount = account
|
||||||
|
BackupConfig.webDavPassword = password
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getWebDavAccount(): String {
|
||||||
|
return BackupConfig.webDavAccount
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getWebDavPassword(): String {
|
||||||
|
return BackupConfig.webDavPassword
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun testWebDav(): Boolean {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
AppWebDav.testWebDav()
|
||||||
|
true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun backup(backupPath: String, onSuccess: () -> Unit, onError: (String) -> Unit) {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
Backup.backupLocked(appCtx, backupPath)
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
onSuccess()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
onError(e.localizedMessage ?: "备份出错")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getBackupNames(): List<String> {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
AppWebDav.getBackupNames()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun restoreWebDav(name: String, onSuccess: () -> Unit, onError: (String) -> Unit) {
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
AppWebDav.restoreWebDav(name)
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
onSuccess()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
onError(e.localizedMessage ?: "恢复出错")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -39,7 +39,6 @@ import org.koin.androidx.compose.koinViewModel
|
|||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun BackgroundImageManageSheet(
|
fun BackgroundImageManageSheet(
|
||||||
preferenceKey: String,
|
|
||||||
isDarkTheme: Boolean,
|
isDarkTheme: Boolean,
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
viewModel: ThemeConfigViewModel = koinViewModel()
|
viewModel: ThemeConfigViewModel = koinViewModel()
|
||||||
@@ -53,7 +52,7 @@ fun BackgroundImageManageSheet(
|
|||||||
scope.launch {
|
scope.launch {
|
||||||
viewModel.setBackgroundFromUri(
|
viewModel.setBackgroundFromUri(
|
||||||
uri = it,
|
uri = it,
|
||||||
preferenceKey = preferenceKey
|
isDarkTheme = isDarkTheme
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -118,7 +117,7 @@ fun BackgroundImageManageSheet(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = { viewModel.removeBackground(preferenceKey) },
|
onClick = { viewModel.removeBackground(isDarkTheme) },
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.TopEnd)
|
.align(Alignment.TopEnd)
|
||||||
.padding(8.dp)
|
.padding(8.dp)
|
||||||
|
|||||||
@@ -0,0 +1,200 @@
|
|||||||
|
package io.legado.app.ui.config.themeConfig
|
||||||
|
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.widget.ImageView
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
|
import androidx.compose.foundation.lazy.grid.items
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
|
import io.legado.app.R
|
||||||
|
import io.legado.app.ui.main.Launcher0
|
||||||
|
import io.legado.app.ui.main.Launcher1
|
||||||
|
import io.legado.app.ui.main.Launcher2
|
||||||
|
import io.legado.app.ui.main.Launcher3
|
||||||
|
import io.legado.app.ui.main.Launcher4
|
||||||
|
import io.legado.app.ui.main.Launcher5
|
||||||
|
import io.legado.app.ui.main.Launcher6
|
||||||
|
import io.legado.app.ui.main.LauncherW
|
||||||
|
import io.legado.app.ui.widget.components.modalBottomSheet.GlassModalBottomSheet
|
||||||
|
import io.legado.app.utils.getCompatDrawable
|
||||||
|
import splitties.init.appCtx
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun LauncherIconPickerSheet(
|
||||||
|
selectedValue: String,
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
onValueChange: (String) -> Unit
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val icons = LauncherIcons.list
|
||||||
|
|
||||||
|
GlassModalBottomSheet(
|
||||||
|
onDismissRequest = onDismissRequest
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(bottom = 24.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.change_icon),
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
modifier = Modifier.padding(16.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
LazyVerticalGrid(
|
||||||
|
columns = GridCells.Fixed(3),
|
||||||
|
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
items(icons, key = { it.value }) { item ->
|
||||||
|
|
||||||
|
val isSelected = item.value == selectedValue
|
||||||
|
val drawable = remember(item.resId) {
|
||||||
|
context.getCompatDrawable(item.resId)
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.aspectRatio(1f)
|
||||||
|
.clip(MaterialTheme.shapes.large)
|
||||||
|
.background(
|
||||||
|
if (isSelected)
|
||||||
|
MaterialTheme.colorScheme.secondaryContainer
|
||||||
|
else
|
||||||
|
MaterialTheme.colorScheme.surfaceContainer
|
||||||
|
)
|
||||||
|
.then(
|
||||||
|
if (isSelected) {
|
||||||
|
Modifier.border(
|
||||||
|
width = 2.dp,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
shape = MaterialTheme.shapes.large
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.clickable {
|
||||||
|
onValueChange(item.value)
|
||||||
|
onDismissRequest()
|
||||||
|
}
|
||||||
|
.padding(16.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
AndroidView(
|
||||||
|
factory = { ctx ->
|
||||||
|
ImageView(ctx).apply {
|
||||||
|
scaleType = ImageView.ScaleType.FIT_CENTER
|
||||||
|
}
|
||||||
|
},
|
||||||
|
update = { imageView ->
|
||||||
|
imageView.setImageDrawable(drawable)
|
||||||
|
},
|
||||||
|
modifier = Modifier.size(48.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class LauncherIconItem(
|
||||||
|
val value: String,
|
||||||
|
val label: String,
|
||||||
|
val resId: Int,
|
||||||
|
val component: ComponentName
|
||||||
|
)
|
||||||
|
|
||||||
|
object LauncherIcons {
|
||||||
|
|
||||||
|
val list = listOf(
|
||||||
|
LauncherIconItem(
|
||||||
|
value = "ic_launcher",
|
||||||
|
label = "iconMain",
|
||||||
|
resId = R.mipmap.ic_launcher,
|
||||||
|
component = ComponentName(appCtx, LauncherW::class.java)
|
||||||
|
),
|
||||||
|
LauncherIconItem(
|
||||||
|
value = "launcherw",
|
||||||
|
label = "iconWhite",
|
||||||
|
resId = R.mipmap.launcherw,
|
||||||
|
component = ComponentName(appCtx, LauncherW::class.java)
|
||||||
|
),
|
||||||
|
LauncherIconItem(
|
||||||
|
value = "launcher0",
|
||||||
|
label = "icon0",
|
||||||
|
resId = R.mipmap.launcher0,
|
||||||
|
component = ComponentName(appCtx, Launcher0::class.java)
|
||||||
|
),
|
||||||
|
LauncherIconItem(
|
||||||
|
value = "launcher1",
|
||||||
|
label = "icon1",
|
||||||
|
resId = R.mipmap.launcher1,
|
||||||
|
component = ComponentName(appCtx, Launcher1::class.java)
|
||||||
|
),
|
||||||
|
LauncherIconItem(
|
||||||
|
value = "launcher2",
|
||||||
|
label = "icon2",
|
||||||
|
resId = R.mipmap.launcher2,
|
||||||
|
component = ComponentName(appCtx, Launcher2::class.java)
|
||||||
|
),
|
||||||
|
LauncherIconItem(
|
||||||
|
value = "launcher3",
|
||||||
|
label = "icon3",
|
||||||
|
resId = R.mipmap.launcher3,
|
||||||
|
component = ComponentName(appCtx, Launcher3::class.java)
|
||||||
|
),
|
||||||
|
LauncherIconItem(
|
||||||
|
value = "launcher4",
|
||||||
|
label = "icon4",
|
||||||
|
resId = R.mipmap.launcher4,
|
||||||
|
component = ComponentName(appCtx, Launcher4::class.java)
|
||||||
|
),
|
||||||
|
LauncherIconItem(
|
||||||
|
value = "launcher5",
|
||||||
|
label = "icon5",
|
||||||
|
resId = R.mipmap.launcher5,
|
||||||
|
component = ComponentName(appCtx, Launcher5::class.java)
|
||||||
|
),
|
||||||
|
LauncherIconItem(
|
||||||
|
value = "launcher6",
|
||||||
|
label = "icon6",
|
||||||
|
resId = R.mipmap.launcher6,
|
||||||
|
component = ComponentName(appCtx, Launcher6::class.java)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
fun find(value: String?): LauncherIconItem? {
|
||||||
|
return list.find { it.value == value }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
package io.legado.app.ui.config.themeConfig
|
package io.legado.app.ui.config.themeConfig
|
||||||
|
|
||||||
import android.util.Log
|
|
||||||
import io.legado.app.constant.EventBus
|
import io.legado.app.constant.EventBus
|
||||||
import io.legado.app.constant.PreferKey
|
import io.legado.app.constant.PreferKey
|
||||||
import io.legado.app.ui.config.prefDelegate
|
import io.legado.app.ui.config.prefDelegate
|
||||||
@@ -20,6 +19,8 @@ object ThemeConfig {
|
|||||||
|
|
||||||
var appTheme by prefDelegate(PreferKey.appTheme, "0")
|
var appTheme by prefDelegate(PreferKey.appTheme, "0")
|
||||||
|
|
||||||
|
var themeMode by prefDelegate(PreferKey.themeMode, "0")
|
||||||
|
|
||||||
var isPureBlack by prefDelegate(PreferKey.pureBlack, false)
|
var isPureBlack by prefDelegate(PreferKey.pureBlack, false)
|
||||||
|
|
||||||
var bgImageLight by prefDelegate<String?>(PreferKey.bgImage, null) {
|
var bgImageLight by prefDelegate<String?>(PreferKey.bgImage, null) {
|
||||||
@@ -34,28 +35,37 @@ object ThemeConfig {
|
|||||||
|
|
||||||
var bgImageNBlurring by prefDelegate(PreferKey.bgImageNBlurring, 0)
|
var bgImageNBlurring by prefDelegate(PreferKey.bgImageNBlurring, 0)
|
||||||
|
|
||||||
fun hasImageBg(isDark: Boolean): Boolean {
|
var isPredictiveBackEnabled by prefDelegate(PreferKey.isPredictiveBackEnabled, true)
|
||||||
val result = if (isDark) {
|
|
||||||
!bgImageDark.isNullOrBlank()
|
var customMode by prefDelegate<String?>(PreferKey.customMode, "tonalSpot")
|
||||||
} else {
|
|
||||||
!bgImageLight.isNullOrBlank()
|
var fontScale by prefDelegate(PreferKey.fontScale, 10) {
|
||||||
|
postEvent(EventBus.RECREATE, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d(
|
var cPrimary by prefDelegate(PreferKey.cPrimary, 0) {
|
||||||
"MainConfig",
|
postEvent(EventBus.RECREATE, "")
|
||||||
"hasImageBg -> isDark=$isDark, " +
|
|
||||||
"bgImageDark=$bgImageDark, " +
|
|
||||||
"bgImageLight=$bgImageLight, " +
|
|
||||||
"result=$result"
|
|
||||||
)
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
var launcherIcon by prefDelegate(PreferKey.launcherIcon, "ic_launcher")
|
||||||
fun hasImageBg(isDark: Boolean): Boolean {
|
|
||||||
return if (isDark) bgImageDark.isNullOrBlank() else bgImageLight.isNullOrBlank()
|
var showDiscovery by prefDelegate(PreferKey.showDiscovery, true)
|
||||||
}
|
|
||||||
*/
|
var showRss by prefDelegate(PreferKey.showRss, true)
|
||||||
|
|
||||||
|
var showStatusBar by prefDelegate(PreferKey.showStatusBar, true)
|
||||||
|
|
||||||
|
var swipeAnimation by prefDelegate(PreferKey.swipeAnimation, true)
|
||||||
|
|
||||||
|
var showBottomView by prefDelegate(PreferKey.showBottomView, true)
|
||||||
|
|
||||||
|
var tabletInterface by prefDelegate(PreferKey.tabletInterface, "auto")
|
||||||
|
|
||||||
|
var labelVisibilityMode by prefDelegate(PreferKey.labelVisibilityMode, "auto")
|
||||||
|
|
||||||
|
var defaultHomePage by prefDelegate(PreferKey.defaultHomePage, "bookshelf")
|
||||||
|
|
||||||
|
fun hasImageBg(isDark: Boolean): Boolean =
|
||||||
|
!(if (isDark) bgImageDark else bgImageLight).isNullOrBlank()
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,42 +1,116 @@
|
|||||||
package io.legado.app.ui.config.themeConfig
|
package io.legado.app.ui.config.themeConfig
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
|
import androidx.compose.foundation.Canvas
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.BrightnessMedium
|
||||||
|
import androidx.compose.material.icons.filled.Check
|
||||||
|
import androidx.compose.material.icons.filled.DarkMode
|
||||||
|
import androidx.compose.material.icons.filled.LightMode
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.ButtonGroupDefaults
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedButton
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.material3.ToggleButton
|
||||||
|
import androidx.compose.material3.ToggleButtonDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringArrayResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.semantics.Role
|
||||||
|
import androidx.compose.ui.semantics.role
|
||||||
|
import androidx.compose.ui.semantics.semantics
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.constraintlayout.compose.ConstraintLayout
|
||||||
|
import com.google.android.material.color.DynamicColors
|
||||||
|
import com.google.android.material.color.DynamicColorsOptions
|
||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
import io.legado.app.constant.PreferKey
|
import io.legado.app.base.AppContextWrapper
|
||||||
|
import io.legado.app.constant.EventBus
|
||||||
|
import io.legado.app.help.LauncherIconHelp
|
||||||
|
import io.legado.app.help.config.AppConfig
|
||||||
|
import io.legado.app.help.config.OldThemeConfig
|
||||||
|
import io.legado.app.lib.theme.ThemeStore
|
||||||
|
import io.legado.app.lib.theme.primaryColor
|
||||||
|
import io.legado.app.ui.theme.ThemeManager
|
||||||
|
import io.legado.app.ui.theme.ThemeResolver
|
||||||
import io.legado.app.ui.widget.components.AppScaffold
|
import io.legado.app.ui.widget.components.AppScaffold
|
||||||
import io.legado.app.ui.widget.components.GlassMediumFlexibleTopAppBar
|
import io.legado.app.ui.widget.components.GlassMediumFlexibleTopAppBar
|
||||||
import io.legado.app.ui.widget.components.GlassTopAppBarDefaults
|
import io.legado.app.ui.widget.components.GlassTopAppBarDefaults
|
||||||
import io.legado.app.ui.widget.components.SplicedColumnGroup
|
import io.legado.app.ui.widget.components.SplicedColumnGroup
|
||||||
import io.legado.app.ui.widget.components.button.TopbarNavigationButton
|
import io.legado.app.ui.widget.components.button.TopbarNavigationButton
|
||||||
|
import io.legado.app.ui.widget.components.dialog.ColorPickerSheet
|
||||||
import io.legado.app.ui.widget.components.settingItem.ClickableSettingItem
|
import io.legado.app.ui.widget.components.settingItem.ClickableSettingItem
|
||||||
import io.legado.app.ui.widget.components.settingItem.DropdownListSettingItem
|
import io.legado.app.ui.widget.components.settingItem.DropdownListSettingItem
|
||||||
import io.legado.app.ui.widget.components.settingItem.SliderSettingItem
|
import io.legado.app.ui.widget.components.settingItem.SliderSettingItem
|
||||||
import io.legado.app.ui.widget.components.settingItem.SwitchSettingItem
|
import io.legado.app.ui.widget.components.settingItem.SwitchSettingItem
|
||||||
|
import io.legado.app.utils.postEvent
|
||||||
|
import io.legado.app.utils.restart
|
||||||
|
import io.legado.app.utils.toastOnUi
|
||||||
import org.koin.androidx.compose.koinViewModel
|
import org.koin.androidx.compose.koinViewModel
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ThemeConfigScreen(
|
fun ThemeConfigScreen(
|
||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
viewModel: ThemeConfigViewModel = koinViewModel()
|
viewModel: ThemeConfigViewModel = koinViewModel()
|
||||||
) {
|
) {
|
||||||
val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
|
val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
|
||||||
var manageKey by remember { mutableStateOf<Pair<String, Boolean>?>(null) }
|
var manageKey by remember { mutableStateOf<Boolean?>(null) }
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
var selectedThemeMode by remember { mutableStateOf(ThemeConfig.themeMode) }
|
||||||
|
var selectedTheme by remember { mutableStateOf(ThemeConfig.appTheme) }
|
||||||
|
var showRestartDialog by remember { mutableStateOf(false) }
|
||||||
|
var showColorPicker by remember { mutableStateOf(false) }
|
||||||
|
var showLauncherIconPicker by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val fontScaleValue = remember { mutableFloatStateOf(ThemeConfig.fontScale.toFloat()) }
|
||||||
|
val primaryColorValue = remember { mutableIntStateOf(ThemeConfig.cPrimary) }
|
||||||
|
|
||||||
AppScaffold(
|
AppScaffold(
|
||||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
@@ -57,26 +131,199 @@ fun ThemeConfigScreen(
|
|||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
.padding(16.dp)
|
.padding(16.dp)
|
||||||
) {
|
) {
|
||||||
|
val isDarkTheme = when (selectedThemeMode) {
|
||||||
|
"1" -> false
|
||||||
|
"2" -> true
|
||||||
|
else -> isSystemInDarkTheme()
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
ThemeCard(
|
||||||
|
context = context,
|
||||||
|
value = selectedTheme,
|
||||||
|
isDark = isDarkTheme,
|
||||||
|
isAmoled = ThemeConfig.isPureBlack,
|
||||||
|
paletteStyle = ThemeConfig.paletteStyle
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
val themeItems = stringArrayResource(R.array.themes_item)
|
||||||
|
val themeValues = stringArrayResource(R.array.themes_value)
|
||||||
|
val themes = remember(themeItems, themeValues) {
|
||||||
|
themeItems.zip(themeValues).toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
SplicedColumnGroup(title = stringResource(R.string.theme)) {
|
||||||
|
ThemeModeSelector(
|
||||||
|
selectedMode = selectedThemeMode,
|
||||||
|
onModeSelected = { mode ->
|
||||||
|
selectedThemeMode = mode
|
||||||
|
ThemeConfig.themeMode = mode
|
||||||
|
OldThemeConfig.applyDayNight(context)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
ThemeColorSelector(
|
||||||
|
context = context,
|
||||||
|
themes = themes,
|
||||||
|
selectedTheme = selectedTheme,
|
||||||
|
isDark = isDarkTheme,
|
||||||
|
isAmoled = ThemeConfig.isPureBlack,
|
||||||
|
paletteStyle = ThemeConfig.paletteStyle,
|
||||||
|
onThemeSelected = { theme ->
|
||||||
|
if (theme == "13") {
|
||||||
|
val hasLightBg = !ThemeConfig.bgImageLight.isNullOrEmpty()
|
||||||
|
val hasDarkBg = !ThemeConfig.bgImageDark.isNullOrEmpty()
|
||||||
|
if (!hasLightBg || !hasDarkBg) {
|
||||||
|
context.toastOnUi(R.string.transparent_theme_alarm)
|
||||||
|
return@ThemeColorSelector
|
||||||
|
} else {
|
||||||
|
AppConfig.containerOpacity = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val oldTheme = selectedTheme
|
||||||
|
selectedTheme = theme
|
||||||
|
ThemeConfig.appTheme = theme
|
||||||
|
val isDynamicSwitch = (oldTheme == "12" || theme == "12")
|
||||||
|
if (isDynamicSwitch) {
|
||||||
|
showRestartDialog = true
|
||||||
|
} else {
|
||||||
|
postEvent(EventBus.RECREATE, "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
SplicedColumnGroup {
|
SplicedColumnGroup {
|
||||||
SwitchSettingItem(
|
SwitchSettingItem(
|
||||||
title = stringResource(R.string.pure_black),
|
title = stringResource(R.string.pure_black),
|
||||||
checked = ThemeConfig.isPureBlack,
|
checked = ThemeConfig.isPureBlack,
|
||||||
onCheckedChange = { ThemeConfig.isPureBlack = it }
|
onCheckedChange = { ThemeConfig.isPureBlack = it }
|
||||||
)
|
)
|
||||||
|
ClickableSettingItem(
|
||||||
|
title = stringResource(R.string.change_icon),
|
||||||
|
description = stringResource(R.string.change_icon_summary),
|
||||||
|
onClick = { showLauncherIconPicker = true }
|
||||||
|
)
|
||||||
|
SwitchSettingItem(
|
||||||
|
title = "预见性返回手势",
|
||||||
|
description = "启用系统预见性返回手势",
|
||||||
|
checked = ThemeConfig.isPredictiveBackEnabled,
|
||||||
|
onCheckedChange = {
|
||||||
|
ThemeConfig.isPredictiveBackEnabled = it
|
||||||
|
context.toastOnUi("重启以应用")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
SliderSettingItem(
|
||||||
|
title = stringResource(R.string.font_scale),
|
||||||
|
description = stringResource(
|
||||||
|
R.string.font_scale_summary,
|
||||||
|
AppContextWrapper.getFontScale(context)
|
||||||
|
),
|
||||||
|
value = fontScaleValue.value,
|
||||||
|
defaultValue = 10f,
|
||||||
|
valueRange = 8f..16f,
|
||||||
|
steps = 7,
|
||||||
|
onValueChange = { value ->
|
||||||
|
fontScaleValue.floatValue = value
|
||||||
|
ThemeConfig.fontScale = value.toInt()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedTheme == "12") {
|
||||||
|
SplicedColumnGroup(title = "自定义主题") {
|
||||||
|
ClickableSettingItem(
|
||||||
|
title = stringResource(R.string.seed_color),
|
||||||
|
option = if (primaryColorValue.intValue != 0) "#${
|
||||||
|
Integer.toHexString(
|
||||||
|
primaryColorValue.intValue
|
||||||
|
).uppercase()
|
||||||
|
}" else "点击选择",
|
||||||
|
onClick = { showColorPicker = true },
|
||||||
|
trailingContent = {
|
||||||
|
if (primaryColorValue.intValue != 0) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(28.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(Color(primaryColorValue.intValue))
|
||||||
|
.border(
|
||||||
|
1.dp,
|
||||||
|
MaterialTheme.colorScheme.outlineVariant,
|
||||||
|
CircleShape
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
DropdownListSettingItem(
|
DropdownListSettingItem(
|
||||||
title = stringResource(R.string.palette_style),
|
title = stringResource(R.string.palette_style),
|
||||||
selectedValue = ThemeConfig.paletteStyle,
|
selectedValue = ThemeConfig.paletteStyle,
|
||||||
displayEntries = arrayOf(
|
displayEntries = stringArrayResource(R.array.paletteStyle),
|
||||||
"柔和", "中性", "鲜艳", "表现力", "彩虹",
|
entryValues = stringArrayResource(R.array.paletteStyle_value),
|
||||||
"水果拼盘", "单色", "仿真", "内容取色"
|
|
||||||
),
|
|
||||||
entryValues = arrayOf(
|
|
||||||
"tonalSpot", "neutral", "vibrant", "expressive", "rainbow",
|
|
||||||
"fruitSalad", "monochrome", "fidelity", "content"
|
|
||||||
),
|
|
||||||
onValueChange = { ThemeConfig.paletteStyle = it }
|
onValueChange = { ThemeConfig.paletteStyle = it }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SplicedColumnGroup(title = stringResource(R.string.main_activity)) {
|
||||||
|
SwitchSettingItem(
|
||||||
|
title = stringResource(R.string.show_discovery),
|
||||||
|
checked = ThemeConfig.showDiscovery,
|
||||||
|
onCheckedChange = { ThemeConfig.showDiscovery = it }
|
||||||
|
)
|
||||||
|
SwitchSettingItem(
|
||||||
|
title = stringResource(R.string.show_rss),
|
||||||
|
checked = ThemeConfig.showRss,
|
||||||
|
onCheckedChange = { ThemeConfig.showRss = it }
|
||||||
|
)
|
||||||
|
SwitchSettingItem(
|
||||||
|
title = stringResource(R.string.show_status),
|
||||||
|
checked = ThemeConfig.showStatusBar,
|
||||||
|
onCheckedChange = { ThemeConfig.showStatusBar = it }
|
||||||
|
)
|
||||||
|
//TODO:这个可以不要了,在删掉原来的设置页以后删
|
||||||
|
SwitchSettingItem(
|
||||||
|
title = stringResource(R.string.show_swipe_animation),
|
||||||
|
checked = ThemeConfig.swipeAnimation,
|
||||||
|
onCheckedChange = { ThemeConfig.swipeAnimation = it }
|
||||||
|
)
|
||||||
|
SwitchSettingItem(
|
||||||
|
title = stringResource(R.string.show_bottom_nav),
|
||||||
|
description = stringResource(R.string.be_swiped),
|
||||||
|
checked = ThemeConfig.showBottomView,
|
||||||
|
onCheckedChange = { ThemeConfig.showBottomView = it }
|
||||||
|
)
|
||||||
|
DropdownListSettingItem(
|
||||||
|
title = stringResource(R.string.tabletInterface),
|
||||||
|
selectedValue = ThemeConfig.tabletInterface,
|
||||||
|
displayEntries = stringArrayResource(R.array.tabletInterface),
|
||||||
|
entryValues = stringArrayResource(R.array.tabletInterface_value),
|
||||||
|
onValueChange = { ThemeConfig.tabletInterface = it }
|
||||||
|
)
|
||||||
|
DropdownListSettingItem(
|
||||||
|
title = stringResource(R.string.nav_label_mode),
|
||||||
|
selectedValue = ThemeConfig.labelVisibilityMode,
|
||||||
|
displayEntries = stringArrayResource(R.array.label_vis_mode),
|
||||||
|
entryValues = stringArrayResource(R.array.label_vis_mode_value),
|
||||||
|
onValueChange = { ThemeConfig.labelVisibilityMode = it }
|
||||||
|
)
|
||||||
|
DropdownListSettingItem(
|
||||||
|
title = stringResource(R.string.default_home_page),
|
||||||
|
selectedValue = ThemeConfig.defaultHomePage,
|
||||||
|
displayEntries = stringArrayResource(R.array.default_home_page),
|
||||||
|
entryValues = stringArrayResource(R.array.default_home_page_value),
|
||||||
|
onValueChange = { ThemeConfig.defaultHomePage = it }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
SplicedColumnGroup(title = "Compose 相关") {
|
SplicedColumnGroup(title = "Compose 相关") {
|
||||||
SwitchSettingItem(
|
SwitchSettingItem(
|
||||||
@@ -117,7 +364,7 @@ fun ThemeConfigScreen(
|
|||||||
description = if (hasLightBg) stringResource(R.string.click_to_delete) else stringResource(
|
description = if (hasLightBg) stringResource(R.string.click_to_delete) else stringResource(
|
||||||
R.string.select_image
|
R.string.select_image
|
||||||
),
|
),
|
||||||
onClick = { manageKey = Pair(PreferKey.bgImage, false) }
|
onClick = { manageKey = false }
|
||||||
)
|
)
|
||||||
|
|
||||||
if (hasLightBg) {
|
if (hasLightBg) {
|
||||||
@@ -141,7 +388,7 @@ fun ThemeConfigScreen(
|
|||||||
description = if (hasDarkBg) stringResource(R.string.click_to_delete) else stringResource(
|
description = if (hasDarkBg) stringResource(R.string.click_to_delete) else stringResource(
|
||||||
R.string.select_image
|
R.string.select_image
|
||||||
),
|
),
|
||||||
onClick = { manageKey = Pair(PreferKey.bgImageN, true) }
|
onClick = { manageKey = true }
|
||||||
)
|
)
|
||||||
|
|
||||||
if (hasDarkBg) {
|
if (hasDarkBg) {
|
||||||
@@ -160,11 +407,394 @@ fun ThemeConfigScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
manageKey?.let { (key, isDark) ->
|
if (showRestartDialog) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { showRestartDialog = false },
|
||||||
|
title = { Text(stringResource(R.string.restart_required_message)) },
|
||||||
|
confirmButton = {
|
||||||
|
OutlinedButton(onClick = {
|
||||||
|
showRestartDialog = false
|
||||||
|
Handler(Looper.getMainLooper()).postDelayed({
|
||||||
|
context.restart()
|
||||||
|
}, 100)
|
||||||
|
}) {
|
||||||
|
Text(stringResource(R.string.ok))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
showRestartDialog = false
|
||||||
|
context.toastOnUi(R.string.restart_later_message)
|
||||||
|
}) {
|
||||||
|
Text(stringResource(R.string.cancel))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
manageKey?.let { isDark ->
|
||||||
BackgroundImageManageSheet(
|
BackgroundImageManageSheet(
|
||||||
preferenceKey = key,
|
|
||||||
isDarkTheme = isDark,
|
isDarkTheme = isDark,
|
||||||
onDismissRequest = { manageKey = null }
|
onDismissRequest = { manageKey = null }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (showColorPicker) {
|
||||||
|
ColorPickerSheet(
|
||||||
|
initialColor = primaryColorValue.value,
|
||||||
|
onDismissRequest = { showColorPicker = false },
|
||||||
|
onColorSelected = { color ->
|
||||||
|
primaryColorValue.value = color
|
||||||
|
ThemeConfig.cPrimary = color
|
||||||
|
ThemeStore.editTheme(context)
|
||||||
|
.primaryColor(color)
|
||||||
|
.apply()
|
||||||
|
DynamicColors.applyToActivitiesIfAvailable(
|
||||||
|
context.applicationContext as android.app.Application,
|
||||||
|
DynamicColorsOptions.Builder()
|
||||||
|
.setContentBasedSource(context.primaryColor)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showLauncherIconPicker) {
|
||||||
|
LauncherIconPickerSheet(
|
||||||
|
selectedValue = ThemeConfig.launcherIcon,
|
||||||
|
onDismissRequest = { showLauncherIconPicker = false },
|
||||||
|
onValueChange = {
|
||||||
|
ThemeConfig.launcherIcon = it
|
||||||
|
LauncherIconHelp.changeIcon(it)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
|
@Composable
|
||||||
|
fun ThemeModeSelector(
|
||||||
|
selectedMode: String,
|
||||||
|
onModeSelected: (String) -> Unit
|
||||||
|
) {
|
||||||
|
val modes = listOf(
|
||||||
|
Triple("0", stringResource(R.string.flow_sys), Icons.Filled.BrightnessMedium),
|
||||||
|
Triple("1", stringResource(R.string.light_mode), Icons.Filled.LightMode),
|
||||||
|
Triple("2", stringResource(R.string.dark_mode), Icons.Filled.DarkMode)
|
||||||
|
)
|
||||||
|
|
||||||
|
val selectedIndex = modes.indexOfFirst { it.first == selectedMode }
|
||||||
|
.coerceAtLeast(0)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(
|
||||||
|
ButtonGroupDefaults.ConnectedSpaceBetween
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
val modifiers = listOf(Modifier.weight(1.2f), Modifier.weight(1f), Modifier.weight(1f))
|
||||||
|
|
||||||
|
modes.forEachIndexed { index, (value, label, icon) ->
|
||||||
|
|
||||||
|
ToggleButton(
|
||||||
|
checked = selectedIndex == index,
|
||||||
|
onCheckedChange = { onModeSelected(value) },
|
||||||
|
modifier = modifiers[index]
|
||||||
|
.semantics { role = Role.RadioButton },
|
||||||
|
|
||||||
|
shapes = when (index) {
|
||||||
|
0 -> ButtonGroupDefaults.connectedLeadingButtonShapes()
|
||||||
|
modes.lastIndex -> ButtonGroupDefaults.connectedTrailingButtonShapes()
|
||||||
|
else -> ButtonGroupDefaults.connectedMiddleButtonShapes()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(Modifier.size(ToggleButtonDefaults.IconSpacing))
|
||||||
|
|
||||||
|
Text(text = label)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ThemeColorSelector(
|
||||||
|
context: Context,
|
||||||
|
themes: List<Pair<String, String>>,
|
||||||
|
selectedTheme: String,
|
||||||
|
isDark: Boolean,
|
||||||
|
isAmoled: Boolean,
|
||||||
|
paletteStyle: String?,
|
||||||
|
onThemeSelected: (String) -> Unit
|
||||||
|
) {
|
||||||
|
LazyRow(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
items(themes) { (label, value) ->
|
||||||
|
ThemeColorButton(
|
||||||
|
context = context,
|
||||||
|
label = label,
|
||||||
|
value = value,
|
||||||
|
isSelected = selectedTheme == value,
|
||||||
|
isDark = isDark,
|
||||||
|
isAmoled = isAmoled,
|
||||||
|
paletteStyle = paletteStyle,
|
||||||
|
onClick = { onThemeSelected(value) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun ThemeColorButton(
|
||||||
|
context: Context,
|
||||||
|
label: String,
|
||||||
|
value: String,
|
||||||
|
isSelected: Boolean,
|
||||||
|
isDark: Boolean,
|
||||||
|
isAmoled: Boolean,
|
||||||
|
paletteStyle: String?,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
val colors = getThemeColorPalette(context, value, isDark, isAmoled, paletteStyle)
|
||||||
|
val borderWidth by animateDpAsState(
|
||||||
|
targetValue = if (isSelected) 2.dp else 0.dp,
|
||||||
|
label = "borderWidth"
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
onClick = onClick,
|
||||||
|
modifier = Modifier.size(64.dp),
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
border = if (isSelected) BorderStroke(
|
||||||
|
borderWidth,
|
||||||
|
MaterialTheme.colorScheme.primary
|
||||||
|
) else null,
|
||||||
|
colors = CardDefaults.cardColors(containerColor = colors.surfaceContainer)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.size(48.dp)
|
||||||
|
) {
|
||||||
|
Canvas(
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) {
|
||||||
|
drawArc(
|
||||||
|
color = colors.secondary,
|
||||||
|
startAngle = -90f,
|
||||||
|
sweepAngle = 180f,
|
||||||
|
useCenter = true,
|
||||||
|
size = size
|
||||||
|
)
|
||||||
|
|
||||||
|
drawArc(
|
||||||
|
color = colors.tertiary,
|
||||||
|
startAngle = 90f,
|
||||||
|
sweepAngle = 180f,
|
||||||
|
useCenter = true,
|
||||||
|
size = size
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(24.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(colors.primary)
|
||||||
|
.align(Alignment.Center)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSelected) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(40.dp)
|
||||||
|
.clip(RoundedCornerShape(12.dp)),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Check,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.onPrimary,
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ThemeCard(
|
||||||
|
context: Context,
|
||||||
|
value: String,
|
||||||
|
isDark: Boolean,
|
||||||
|
isAmoled: Boolean,
|
||||||
|
paletteStyle: String?
|
||||||
|
) {
|
||||||
|
val colors = getThemeColors(context, value, isDark, isAmoled, paletteStyle)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(128.dp)
|
||||||
|
.height(256.dp),
|
||||||
|
shape = MaterialTheme.shapes.large,
|
||||||
|
border = BorderStroke(1.dp, colors.primary),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = colors.surface)
|
||||||
|
) {
|
||||||
|
ConstraintLayout(
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) {
|
||||||
|
val (colorTop, colorBook, colorBottom) = createRefs()
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(width = 48.dp, height = 16.dp)
|
||||||
|
.constrainAs(colorTop) {
|
||||||
|
top.linkTo(parent.top, margin = 12.dp)
|
||||||
|
start.linkTo(parent.start, margin = 12.dp)
|
||||||
|
}
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
.background(colors.onSurfaceVariant)
|
||||||
|
)
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(width = 56.dp, height = 80.dp)
|
||||||
|
.constrainAs(colorBook) {
|
||||||
|
top.linkTo(colorTop.bottom, margin = 8.dp)
|
||||||
|
start.linkTo(colorTop.start)
|
||||||
|
}
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
.background(colors.secondaryContainer)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.TopEnd)
|
||||||
|
.padding(4.dp)
|
||||||
|
.size(width = 16.dp, height = 12.dp)
|
||||||
|
.clip(RoundedCornerShape(4.dp))
|
||||||
|
.background(colors.secondary)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(40.dp)
|
||||||
|
.constrainAs(colorBottom) {
|
||||||
|
bottom.linkTo(parent.bottom)
|
||||||
|
start.linkTo(parent.start, margin = 4.dp)
|
||||||
|
end.linkTo(parent.end, margin = 4.dp)
|
||||||
|
}
|
||||||
|
.background(colors.surfaceContainer)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.Center)
|
||||||
|
.size(20.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(colors.primary)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ThemeColorPalette(
|
||||||
|
val primary: Color,
|
||||||
|
val secondary: Color,
|
||||||
|
val tertiary: Color,
|
||||||
|
val surfaceContainer: Color
|
||||||
|
)
|
||||||
|
|
||||||
|
data class ThemeColors(
|
||||||
|
val primary: Color,
|
||||||
|
val secondary: Color,
|
||||||
|
val tertiary: Color,
|
||||||
|
val surface: Color,
|
||||||
|
val surfaceContainer: Color,
|
||||||
|
val secondaryContainer: Color,
|
||||||
|
val onSurface: Color,
|
||||||
|
val onSurfaceVariant: Color
|
||||||
|
)
|
||||||
|
|
||||||
|
@SuppressLint("ResourceType")
|
||||||
|
private fun getThemeColorPalette(
|
||||||
|
context: Context,
|
||||||
|
value: String,
|
||||||
|
isDark: Boolean,
|
||||||
|
isAmoled: Boolean,
|
||||||
|
paletteStyle: String?
|
||||||
|
): ThemeColorPalette {
|
||||||
|
val appThemeMode = ThemeResolver.resolveThemeMode(value)
|
||||||
|
val colorScheme = ThemeManager.getColorScheme(
|
||||||
|
context = context,
|
||||||
|
mode = appThemeMode,
|
||||||
|
darkTheme = isDark,
|
||||||
|
isAmoled = isAmoled,
|
||||||
|
paletteStyle = paletteStyle
|
||||||
|
)
|
||||||
|
|
||||||
|
return ThemeColorPalette(
|
||||||
|
primary = colorScheme.primary,
|
||||||
|
secondary = colorScheme.secondaryContainer,
|
||||||
|
tertiary = colorScheme.tertiaryContainer,
|
||||||
|
surfaceContainer = colorScheme.surfaceContainer
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("ResourceType")
|
||||||
|
private fun getThemeColors(
|
||||||
|
context: Context,
|
||||||
|
value: String,
|
||||||
|
isDark: Boolean,
|
||||||
|
isAmoled: Boolean,
|
||||||
|
paletteStyle: String?
|
||||||
|
): ThemeColors {
|
||||||
|
val appThemeMode = ThemeResolver.resolveThemeMode(value)
|
||||||
|
val colorScheme = ThemeManager.getColorScheme(
|
||||||
|
context = context,
|
||||||
|
mode = appThemeMode,
|
||||||
|
darkTheme = isDark,
|
||||||
|
isAmoled = isAmoled,
|
||||||
|
paletteStyle = paletteStyle
|
||||||
|
)
|
||||||
|
|
||||||
|
return ThemeColors(
|
||||||
|
primary = colorScheme.primary,
|
||||||
|
secondary = colorScheme.secondary,
|
||||||
|
tertiary = colorScheme.tertiary,
|
||||||
|
surface = colorScheme.surface,
|
||||||
|
surfaceContainer = colorScheme.surfaceContainer,
|
||||||
|
secondaryContainer = colorScheme.secondaryContainer,
|
||||||
|
onSurface = colorScheme.onSurface,
|
||||||
|
onSurfaceVariant = colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
}
|
}
|
||||||
@@ -21,7 +21,7 @@ class ThemeConfigViewModel : ViewModel() {
|
|||||||
*/
|
*/
|
||||||
suspend fun setBackgroundFromUri(
|
suspend fun setBackgroundFromUri(
|
||||||
uri: Uri,
|
uri: Uri,
|
||||||
preferenceKey: String
|
isDarkTheme: Boolean
|
||||||
) = withContext(Dispatchers.IO) {
|
) = withContext(Dispatchers.IO) {
|
||||||
kotlin.runCatching {
|
kotlin.runCatching {
|
||||||
val fileDoc = FileDoc.fromUri(uri, false)
|
val fileDoc = FileDoc.fromUri(uri, false)
|
||||||
@@ -33,6 +33,7 @@ class ThemeConfigViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
val fileName = "$md5.$suffix"
|
val fileName = "$md5.$suffix"
|
||||||
|
|
||||||
|
val preferenceKey = if (isDarkTheme) PreferKey.bgImageN else PreferKey.bgImage
|
||||||
val folder = File(appCtx.externalFiles, preferenceKey)
|
val folder = File(appCtx.externalFiles, preferenceKey)
|
||||||
val file = File(folder, fileName)
|
val file = File(folder, fileName)
|
||||||
|
|
||||||
@@ -45,24 +46,24 @@ class ThemeConfigViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateBackgroundPath(preferenceKey, file.absolutePath)
|
updateBackgroundPath(isDarkTheme, file.absolutePath)
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
it.printStackTrace()
|
it.printStackTrace()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeBackground(preferenceKey: String) {
|
fun removeBackground(isDarkTheme: Boolean) {
|
||||||
updateBackgroundPath(preferenceKey, null)
|
updateBackgroundPath(isDarkTheme, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新背景路径并清理旧文件
|
* 更新背景路径并清理旧文件
|
||||||
*/
|
*/
|
||||||
private fun updateBackgroundPath(preferenceKey: String, newPath: String?) {
|
private fun updateBackgroundPath(isDarkTheme: Boolean, newPath: String?) {
|
||||||
val oldPath = when (preferenceKey) {
|
val oldPath = if (isDarkTheme) {
|
||||||
PreferKey.bgImage -> ThemeConfig.bgImageLight
|
ThemeConfig.bgImageDark
|
||||||
PreferKey.bgImageN -> ThemeConfig.bgImageDark
|
} else {
|
||||||
else -> null
|
ThemeConfig.bgImageLight
|
||||||
}
|
}
|
||||||
|
|
||||||
if (oldPath != newPath && oldPath != null) {
|
if (oldPath != newPath && oldPath != null) {
|
||||||
@@ -72,9 +73,10 @@ class ThemeConfigViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
when (preferenceKey) {
|
if (isDarkTheme) {
|
||||||
PreferKey.bgImage -> ThemeConfig.bgImageLight = newPath
|
ThemeConfig.bgImageDark = newPath
|
||||||
PreferKey.bgImageN -> ThemeConfig.bgImageDark = newPath
|
} else {
|
||||||
|
ThemeConfig.bgImageLight = newPath
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -198,14 +198,24 @@ fun BookshelfItem(
|
|||||||
@Composable
|
@Composable
|
||||||
fun BookGroupCover(
|
fun BookGroupCover(
|
||||||
books: List<Book>,
|
books: List<Book>,
|
||||||
|
coverPath: String? = null,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.aspectRatio(5f / 7f)
|
.aspectRatio(5f / 7f)
|
||||||
.clip(RoundedCornerShape(4.dp))
|
.clip(RoundedCornerShape(4.dp))
|
||||||
.background(MaterialTheme.colorScheme.surfaceVariant)
|
.background(MaterialTheme.colorScheme.surfaceContainer)
|
||||||
) {
|
) {
|
||||||
|
if (coverPath != null) {
|
||||||
|
BookCover(
|
||||||
|
name = "",
|
||||||
|
author = "",
|
||||||
|
path = coverPath,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
ignoreUseDefaultCover = true
|
||||||
|
)
|
||||||
|
} else {
|
||||||
Column(modifier = Modifier.fillMaxSize()) {
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
Row(modifier = Modifier.weight(1f)) {
|
Row(modifier = Modifier.weight(1f)) {
|
||||||
Box(
|
Box(
|
||||||
@@ -274,6 +284,7 @@ fun BookGroupCover(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BookGroupItemGrid(
|
fun BookGroupItemGrid(
|
||||||
@@ -292,7 +303,7 @@ fun BookGroupItemGrid(
|
|||||||
isGrid = true,
|
isGrid = true,
|
||||||
gridStyle = gridStyle,
|
gridStyle = gridStyle,
|
||||||
isCompact = false,
|
isCompact = false,
|
||||||
cover = { BookGroupCover(books = previewBooks, modifier = it) },
|
cover = { BookGroupCover(books = previewBooks, coverPath = group.cover, modifier = it) },
|
||||||
title = group.groupName,
|
title = group.groupName,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
titleSmallFont = titleSmallFont,
|
titleSmallFont = titleSmallFont,
|
||||||
@@ -321,7 +332,7 @@ fun BookGroupItemList(
|
|||||||
isGrid = false,
|
isGrid = false,
|
||||||
gridStyle = 0,
|
gridStyle = 0,
|
||||||
isCompact = isCompact,
|
isCompact = isCompact,
|
||||||
cover = { BookGroupCover(books = previewBooks, modifier = it) },
|
cover = { BookGroupCover(books = previewBooks, coverPath = group.cover, modifier = it) },
|
||||||
title = group.groupName,
|
title = group.groupName,
|
||||||
titleSmallFont = titleSmallFont,
|
titleSmallFont = titleSmallFont,
|
||||||
titleCenter = titleCenter,
|
titleCenter = titleCenter,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import android.content.res.Configuration
|
|||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.animation.AnimatedContent
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@@ -162,6 +163,17 @@ fun BookshelfScreen(
|
|||||||
pageCount = { uiState.groups.size }
|
pageCount = { uiState.groups.size }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
LaunchedEffect(uiState.groups) {
|
||||||
|
if (uiState.groups.isNotEmpty()) {
|
||||||
|
val savedGroupId = BookshelfConfig.saveTabPosition
|
||||||
|
val savedGroupIndex = uiState.groups.indexOfFirst { it.groupId == savedGroupId }
|
||||||
|
if (savedGroupIndex >= 0 && savedGroupIndex != pagerState.currentPage) {
|
||||||
|
viewModel.changeGroup(savedGroupId)
|
||||||
|
pagerState.scrollToPage(savedGroupIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(pagerState) {
|
LaunchedEffect(pagerState) {
|
||||||
snapshotFlow { pagerState.currentPage }
|
snapshotFlow { pagerState.currentPage }
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
@@ -348,7 +360,11 @@ fun BookshelfScreen(
|
|||||||
enabled = pullToRefreshEnabled
|
enabled = pullToRefreshEnabled
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
if (bookGroupStyle == 2 && isInFolderRoot) {
|
AnimatedContent(
|
||||||
|
targetState = isInFolderRoot,
|
||||||
|
label = "FolderTransition"
|
||||||
|
) { isRoot ->
|
||||||
|
if (bookGroupStyle == 2 && isRoot) {
|
||||||
FastScrollLazyVerticalGrid(
|
FastScrollLazyVerticalGrid(
|
||||||
columns = GridCells.Fixed(bookshelfLayoutGrid.coerceAtLeast(1)),
|
columns = GridCells.Fixed(bookshelfLayoutGrid.coerceAtLeast(1)),
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
@@ -362,11 +378,14 @@ fun BookshelfScreen(
|
|||||||
horizontalArrangement = Arrangement.spacedBy(if (bookshelfLayoutMode != 0) 8.dp else 0.dp),
|
horizontalArrangement = Arrangement.spacedBy(if (bookshelfLayoutMode != 0) 8.dp else 0.dp),
|
||||||
showFastScroll = BookshelfConfig.showBookshelfFastScroller
|
showFastScroll = BookshelfConfig.showBookshelfFastScroller
|
||||||
) {
|
) {
|
||||||
itemsIndexed(uiState.groups, key = { _, it -> it.groupId }) { index, group ->
|
itemsIndexed(
|
||||||
|
uiState.groups,
|
||||||
|
key = { _, it -> it.groupId }) { index, group ->
|
||||||
if (bookshelfLayoutMode == 0) {
|
if (bookshelfLayoutMode == 0) {
|
||||||
BookGroupItemList(
|
BookGroupItemList(
|
||||||
group = group,
|
group = group,
|
||||||
previewBooks = uiState.groupPreviews[group.groupId] ?: emptyList(),
|
previewBooks = uiState.groupPreviews[group.groupId]
|
||||||
|
?: emptyList(),
|
||||||
isCompact = BookshelfConfig.bookshelfLayoutCompact,
|
isCompact = BookshelfConfig.bookshelfLayoutCompact,
|
||||||
titleSmallFont = BookshelfConfig.bookshelfTitleSmallFont,
|
titleSmallFont = BookshelfConfig.bookshelfTitleSmallFont,
|
||||||
titleCenter = BookshelfConfig.bookshelfTitleCenter,
|
titleCenter = BookshelfConfig.bookshelfTitleCenter,
|
||||||
@@ -380,7 +399,8 @@ fun BookshelfScreen(
|
|||||||
} else {
|
} else {
|
||||||
BookGroupItemGrid(
|
BookGroupItemGrid(
|
||||||
group = group,
|
group = group,
|
||||||
previewBooks = uiState.groupPreviews[group.groupId] ?: emptyList(),
|
previewBooks = uiState.groupPreviews[group.groupId]
|
||||||
|
?: emptyList(),
|
||||||
gridStyle = BookshelfConfig.bookshelfGridLayout,
|
gridStyle = BookshelfConfig.bookshelfGridLayout,
|
||||||
titleSmallFont = BookshelfConfig.bookshelfTitleSmallFont,
|
titleSmallFont = BookshelfConfig.bookshelfTitleSmallFont,
|
||||||
titleCenter = BookshelfConfig.bookshelfTitleCenter,
|
titleCenter = BookshelfConfig.bookshelfTitleCenter,
|
||||||
@@ -420,6 +440,7 @@ fun BookshelfScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
PullToRefreshDefaults.LoadingIndicator(
|
PullToRefreshDefaults.LoadingIndicator(
|
||||||
state = pullToRefreshState,
|
state = pullToRefreshState,
|
||||||
|
|||||||
@@ -138,11 +138,18 @@ class BookshelfViewModel(
|
|||||||
if (BookshelfConfig.bookGroupStyle in 2..3) {
|
if (BookshelfConfig.bookGroupStyle in 2..3) {
|
||||||
groups.associate { group ->
|
groups.associate { group ->
|
||||||
val groupBooks = if (group.groupId == BookGroup.IdAll) {
|
val groupBooks = if (group.groupId == BookGroup.IdAll) {
|
||||||
allBooks.take(4)
|
allBooks
|
||||||
} else {
|
} else {
|
||||||
allBooks.filter { (it.group and group.groupId) != 0L }.take(4)
|
allBooks.filter { (it.group and group.groupId) != 0L }
|
||||||
}
|
}
|
||||||
group.groupId to groupBooks
|
val sortedBooks = sortBooks(groupBooks, group)
|
||||||
|
val booksWithCover = sortedBooks.filter { it.getDisplayCover() != null }
|
||||||
|
val result = if (booksWithCover.size >= 4) {
|
||||||
|
booksWithCover.take(4)
|
||||||
|
} else {
|
||||||
|
(booksWithCover + sortedBooks.filter { it.getDisplayCover() == null }).take(4)
|
||||||
|
}
|
||||||
|
group.groupId to result
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
emptyMap()
|
emptyMap()
|
||||||
|
|||||||
@@ -1,39 +1,91 @@
|
|||||||
package io.legado.app.ui.widget.components
|
package io.legado.app.ui.widget.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.RowScope
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.FabPosition
|
import androidx.compose.material3.FabPosition
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.ScaffoldDefaults
|
import androidx.compose.material3.ScaffoldDefaults
|
||||||
|
import androidx.compose.material3.TopAppBarColors
|
||||||
|
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||||
import androidx.compose.material3.contentColorFor
|
import androidx.compose.material3.contentColorFor
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import dev.chrisbanes.haze.HazeState
|
||||||
|
import io.legado.app.ui.config.themeConfig.ThemeConfig
|
||||||
|
import io.legado.app.ui.theme.responsiveHazeEffect
|
||||||
|
import io.legado.app.ui.theme.responsiveHazeSource
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun AppScaffold(
|
fun AppScaffold(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
title: (@Composable () -> Unit)? = null,
|
||||||
|
subtitle: (@Composable () -> Unit)? = null,
|
||||||
|
scrollBehavior: TopAppBarScrollBehavior? = null,
|
||||||
|
navigationIcon: @Composable () -> Unit = {},
|
||||||
|
actions: @Composable RowScope.() -> Unit = {},
|
||||||
|
colors: TopAppBarColors = GlassTopAppBarDefaults.glassColors(),
|
||||||
topBar: @Composable () -> Unit = {},
|
topBar: @Composable () -> Unit = {},
|
||||||
bottomBar: @Composable () -> Unit = {},
|
bottomBar: @Composable () -> Unit = {},
|
||||||
snackbarHost: @Composable () -> Unit = {},
|
snackbarHost: @Composable () -> Unit = {},
|
||||||
floatingActionButton: @Composable () -> Unit = {},
|
floatingActionButton: @Composable () -> Unit = {},
|
||||||
floatingActionButtonPosition: FabPosition = FabPosition.End,
|
floatingActionButtonPosition: FabPosition = FabPosition.End,
|
||||||
containerColor: Color = Color.Transparent,
|
|
||||||
contentColor: Color = contentColorFor(MaterialTheme.colorScheme.background),
|
contentColor: Color = contentColorFor(MaterialTheme.colorScheme.background),
|
||||||
contentWindowInsets: WindowInsets = ScaffoldDefaults.contentWindowInsets,
|
contentWindowInsets: WindowInsets = ScaffoldDefaults.contentWindowInsets,
|
||||||
content: @Composable (PaddingValues) -> Unit
|
content: @Composable (PaddingValues) -> Unit
|
||||||
) {
|
) {
|
||||||
|
val isDark = isSystemInDarkTheme()
|
||||||
|
val hasImageBg = ThemeConfig.hasImageBg(isDark)
|
||||||
|
val hazeState = remember { HazeState() }
|
||||||
|
|
||||||
|
val containerColor = if (hasImageBg) {
|
||||||
|
Color.Transparent
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.background
|
||||||
|
}
|
||||||
|
|
||||||
|
val finalTopBar = @Composable {
|
||||||
|
Box(modifier = Modifier.responsiveHazeEffect(state = hazeState)) {
|
||||||
|
if (title != null) {
|
||||||
|
GlassMediumFlexibleTopAppBar(
|
||||||
|
title = title,
|
||||||
|
subtitle = subtitle,
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
navigationIcon = navigationIcon,
|
||||||
|
actions = actions,
|
||||||
|
colors = colors
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
topBar()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
topBar = topBar,
|
topBar = finalTopBar,
|
||||||
bottomBar = bottomBar,
|
bottomBar = bottomBar,
|
||||||
snackbarHost = snackbarHost,
|
snackbarHost = snackbarHost,
|
||||||
floatingActionButton = floatingActionButton,
|
floatingActionButton = floatingActionButton,
|
||||||
floatingActionButtonPosition = floatingActionButtonPosition,
|
floatingActionButtonPosition = floatingActionButtonPosition,
|
||||||
containerColor = containerColor,
|
containerColor = containerColor,
|
||||||
contentColor = contentColor,
|
contentColor = contentColor,
|
||||||
contentWindowInsets = contentWindowInsets,
|
contentWindowInsets = contentWindowInsets
|
||||||
content = content
|
) { paddingValues ->
|
||||||
)
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.responsiveHazeSource(hazeState)
|
||||||
|
) {
|
||||||
|
content(paddingValues)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+1
-4
@@ -75,10 +75,7 @@ object GlassTopAppBarDefaults {
|
|||||||
|
|
||||||
return TopAppBarDefaults.topAppBarColors(
|
return TopAppBarDefaults.topAppBarColors(
|
||||||
containerColor = containerColor,
|
containerColor = containerColor,
|
||||||
scrolledContainerColor = scrolledContainerColor,
|
scrolledContainerColor = scrolledContainerColor
|
||||||
navigationIconContentColor = MaterialTheme.colorScheme.onSurface,
|
|
||||||
titleContentColor = MaterialTheme.colorScheme.onSurface,
|
|
||||||
actionIconContentColor = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.ColumnScope
|
|||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
@@ -14,7 +15,6 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.legado.app.ui.widget.components.card.GlassCard
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Settings Group Container by https://github.com/wxxsfxyzm/InstallerX-Revived
|
* Settings Group Container by https://github.com/wxxsfxyzm/InstallerX-Revived
|
||||||
@@ -29,17 +29,17 @@ fun SplicedColumnGroup(
|
|||||||
title: String = "",
|
title: String = "",
|
||||||
content: @Composable ColumnScope.() -> Unit,
|
content: @Composable ColumnScope.() -> Unit,
|
||||||
) {
|
) {
|
||||||
Column(modifier = modifier.padding(top = 4.dp, bottom = 12.dp)) {
|
Column(modifier = modifier.padding(top = 8.dp, bottom = 16.dp)) {
|
||||||
if (title.isNotEmpty()) {
|
if (title.isNotEmpty()) {
|
||||||
Text(
|
Text(
|
||||||
text = title,
|
text = title,
|
||||||
style = MaterialTheme.typography.labelLarge,
|
style = MaterialTheme.typography.labelLarge,
|
||||||
color = MaterialTheme.colorScheme.primary,
|
color = MaterialTheme.colorScheme.primary,
|
||||||
modifier = Modifier.padding(start = 16.dp, bottom = 8.dp, top = 8.dp)
|
modifier = Modifier.padding(start = 16.dp, bottom = 8.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
GlassCard(
|
Card(
|
||||||
shape = RoundedCornerShape(16.dp),
|
shape = RoundedCornerShape(16.dp),
|
||||||
colors = CardDefaults.cardColors(containerColor = Color.Transparent),
|
colors = CardDefaults.cardColors(containerColor = Color.Transparent),
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
|||||||
@@ -26,11 +26,18 @@ fun GlassCard(
|
|||||||
) {
|
) {
|
||||||
val containerAlpha = ThemeConfig.containerOpacity / 100f
|
val containerAlpha = ThemeConfig.containerOpacity / 100f
|
||||||
|
|
||||||
val finalColors = colors ?: CardDefaults.cardColors(
|
val baseColors = colors ?: CardDefaults.cardColors(
|
||||||
containerColor = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = containerAlpha),
|
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||||
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
|
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val finalColors = CardDefaults.cardColors(
|
||||||
|
containerColor = baseColors.containerColor.copy(alpha = containerAlpha),
|
||||||
|
contentColor = baseColors.contentColor,
|
||||||
|
disabledContainerColor = baseColors.disabledContainerColor.copy(alpha = containerAlpha),
|
||||||
|
disabledContentColor = baseColors.disabledContentColor
|
||||||
|
)
|
||||||
|
|
||||||
if (onClick != null) {
|
if (onClick != null) {
|
||||||
Card(
|
Card(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
|
|||||||
@@ -54,17 +54,25 @@ fun BookCover(
|
|||||||
badgeText: String? = null,
|
badgeText: String? = null,
|
||||||
showBadgeDot: Boolean = false,
|
showBadgeDot: Boolean = false,
|
||||||
sourceOrigin: String? = null,
|
sourceOrigin: String? = null,
|
||||||
onLoadFinish: (() -> Unit)? = null
|
onLoadFinish: (() -> Unit)? = null,
|
||||||
|
ignoreUseDefaultCover: Boolean = false
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val isNight = isSystemInDarkTheme()
|
val isNight = isSystemInDarkTheme()
|
||||||
|
|
||||||
val useDefault = CoverConfig.useDefaultCover
|
val useDefault = !ignoreUseDefaultCover && CoverConfig.useDefaultCover
|
||||||
val finalPath = if (useDefault) null else path
|
val finalPath = if (useDefault) null else path
|
||||||
|
|
||||||
// 获取随机封面路径(字符串),而非直接获取 Drawable,避免主线程解码大图
|
// 获取随机封面路径(字符串),增加配置项作为 Key,确保设置更改后立即刷新
|
||||||
val randomPath = remember(name, author, path, isNight) {
|
val randomPath = remember(
|
||||||
BookCover.getRandomDefaultPath(seed = name ?: author ?: path ?: "")
|
name, author, path, isNight,
|
||||||
|
CoverConfig.defaultCover,
|
||||||
|
CoverConfig.defaultCoverDark
|
||||||
|
) {
|
||||||
|
BookCover.getRandomDefaultPath(
|
||||||
|
seed = name ?: author ?: path ?: "",
|
||||||
|
isNight = isNight
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否有自定义随机封面设置
|
// 检查是否有自定义随机封面设置
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ fun ColorPickerSheet(
|
|||||||
HsvColorPicker(
|
HsvColorPicker(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(250.dp)
|
.height(240.dp)
|
||||||
.padding(10.dp),
|
.padding(10.dp),
|
||||||
controller = controller,
|
controller = controller,
|
||||||
initialColor = Color(initialColor),
|
initialColor = Color(initialColor),
|
||||||
@@ -122,34 +122,6 @@ fun ColorPickerSheet(
|
|||||||
controller = controller,
|
controller = controller,
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 10.dp)
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(40.dp)
|
|
||||||
.clip(RoundedCornerShape(8.dp))
|
|
||||||
.background(currentColor)
|
|
||||||
.border(
|
|
||||||
1.dp,
|
|
||||||
MaterialTheme.colorScheme.outlineVariant,
|
|
||||||
RoundedCornerShape(8.dp)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.width(12.dp))
|
|
||||||
Text(
|
|
||||||
text = "#${Integer.toHexString(currentColor.toArgb()).uppercase()}",
|
|
||||||
style = MaterialTheme.typography.bodyLarge
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
|
|
||||||
LazyVerticalGrid(
|
LazyVerticalGrid(
|
||||||
columns = GridCells.Adaptive(36.dp),
|
columns = GridCells.Adaptive(36.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
@@ -176,7 +148,30 @@ fun ColorPickerSheet(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 10.dp)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(40.dp)
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
.background(currentColor)
|
||||||
|
.border(
|
||||||
|
1.dp,
|
||||||
|
MaterialTheme.colorScheme.outlineVariant,
|
||||||
|
RoundedCornerShape(8.dp)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
Text(
|
||||||
|
text = "#${Integer.toHexString(currentColor.toArgb()).uppercase()}",
|
||||||
|
style = MaterialTheme.typography.bodyLarge
|
||||||
|
)
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
@@ -196,3 +191,4 @@ fun ColorPickerSheet(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
-1
@@ -145,7 +145,6 @@ fun <T> BatchImportDialog(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.heightIn(max = LocalConfiguration.current.screenHeightDp.dp * 0.8f),
|
.heightIn(max = LocalConfiguration.current.screenHeightDp.dp * 0.8f),
|
||||||
containerColor = Color.Transparent,
|
|
||||||
topBar = {
|
topBar = {
|
||||||
CenterAlignedTopAppBar(
|
CenterAlignedTopAppBar(
|
||||||
title = {
|
title = {
|
||||||
|
|||||||
@@ -35,9 +35,6 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.zIndex
|
import androidx.compose.ui.zIndex
|
||||||
import dev.chrisbanes.haze.HazeState
|
|
||||||
import io.legado.app.ui.theme.responsiveHazeEffect
|
|
||||||
import io.legado.app.ui.theme.responsiveHazeSource
|
|
||||||
import io.legado.app.ui.widget.components.AppScaffold
|
import io.legado.app.ui.widget.components.AppScaffold
|
||||||
import io.legado.app.ui.widget.components.GlassTopAppBarDefaults
|
import io.legado.app.ui.widget.components.GlassTopAppBarDefaults
|
||||||
import io.legado.app.ui.widget.components.SelectionActions
|
import io.legado.app.ui.widget.components.SelectionActions
|
||||||
@@ -84,7 +81,6 @@ fun <T> ListScaffold(
|
|||||||
content: @Composable (PaddingValues) -> Unit
|
content: @Composable (PaddingValues) -> Unit
|
||||||
) {
|
) {
|
||||||
val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
|
val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
|
||||||
val hazeState = remember { HazeState() }
|
|
||||||
|
|
||||||
AppScaffold(
|
AppScaffold(
|
||||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
@@ -96,7 +92,6 @@ fun <T> ListScaffold(
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
topBar = {
|
topBar = {
|
||||||
Box(modifier = Modifier.responsiveHazeEffect(state = hazeState)) {
|
|
||||||
DynamicTopAppBar(
|
DynamicTopAppBar(
|
||||||
title = title,
|
title = title,
|
||||||
subtitle = subtitle,
|
subtitle = subtitle,
|
||||||
@@ -111,14 +106,11 @@ fun <T> ListScaffold(
|
|||||||
dropDownMenuContent = dropDownMenuContent,
|
dropDownMenuContent = dropDownMenuContent,
|
||||||
bottomContent = bottomContent
|
bottomContent = bottomContent
|
||||||
)
|
)
|
||||||
}
|
|
||||||
},
|
},
|
||||||
floatingActionButton = floatingActionButton
|
floatingActionButton = floatingActionButton
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxSize()
|
||||||
.fillMaxSize()
|
|
||||||
.responsiveHazeSource(hazeState)
|
|
||||||
) {
|
) {
|
||||||
content(paddingValues)
|
content(paddingValues)
|
||||||
|
|
||||||
|
|||||||
+1
-2
@@ -50,9 +50,8 @@ fun SliderSettingItem(
|
|||||||
|
|
||||||
SettingItem(
|
SettingItem(
|
||||||
title = title,
|
title = title,
|
||||||
description = description,
|
|
||||||
color = color ?: MaterialTheme.colorScheme.surfaceContainerLow,
|
color = color ?: MaterialTheme.colorScheme.surfaceContainerLow,
|
||||||
option = value.toInt().toString(),
|
option = description,
|
||||||
expanded = expanded,
|
expanded = expanded,
|
||||||
onExpandChange = { expanded = it },
|
onExpandChange = { expanded = it },
|
||||||
expandContent = {
|
expandContent = {
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import io.legado.app.help.config.AppConfig
|
|||||||
import io.legado.app.help.glide.ImageLoader
|
import io.legado.app.help.glide.ImageLoader
|
||||||
import io.legado.app.help.glide.OkHttpModelLoader
|
import io.legado.app.help.glide.OkHttpModelLoader
|
||||||
import io.legado.app.model.BookCover
|
import io.legado.app.model.BookCover
|
||||||
|
import io.legado.app.ui.config.coverConfig.CoverConfig
|
||||||
import io.legado.app.utils.spToPx
|
import io.legado.app.utils.spToPx
|
||||||
import io.legado.app.utils.textHeight
|
import io.legado.app.utils.textHeight
|
||||||
import io.legado.app.utils.themeColor
|
import io.legado.app.utils.themeColor
|
||||||
@@ -46,9 +47,9 @@ class CoverImageView @JvmOverloads constructor(
|
|||||||
private var author: String? = null
|
private var author: String? = null
|
||||||
private var nameHeight = 0f
|
private var nameHeight = 0f
|
||||||
private var authorHeight = 0f
|
private var authorHeight = 0f
|
||||||
private val isNightTheme = AppConfig.isNightTheme
|
private val isNightTheme get() = AppConfig.isNightTheme
|
||||||
val colorKey = if (isNightTheme) BookCover.coverTextColorN else BookCover.coverTextColor
|
private val colorKey get() = if (isNightTheme) CoverConfig.coverTextColorN else CoverConfig.coverTextColor
|
||||||
val shadowKey = if (isNightTheme) BookCover.coverShadowColorN else BookCover.coverShadowColor
|
private val shadowKey get() = if (isNightTheme) CoverConfig.coverShadowColorN else CoverConfig.coverShadowColor
|
||||||
private val namePaint by lazy {
|
private val namePaint by lazy {
|
||||||
val textPaint = TextPaint()
|
val textPaint = TextPaint()
|
||||||
textPaint.typeface = Typeface.DEFAULT_BOLD
|
textPaint.typeface = Typeface.DEFAULT_BOLD
|
||||||
@@ -123,13 +124,14 @@ class CoverImageView @JvmOverloads constructor(
|
|||||||
private fun drawNameAuthor(canvas: Canvas) {
|
private fun drawNameAuthor(canvas: Canvas) {
|
||||||
var startX = width * 0.2f
|
var startX = width * 0.2f
|
||||||
var startY = viewHeight * 0.2f
|
var startY = viewHeight * 0.2f
|
||||||
if (BookCover.drawBookName) {
|
val showName = if (isNightTheme) CoverConfig.coverShowNameN else CoverConfig.coverShowName
|
||||||
|
if (showName) {
|
||||||
name?.toStringArray()?.let { nameChars ->
|
name?.toStringArray()?.let { nameChars ->
|
||||||
val textSize = viewWidth / 8
|
val textSize = viewWidth / 8
|
||||||
namePaint.textSize = textSize
|
namePaint.textSize = textSize
|
||||||
namePaint.strokeWidth = textSize / 8
|
namePaint.strokeWidth = textSize / 8
|
||||||
|
|
||||||
if (BookCover.drawBookShadow) {
|
if (CoverConfig.coverShowShadow) {
|
||||||
namePaint.setShadowLayer(
|
namePaint.setShadowLayer(
|
||||||
4f,
|
4f,
|
||||||
0f,
|
0f,
|
||||||
@@ -141,12 +143,12 @@ class CoverImageView @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
nameChars.forEach { char ->
|
nameChars.forEach { char ->
|
||||||
if (BookCover.drawBookStroke) {
|
if (CoverConfig.coverShowStroke) {
|
||||||
namePaint.color = Color.WHITE
|
namePaint.color = Color.WHITE
|
||||||
namePaint.style = Paint.Style.STROKE
|
namePaint.style = Paint.Style.STROKE
|
||||||
canvas.drawText(char, startX, startY, namePaint)
|
canvas.drawText(char, startX, startY, namePaint)
|
||||||
}
|
}
|
||||||
if (BookCover.coverDefaultColor)
|
if (CoverConfig.coverDefaultColor)
|
||||||
namePaint.color = context.themeColor(com.google.android.material.R.attr.colorSecondary)
|
namePaint.color = context.themeColor(com.google.android.material.R.attr.colorSecondary)
|
||||||
else
|
else
|
||||||
namePaint.color = colorKey
|
namePaint.color = colorKey
|
||||||
@@ -164,14 +166,16 @@ class CoverImageView @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (BookCover.drawBookAuthor) {
|
val showAuthor =
|
||||||
|
if (isNightTheme) CoverConfig.coverShowAuthorN else CoverConfig.coverShowAuthor
|
||||||
|
if (showAuthor) {
|
||||||
author?.toStringArray()?.let { author ->
|
author?.toStringArray()?.let { author ->
|
||||||
authorPaint.textSize = viewWidth / 10
|
authorPaint.textSize = viewWidth / 10
|
||||||
authorPaint.strokeWidth = authorPaint.textSize / 5
|
authorPaint.strokeWidth = authorPaint.textSize / 5
|
||||||
startX = width * 0.8f
|
startX = width * 0.8f
|
||||||
startY = viewHeight * 0.95f - author.size * authorPaint.textHeight
|
startY = viewHeight * 0.95f - author.size * authorPaint.textHeight
|
||||||
startY = maxOf(startY, viewHeight * 0.2f)
|
startY = maxOf(startY, viewHeight * 0.2f)
|
||||||
if (BookCover.drawBookShadow) {
|
if (CoverConfig.coverShowShadow) {
|
||||||
authorPaint.setShadowLayer(
|
authorPaint.setShadowLayer(
|
||||||
4f,
|
4f,
|
||||||
0f,
|
0f,
|
||||||
@@ -183,12 +187,12 @@ class CoverImageView @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
author.forEach {
|
author.forEach {
|
||||||
if (BookCover.drawBookStroke) {
|
if (CoverConfig.coverShowStroke) {
|
||||||
authorPaint.color = Color.WHITE
|
authorPaint.color = Color.WHITE
|
||||||
authorPaint.style = Paint.Style.STROKE
|
authorPaint.style = Paint.Style.STROKE
|
||||||
canvas.drawText(it, startX, startY, authorPaint)
|
canvas.drawText(it, startX, startY, authorPaint)
|
||||||
}
|
}
|
||||||
if (BookCover.coverDefaultColor)
|
if (CoverConfig.coverDefaultColor)
|
||||||
authorPaint.color = context.themeColor(com.google.android.material.R.attr.colorSecondary)
|
authorPaint.color = context.themeColor(com.google.android.material.R.attr.colorSecondary)
|
||||||
else
|
else
|
||||||
authorPaint.color = colorKey
|
authorPaint.color = colorKey
|
||||||
|
|||||||
Reference in new issue
Block a user