fix: 主题相关

This commit is contained in:
HapeLee
2026-07-02 23:27:10 +08:00
parent 6f87071a85
commit 0c15737b7b
10 changed files with 266 additions and 67 deletions
@@ -72,6 +72,13 @@ object ThemeImportExport {
return saveThemeData(name, data) return saveThemeData(name, data)
} }
fun uniqueSavedThemeName(name: String): String {
if (_savedThemes.none { it.name == name }) return name
var index = 2
while (_savedThemes.any { it.name == "$name $index" }) index++
return "$name $index"
}
private fun saveThemeData(name: String, data: ThemeExportData): SavedTheme { private fun saveThemeData(name: String, data: ThemeExportData): SavedTheme {
val file = File(baseDir, "$name.json") val file = File(baseDir, "$name.json")
baseDir.mkdirs() baseDir.mkdirs()
@@ -82,19 +89,6 @@ object ThemeImportExport {
return theme return theme
} }
/**
* 应用已保存的主题
*/
fun applySavedTheme(theme: SavedTheme): Boolean {
return try {
applyToThemeConfig(theme.data)
true
} catch (e: Exception) {
e.printStackTrace()
false
}
}
/** /**
* 删除已保存的主题 * 删除已保存的主题
*/ */
@@ -305,7 +299,10 @@ object ThemeImportExport {
/** /**
* 将导出数据应用到当前配置 * 将导出数据应用到当前配置
*/ */
fun applyToThemeConfig(data: ThemeExportData) { internal fun applyToThemeConfig(
data: ThemeExportData,
applyEmbeddedCoverAssets: Boolean = true,
): AppliedThemeAssets {
// 基础主题设置 // 基础主题设置
ThemeConfig.appTheme = data.appTheme ThemeConfig.appTheme = data.appTheme
ThemeConfig.themeMode = data.themeMode ThemeConfig.themeMode = data.themeMode
@@ -416,16 +413,31 @@ object ThemeImportExport {
CoverConfig.coverInfoOrientation = data.coverInfoOrientation CoverConfig.coverInfoOrientation = data.coverInfoOrientation
// 应用嵌入的资源 // 应用嵌入的资源
data.assets?.let { assets -> val embeddedAssets = data.assets?.let { assets ->
applyAssets(assets) applyAssets(
} assets = assets,
applyCoverAssets = applyEmbeddedCoverAssets,
)
} ?: AppliedThemeAssets()
return AppliedThemeAssets(
lightCoverPaths = embeddedAssets.lightCoverPaths.ifEmpty {
data.coverDefaultImage.toCoverPaths()
},
darkCoverPaths = embeddedAssets.darkCoverPaths.ifEmpty {
data.coverDefaultImageDark.toCoverPaths()
},
)
} }
private fun applyAssets(assets: Map<String, String>) { private fun applyAssets(
assets: Map<String, String>,
applyCoverAssets: Boolean,
): AppliedThemeAssets {
val coverPaths = mutableMapOf<String, MutableList<String>>() val coverPaths = mutableMapOf<String, MutableList<String>>()
assets.forEach { (key, base64) -> assets.forEach { (key, base64) ->
if (base64.isBlank()) return@forEach if (base64.isBlank()) return@forEach
if (!applyCoverAssets && key.startsWith("coverDefaultImage")) return@forEach
try { try {
val bytes = EncoderUtils.base64DecodeToByteArray(base64) val bytes = EncoderUtils.base64DecodeToByteArray(base64)
val destFile = when { val destFile = when {
@@ -493,6 +505,10 @@ object ThemeImportExport {
coverPaths["coverDefaultImageDark"]?.let { paths -> coverPaths["coverDefaultImageDark"]?.let { paths ->
CoverConfig.defaultCoverDark = paths.joinToString(",") CoverConfig.defaultCoverDark = paths.joinToString(",")
} }
return AppliedThemeAssets(
lightCoverPaths = coverPaths["coverDefaultImage"].orEmpty(),
darkCoverPaths = coverPaths["coverDefaultImageDark"].orEmpty(),
)
} }
/** /**
@@ -507,16 +523,22 @@ object ThemeImportExport {
* 从JSON字符串导入主题 * 从JSON字符串导入主题
*/ */
fun importFromJson(json: String): Boolean { fun importFromJson(json: String): Boolean {
return importFromJsonWithAssets(json) != null
}
internal fun importFromJsonWithAssets(json: String): AppliedThemeAssets? {
return try { return try {
val data = parseThemeData(json) ?: return false val data = parseThemeData(json) ?: return null
applyToThemeConfig(data) applyToThemeConfig(data)
true
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
false null
} }
} }
private fun String.toCoverPaths(): List<String> =
split(",").map(String::trim).filter(String::isNotEmpty)
private fun parseThemeData(json: String): ThemeExportData? { private fun parseThemeData(json: String): ThemeExportData? {
val root = JsonParser.parseString(json).asJsonObject val root = JsonParser.parseString(json).asJsonObject
return when { return when {
@@ -761,6 +783,7 @@ data class ThemeExportData(
val bgImageBlurring: Int = 0, val bgImageBlurring: Int = 0,
val bgImageNBlurring: Int = 0, val bgImageNBlurring: Int = 0,
val appFontPath: String? = null, val appFontPath: String? = null,
val selectedCoverAlbumId: String? = null,
// 封面配置 (CoverConfig) // 封面配置 (CoverConfig)
val coverLoadOnlyWifi: Boolean = false, val coverLoadOnlyWifi: Boolean = false,
@@ -792,3 +815,8 @@ data class SavedTheme(
val name: String, val name: String,
val data: ThemeExportData val data: ThemeExportData
) )
internal data class AppliedThemeAssets(
val lightCoverPaths: List<String> = emptyList(),
val darkCoverPaths: List<String> = emptyList(),
)
@@ -2,14 +2,13 @@ package io.legado.app.help.config
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.provider.OpenableColumns
import androidx.annotation.Keep import androidx.annotation.Keep
import com.google.gson.JsonParser import com.google.gson.JsonParser
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
import io.legado.app.R import io.legado.app.R
import io.legado.app.domain.model.CoverAlbumImageInput import io.legado.app.domain.model.CoverAlbumImageInput
import io.legado.app.domain.usecase.CoverAlbumUseCase import io.legado.app.domain.usecase.CoverAlbumUseCase
import io.legado.app.ui.config.coverConfig.CoverConfig
import io.legado.app.ui.config.themeConfig.ThemeConfig
import io.legado.app.utils.GSON import io.legado.app.utils.GSON
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -60,7 +59,10 @@ class ThemePackageManager(
context.contentResolver.openOutputStream(uri)?.use { output -> context.contentResolver.openOutputStream(uri)?.use { output ->
ZipOutputStream(BufferedOutputStream(output)).use { zip -> ZipOutputStream(BufferedOutputStream(output)).use { zip ->
val assetEntries = exportAssets(zip, rawConfig) val assetEntries = exportAssets(zip, rawConfig)
val coverData = exportCoverAlbums(zip) val coverData = exportCoverAlbums(
zip = zip,
preferredAlbumId = rawConfig.selectedCoverAlbumId,
)
val manifest = ThemePackageManifest( val manifest = ThemePackageManifest(
formatVersion = FORMAT_VERSION, formatVersion = FORMAT_VERSION,
name = themeName, name = themeName,
@@ -126,6 +128,10 @@ class ThemePackageManager(
) )
) )
coverAlbumUseCase.selectAlbum(selectedAlbumId) coverAlbumUseCase.selectAlbum(selectedAlbumId)
saveImportedTheme(
name = importedThemeName(uri, manifest.name),
selectedCoverAlbumId = selectedAlbumId,
)
} catch (error: CancellationException) { } catch (error: CancellationException) {
throw error throw error
} catch (error: Exception) { } catch (error: Exception) {
@@ -145,13 +151,61 @@ class ThemePackageManager(
val json = context.contentResolver.openInputStream(uri)?.use { val json = context.contentResolver.openInputStream(uri)?.use {
it.bufferedReader().readText() it.bufferedReader().readText()
} ?: error("无法读取旧主题配置") } ?: error("无法读取旧主题配置")
check(ThemeImportExport.importFromJson(json)) { val appliedAssets = ThemeImportExport.importFromJsonWithAssets(json)
"旧主题配置格式无效或字段已被混淆" ?: error("旧主题配置格式无效或字段已被混淆")
} val name = importedThemeName(uri)
importLegacyCoverAlbums() val selectedCoverAlbumId = importLegacyCoverAlbums(
albumName = name,
appliedAssets = appliedAssets,
)
saveImportedTheme(
name = name,
selectedCoverAlbumId = selectedCoverAlbumId,
)
} }
} }
fun saveTheme(
name: String,
data: ThemeExportData = ThemeImportExport.exportFromCurrent(),
): SavedTheme {
val selectedCoverAlbumId = data.selectedCoverAlbumId
?: coverAlbumUseCase.selection.value.albumId
return ThemeImportExport.saveCurrentAsTheme(
name = name,
data = data.copy(selectedCoverAlbumId = selectedCoverAlbumId),
)
}
suspend fun applySavedTheme(theme: SavedTheme): Result<Unit> =
withContext(Dispatchers.IO) {
runSuspendCatching {
val savedAlbumId = theme.data.selectedCoverAlbumId
?.takeIf { id -> coverAlbumUseCase.albums.value.any { it.id == id } }
val appliedAssets = ThemeImportExport.applyToThemeConfig(
data = theme.data,
applyEmbeddedCoverAssets = savedAlbumId == null,
)
val selectedCoverAlbumId = if (savedAlbumId != null) {
coverAlbumUseCase.selectAlbum(savedAlbumId)
savedAlbumId
} else {
importLegacyCoverAlbums(
albumName = theme.name,
appliedAssets = appliedAssets,
)
}
if (selectedCoverAlbumId != theme.data.selectedCoverAlbumId) {
ThemeImportExport.saveCurrentAsTheme(
name = theme.name,
data = theme.data.copy(
selectedCoverAlbumId = selectedCoverAlbumId,
),
)
}
}
}
private fun exportAssets( private fun exportAssets(
zip: ZipOutputStream, zip: ZipOutputStream,
config: ThemeExportData, config: ThemeExportData,
@@ -183,10 +237,15 @@ class ThemePackageManager(
return result return result
} }
private fun exportCoverAlbums(zip: ZipOutputStream): ExportedCoverData { private fun exportCoverAlbums(
val selection = coverAlbumUseCase.selection.value zip: ZipOutputStream,
preferredAlbumId: String?,
): ExportedCoverData {
val albumsById = coverAlbumUseCase.albums.value.associateBy { it.id } val albumsById = coverAlbumUseCase.albums.value.associateBy { it.id }
val selectedIds = listOfNotNull(selection.albumId) val selectedAlbumId = preferredAlbumId
?.takeIf(albumsById::containsKey)
?: coverAlbumUseCase.selection.value.albumId
val selectedIds = listOfNotNull(selectedAlbumId)
val refById = selectedIds.associateWith { "album_0" } val refById = selectedIds.associateWith { "album_0" }
val exportedAlbums = selectedIds.mapNotNull { albumId -> val exportedAlbums = selectedIds.mapNotNull { albumId ->
val album = albumsById[albumId] ?: return@mapNotNull null val album = albumsById[albumId] ?: return@mapNotNull null
@@ -217,7 +276,7 @@ class ThemePackageManager(
return ExportedCoverData( return ExportedCoverData(
albums = exportedAlbums, albums = exportedAlbums,
selection = ThemePackageCoverSelection( selection = ThemePackageCoverSelection(
albumRef = selection.albumId?.let(refById::get), albumRef = selectedAlbumId?.let(refById::get),
), ),
) )
} }
@@ -289,15 +348,18 @@ class ThemePackageManager(
return result return result
} }
private suspend fun importLegacyCoverAlbums() { private suspend fun importLegacyCoverAlbums(
albumName: String,
appliedAssets: AppliedThemeAssets,
): String? {
val importedIds = mutableListOf<String>() val importedIds = mutableListOf<String>()
try { try {
val lightFiles = CoverConfig.defaultCover.toExistingFiles() val lightFiles = appliedAssets.lightCoverPaths.toExistingFiles()
val darkFiles = CoverConfig.defaultCoverDark.toExistingFiles() val darkFiles = appliedAssets.darkCoverPaths.toExistingFiles()
val albumId = if (lightFiles.isNotEmpty() || darkFiles.isNotEmpty()) { val albumId = if (lightFiles.isNotEmpty() || darkFiles.isNotEmpty()) {
val existingNames = coverAlbumUseCase.albums.value.mapTo(mutableSetOf()) { it.name } val existingNames = coverAlbumUseCase.albums.value.mapTo(mutableSetOf()) { it.name }
val name = uniqueImportedAlbumName( val name = uniqueImportedAlbumName(
context.getString(R.string.default_cover), albumName,
existingNames, existingNames,
) )
coverAlbumUseCase.importAlbum( coverAlbumUseCase.importAlbum(
@@ -311,6 +373,7 @@ class ThemePackageManager(
null null
} }
coverAlbumUseCase.selectAlbum(albumId) coverAlbumUseCase.selectAlbum(albumId)
return albumId
} catch (error: CancellationException) { } catch (error: CancellationException) {
throw error throw error
} catch (error: Exception) { } catch (error: Exception) {
@@ -321,6 +384,43 @@ class ThemePackageManager(
} }
} }
private fun saveImportedTheme(
name: String,
selectedCoverAlbumId: String?,
) {
saveTheme(
name = ThemeImportExport.uniqueSavedThemeName(name),
data = ThemeImportExport.exportFromCurrent().copy(
selectedCoverAlbumId = selectedCoverAlbumId,
),
)
}
private fun importedThemeName(
uri: Uri,
packageName: String? = null,
): String {
val displayName = context.contentResolver.query(
uri,
arrayOf(OpenableColumns.DISPLAY_NAME),
null,
null,
null,
)?.use { cursor ->
if (cursor.moveToFirst()) cursor.getString(0) else null
}
val rawName = packageName
?.takeIf(String::isNotBlank)
?: displayName?.substringBeforeLast('.')
?: uri.lastPathSegment?.substringBeforeLast('.')
?: context.getString(R.string.import_theme)
return rawName
.replace(Regex("""[\\/:*?"<>|]"""), "_")
.trim()
.trim('.')
.ifBlank { context.getString(R.string.import_theme) }
}
private fun extractPackage(uri: Uri, root: File) { private fun extractPackage(uri: Uri, root: File) {
root.mkdirs() root.mkdirs()
var entryCount = 0 var entryCount = 0
@@ -410,6 +510,7 @@ class ThemePackageManager(
appFontPath = null, appFontPath = null,
coverDefaultImage = "", coverDefaultImage = "",
coverDefaultImageDark = "", coverDefaultImageDark = "",
selectedCoverAlbumId = null,
assets = null, assets = null,
) )
@@ -456,9 +557,8 @@ class ThemePackageManager(
return entryBytes return entryBytes
} }
private fun String.toExistingFiles(): List<File> = private fun List<String>.toExistingFiles(): List<File> =
split(",") map(String::trim)
.map(String::trim)
.filter(String::isNotEmpty) .filter(String::isNotEmpty)
.map(::File) .map(::File)
.filter(File::isFile) .filter(File::isFile)
@@ -1,14 +1,12 @@
package io.legado.app.ui.config.customTheme package io.legado.app.ui.config.customTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.ui.draw.clip
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -18,6 +16,7 @@ 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.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color 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.platform.LocalContext
@@ -33,13 +32,13 @@ import io.legado.app.ui.config.themeConfig.ThemeConfig
import io.legado.app.ui.theme.adaptiveContentPadding import io.legado.app.ui.theme.adaptiveContentPadding
import io.legado.app.ui.widget.components.AppScaffold import io.legado.app.ui.widget.components.AppScaffold
import io.legado.app.ui.widget.components.SplicedColumnGroup import io.legado.app.ui.widget.components.SplicedColumnGroup
import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
import io.legado.app.ui.widget.components.settingItem.DropdownListSettingItem
import io.legado.app.ui.widget.components.dialog.ColorPickerSheet 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.SwitchSettingItem import io.legado.app.ui.widget.components.settingItem.SwitchSettingItem
import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@@ -140,7 +139,7 @@ fun CustomThemeScreen(
} }
} else { } else {
item { item {
SplicedColumnGroup(title = stringResource(R.string.custom_theme)) { SplicedColumnGroup(title = stringResource(R.string.custom_theme_colors)) {
ClickableSettingItem( ClickableSettingItem(
title = stringResource(R.string.seed_color), title = stringResource(R.string.seed_color),
description = stringResource(R.string.day), description = stringResource(R.string.day),
@@ -322,6 +322,11 @@ fun ThemeConfigScreen(
} }
) )
} }
ClickableSettingItem(
title = stringResource(R.string.theme_pack),
description = stringResource(R.string.theme_pack_s),
onClick = onNavigateToThemeManage
)
} }
SplicedColumnGroup { SplicedColumnGroup {
@@ -331,7 +336,7 @@ fun ThemeConfigScreen(
) )
if (selectedTheme == "12" && (!isMiuixEngine || useMiuixMonet)) { if (selectedTheme == "12" && (!isMiuixEngine || useMiuixMonet)) {
ClickableSettingItem( ClickableSettingItem(
title = stringResource(R.string.custom_theme), title = stringResource(R.string.custom_theme_colors),
onClick = onNavigateToCustomTheme onClick = onNavigateToCustomTheme
) )
} }
@@ -803,16 +808,6 @@ fun ThemeConfigScreen(
} }
} }
// Theme management
item {
SplicedColumnGroup(title = stringResource(R.string.theme_pack)) {
ClickableSettingItem(
title = stringResource(R.string.theme_pack),
description = stringResource(R.string.theme_pack_s),
onClick = onNavigateToThemeManage
)
}
}
} }
} }
@@ -115,6 +115,14 @@ fun ThemeManageScreen(
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
viewModel.effects.collectLatest { effect -> viewModel.effects.collectLatest { effect ->
when (effect) { when (effect) {
ThemeManageEffect.SavedThemesChanged -> {
savedThemesVersion++
}
ThemeManageEffect.RestartRequired -> {
showRestartDialog = true
}
is ThemeManageEffect.ShowResult -> { is ThemeManageEffect.ShowResult -> {
val message = buildString { val message = buildString {
append(context.getString(effect.messageRes)) append(context.getString(effect.messageRes))
@@ -124,6 +132,9 @@ fun ThemeManageScreen(
} }
} }
context.toastOnUi(message) context.toastOnUi(message)
if (effect.savedThemesChanged) {
savedThemesVersion++
}
if (effect.restartRequired) { if (effect.restartRequired) {
showRestartDialog = true showRestartDialog = true
} }
@@ -249,8 +260,7 @@ fun ThemeManageScreen(
confirmText = stringResource(R.string.theme_manage_save), confirmText = stringResource(R.string.theme_manage_save),
onConfirm = { onConfirm = {
if (newThemeName.isNotBlank()) { if (newThemeName.isNotBlank()) {
ThemeImportExport.saveCurrentAsTheme(newThemeName) viewModel.onIntent(ThemeManageIntent.SaveTheme(newThemeName))
savedThemesVersion++
showSaveDialog = false showSaveDialog = false
} }
}, },
@@ -275,8 +285,7 @@ fun ThemeManageScreen(
confirmText = stringResource(R.string.theme_manage_apply), confirmText = stringResource(R.string.theme_manage_apply),
onConfirm = { onConfirm = {
applyTarget?.let { theme -> applyTarget?.let { theme ->
ThemeImportExport.applySavedTheme(theme) viewModel.onIntent(ThemeManageIntent.ApplySavedTheme(theme))
showRestartDialog = true
} }
applyTarget = null applyTarget = null
}, },
@@ -310,11 +319,13 @@ fun ThemeManageScreen(
themeName = editTarget?.name ?: "", themeName = editTarget?.name ?: "",
onDismissRequest = { editTarget = null }, onDismissRequest = { editTarget = null },
onSave = { newName, newData -> onSave = { newName, newData ->
editTarget?.let { old -> viewModel.onIntent(
ThemeImportExport.deleteSavedTheme(old) ThemeManageIntent.SaveTheme(
} name = newName,
ThemeImportExport.saveCurrentAsTheme(newName, newData) data = newData,
savedThemesVersion++ replacedTheme = editTarget,
)
)
editTarget = null editTarget = null
} }
) )
@@ -5,8 +5,11 @@ import androidx.annotation.StringRes
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import io.legado.app.R import io.legado.app.R
import io.legado.app.help.config.SavedTheme
import io.legado.app.help.config.ThemeExportData import io.legado.app.help.config.ThemeExportData
import io.legado.app.help.config.ThemeImportExport
import io.legado.app.help.config.ThemePackageManager import io.legado.app.help.config.ThemePackageManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -23,6 +26,49 @@ class ThemeManageViewModel(
is ThemeManageIntent.ExportPackage -> exportPackage(intent) is ThemeManageIntent.ExportPackage -> exportPackage(intent)
is ThemeManageIntent.ImportPackage -> importPackage(intent.uri) is ThemeManageIntent.ImportPackage -> importPackage(intent.uri)
is ThemeManageIntent.ImportLegacyJson -> importLegacyJson(intent.uri) is ThemeManageIntent.ImportLegacyJson -> importLegacyJson(intent.uri)
is ThemeManageIntent.SaveTheme -> saveTheme(intent)
is ThemeManageIntent.ApplySavedTheme -> applySavedTheme(intent.theme)
}
}
private fun saveTheme(intent: ThemeManageIntent.SaveTheme) {
viewModelScope.launch(Dispatchers.IO) {
val result = runCatching {
themePackageManager.saveTheme(
name = intent.name,
data = intent.data ?: ThemeImportExport.exportFromCurrent(),
)
intent.replacedTheme
?.takeIf { it.name != intent.name }
?.let(ThemeImportExport::deleteSavedTheme)
}
if (result.isSuccess) {
_effects.emit(ThemeManageEffect.SavedThemesChanged)
} else {
_effects.emit(
ThemeManageEffect.ShowResult(
messageRes = R.string.theme_manage_save_failed,
detail = result.exceptionOrNull()?.localizedMessage,
)
)
}
}
}
private fun applySavedTheme(theme: SavedTheme) {
viewModelScope.launch {
val result = themePackageManager.applySavedTheme(theme)
if (result.isSuccess) {
_effects.emit(ThemeManageEffect.SavedThemesChanged)
_effects.emit(ThemeManageEffect.RestartRequired)
} else {
_effects.emit(
ThemeManageEffect.ShowResult(
messageRes = R.string.theme_manage_apply_failed,
detail = result.exceptionOrNull()?.localizedMessage,
)
)
}
} }
} }
@@ -64,6 +110,7 @@ class ThemeManageViewModel(
ThemeManageEffect.ShowResult( ThemeManageEffect.ShowResult(
messageRes = R.string.theme_manage_import_success, messageRes = R.string.theme_manage_import_success,
restartRequired = true, restartRequired = true,
savedThemesChanged = true,
) )
} else { } else {
ThemeManageEffect.ShowResult( ThemeManageEffect.ShowResult(
@@ -84,12 +131,23 @@ sealed interface ThemeManageIntent {
data class ImportPackage(val uri: String) : ThemeManageIntent data class ImportPackage(val uri: String) : ThemeManageIntent
data class ImportLegacyJson(val uri: String) : ThemeManageIntent data class ImportLegacyJson(val uri: String) : ThemeManageIntent
data class SaveTheme(
val name: String,
val data: ThemeExportData? = null,
val replacedTheme: SavedTheme? = null,
) : ThemeManageIntent
data class ApplySavedTheme(val theme: SavedTheme) : ThemeManageIntent
} }
sealed interface ThemeManageEffect { sealed interface ThemeManageEffect {
data object SavedThemesChanged : ThemeManageEffect
data object RestartRequired : ThemeManageEffect
data class ShowResult( data class ShowResult(
@param:StringRes val messageRes: Int, @param:StringRes val messageRes: Int,
val detail: String? = null, val detail: String? = null,
val restartRequired: Boolean = false, val restartRequired: Boolean = false,
val savedThemesChanged: Boolean = false,
) : ThemeManageEffect ) : ThemeManageEffect
} }
@@ -1436,6 +1436,8 @@
<string name="theme_manage_export_failed">主题导出失败</string> <string name="theme_manage_export_failed">主题导出失败</string>
<string name="theme_manage_import_success">主题导入成功,部分设置需要重启生效</string> <string name="theme_manage_import_success">主题导入成功,部分设置需要重启生效</string>
<string name="theme_manage_import_failed">主题导入失败</string> <string name="theme_manage_import_failed">主题导入失败</string>
<string name="theme_manage_save_failed">主题保存失败</string>
<string name="theme_manage_apply_failed">主题应用失败</string>
<string name="theme_manage_save_current">保存当前设置</string> <string name="theme_manage_save_current">保存当前设置</string>
<string name="theme_manage_save_current_summary">保存当前主题配置为新主题</string> <string name="theme_manage_save_current_summary">保存当前主题配置为新主题</string>
<string name="theme_manage_export_current">导出当前主题</string> <string name="theme_manage_export_current">导出当前主题</string>
@@ -2114,6 +2114,8 @@
<string name="theme_manage_import_config_summary">從舊版 JSON 檔案匯入主題設定</string> <string name="theme_manage_import_config_summary">從舊版 JSON 檔案匯入主題設定</string>
<string name="theme_manage_import_failed">主題匯入失敗</string> <string name="theme_manage_import_failed">主題匯入失敗</string>
<string name="theme_manage_import_success">主題已匯入。部分更改需要重啟。</string> <string name="theme_manage_import_success">主題已匯入。部分更改需要重啟。</string>
<string name="theme_manage_save_failed">主題儲存失敗</string>
<string name="theme_manage_apply_failed">主題套用失敗</string>
<string name="theme_manage_label_container_color">標籤容器色</string> <string name="theme_manage_label_container_color">標籤容器色</string>
<string name="theme_manage_label_visibility">標籤可見性</string> <string name="theme_manage_label_visibility">標籤可見性</string>
<string name="theme_manage_name_hint">輸入主題名稱</string> <string name="theme_manage_name_hint">輸入主題名稱</string>
@@ -1583,6 +1583,8 @@
<string name="theme_manage_export_failed">無法匯出主題</string> <string name="theme_manage_export_failed">無法匯出主題</string>
<string name="theme_manage_import_success">主題已匯入。部分變更需要重新啟動。</string> <string name="theme_manage_import_success">主題已匯入。部分變更需要重新啟動。</string>
<string name="theme_manage_import_failed">無法匯入主題</string> <string name="theme_manage_import_failed">無法匯入主題</string>
<string name="theme_manage_save_failed">無法儲存主題</string>
<string name="theme_manage_apply_failed">無法套用主題</string>
<string name="theme_manage_save_current">儲存目前主題</string> <string name="theme_manage_save_current">儲存目前主題</string>
<string name="theme_manage_save_current_summary">將目前外觀設定儲存為主題</string> <string name="theme_manage_save_current_summary">將目前外觀設定儲存為主題</string>
<string name="theme_manage_export_current">匯出目前主題</string> <string name="theme_manage_export_current">匯出目前主題</string>
+2
View File
@@ -1471,6 +1471,8 @@
<string name="theme_manage_export_failed">Could not export theme</string> <string name="theme_manage_export_failed">Could not export theme</string>
<string name="theme_manage_import_success">Theme imported. Some changes require a restart.</string> <string name="theme_manage_import_success">Theme imported. Some changes require a restart.</string>
<string name="theme_manage_import_failed">Could not import theme</string> <string name="theme_manage_import_failed">Could not import theme</string>
<string name="theme_manage_save_failed">Could not save theme</string>
<string name="theme_manage_apply_failed">Could not apply theme</string>
<string name="theme_manage_save_current">Save Current Theme</string> <string name="theme_manage_save_current">Save Current Theme</string>
<string name="theme_manage_save_current_summary">Save the current appearance settings as a theme</string> <string name="theme_manage_save_current_summary">Save the current appearance settings as a theme</string>
<string name="theme_manage_export_current">Export Current Theme</string> <string name="theme_manage_export_current">Export Current Theme</string>