HapeLee 4 месяцев назад
Родитель
Сommit
0c15737b7b

+ 49 - 21
app/src/main/java/io/legado/app/help/config/ThemeImportExport.kt

@@ -72,6 +72,13 @@ object ThemeImportExport {
         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 {
         val file = File(baseDir, "$name.json")
         baseDir.mkdirs()
@@ -82,19 +89,6 @@ object ThemeImportExport {
         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.themeMode = data.themeMode
@@ -416,16 +413,31 @@ object ThemeImportExport {
         CoverConfig.coverInfoOrientation = data.coverInfoOrientation
 
         // 应用嵌入的资源
-        data.assets?.let { assets ->
-            applyAssets(assets)
-        }
+        val embeddedAssets = data.assets?.let { 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>>()
 
         assets.forEach { (key, base64) ->
             if (base64.isBlank()) return@forEach
+            if (!applyCoverAssets && key.startsWith("coverDefaultImage")) return@forEach
             try {
                 val bytes = EncoderUtils.base64DecodeToByteArray(base64)
                 val destFile = when {
@@ -493,6 +505,10 @@ object ThemeImportExport {
         coverPaths["coverDefaultImageDark"]?.let { paths ->
             CoverConfig.defaultCoverDark = paths.joinToString(",")
         }
+        return AppliedThemeAssets(
+            lightCoverPaths = coverPaths["coverDefaultImage"].orEmpty(),
+            darkCoverPaths = coverPaths["coverDefaultImageDark"].orEmpty(),
+        )
     }
 
     /**
@@ -507,16 +523,22 @@ object ThemeImportExport {
      * 从JSON字符串导入主题
      */
     fun importFromJson(json: String): Boolean {
+        return importFromJsonWithAssets(json) != null
+    }
+
+    internal fun importFromJsonWithAssets(json: String): AppliedThemeAssets? {
         return try {
-            val data = parseThemeData(json) ?: return false
+            val data = parseThemeData(json) ?: return null
             applyToThemeConfig(data)
-            true
         } catch (e: Exception) {
             e.printStackTrace()
-            false
+            null
         }
     }
 
+    private fun String.toCoverPaths(): List<String> =
+        split(",").map(String::trim).filter(String::isNotEmpty)
+
     private fun parseThemeData(json: String): ThemeExportData? {
         val root = JsonParser.parseString(json).asJsonObject
         return when {
@@ -761,6 +783,7 @@ data class ThemeExportData(
     val bgImageBlurring: Int = 0,
     val bgImageNBlurring: Int = 0,
     val appFontPath: String? = null,
+    val selectedCoverAlbumId: String? = null,
 
     // 封面配置 (CoverConfig)
     val coverLoadOnlyWifi: Boolean = false,
@@ -792,3 +815,8 @@ data class SavedTheme(
     val name: String,
     val data: ThemeExportData
 )
+
+internal data class AppliedThemeAssets(
+    val lightCoverPaths: List<String> = emptyList(),
+    val darkCoverPaths: List<String> = emptyList(),
+)

+ 118 - 18
app/src/main/java/io/legado/app/help/config/ThemePackageManager.kt

@@ -2,14 +2,13 @@ package io.legado.app.help.config
 
 import android.content.Context
 import android.net.Uri
+import android.provider.OpenableColumns
 import androidx.annotation.Keep
 import com.google.gson.JsonParser
 import com.google.gson.annotations.SerializedName
 import io.legado.app.R
 import io.legado.app.domain.model.CoverAlbumImageInput
 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 kotlinx.coroutines.CancellationException
 import kotlinx.coroutines.Dispatchers
@@ -60,7 +59,10 @@ class ThemePackageManager(
             context.contentResolver.openOutputStream(uri)?.use { output ->
                 ZipOutputStream(BufferedOutputStream(output)).use { zip ->
                     val assetEntries = exportAssets(zip, rawConfig)
-                    val coverData = exportCoverAlbums(zip)
+                    val coverData = exportCoverAlbums(
+                        zip = zip,
+                        preferredAlbumId = rawConfig.selectedCoverAlbumId,
+                    )
                     val manifest = ThemePackageManifest(
                         formatVersion = FORMAT_VERSION,
                         name = themeName,
@@ -126,6 +128,10 @@ class ThemePackageManager(
                     )
                 )
                 coverAlbumUseCase.selectAlbum(selectedAlbumId)
+                saveImportedTheme(
+                    name = importedThemeName(uri, manifest.name),
+                    selectedCoverAlbumId = selectedAlbumId,
+                )
             } catch (error: CancellationException) {
                 throw error
             } catch (error: Exception) {
@@ -145,13 +151,61 @@ class ThemePackageManager(
             val json = context.contentResolver.openInputStream(uri)?.use {
                 it.bufferedReader().readText()
             } ?: error("无法读取旧主题配置")
-            check(ThemeImportExport.importFromJson(json)) {
-                "旧主题配置格式无效或字段已被混淆"
-            }
-            importLegacyCoverAlbums()
+            val appliedAssets = ThemeImportExport.importFromJsonWithAssets(json)
+                ?: error("旧主题配置格式无效或字段已被混淆")
+            val name = importedThemeName(uri)
+            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(
         zip: ZipOutputStream,
         config: ThemeExportData,
@@ -183,10 +237,15 @@ class ThemePackageManager(
         return result
     }
 
-    private fun exportCoverAlbums(zip: ZipOutputStream): ExportedCoverData {
-        val selection = coverAlbumUseCase.selection.value
+    private fun exportCoverAlbums(
+        zip: ZipOutputStream,
+        preferredAlbumId: String?,
+    ): ExportedCoverData {
         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 exportedAlbums = selectedIds.mapNotNull { albumId ->
             val album = albumsById[albumId] ?: return@mapNotNull null
@@ -217,7 +276,7 @@ class ThemePackageManager(
         return ExportedCoverData(
             albums = exportedAlbums,
             selection = ThemePackageCoverSelection(
-                albumRef = selection.albumId?.let(refById::get),
+                albumRef = selectedAlbumId?.let(refById::get),
             ),
         )
     }
@@ -289,15 +348,18 @@ class ThemePackageManager(
         return result
     }
 
-    private suspend fun importLegacyCoverAlbums() {
+    private suspend fun importLegacyCoverAlbums(
+        albumName: String,
+        appliedAssets: AppliedThemeAssets,
+    ): String? {
         val importedIds = mutableListOf<String>()
         try {
-            val lightFiles = CoverConfig.defaultCover.toExistingFiles()
-            val darkFiles = CoverConfig.defaultCoverDark.toExistingFiles()
+            val lightFiles = appliedAssets.lightCoverPaths.toExistingFiles()
+            val darkFiles = appliedAssets.darkCoverPaths.toExistingFiles()
             val albumId = if (lightFiles.isNotEmpty() || darkFiles.isNotEmpty()) {
                 val existingNames = coverAlbumUseCase.albums.value.mapTo(mutableSetOf()) { it.name }
                 val name = uniqueImportedAlbumName(
-                    context.getString(R.string.default_cover),
+                    albumName,
                     existingNames,
                 )
                 coverAlbumUseCase.importAlbum(
@@ -311,6 +373,7 @@ class ThemePackageManager(
                 null
             }
             coverAlbumUseCase.selectAlbum(albumId)
+            return albumId
         } catch (error: CancellationException) {
             throw error
         } 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) {
         root.mkdirs()
         var entryCount = 0
@@ -410,6 +510,7 @@ class ThemePackageManager(
         appFontPath = null,
         coverDefaultImage = "",
         coverDefaultImageDark = "",
+        selectedCoverAlbumId = null,
         assets = null,
     )
 
@@ -456,9 +557,8 @@ class ThemePackageManager(
         return entryBytes
     }
 
-    private fun String.toExistingFiles(): List<File> =
-        split(",")
-            .map(String::trim)
+    private fun List<String>.toExistingFiles(): List<File> =
+        map(String::trim)
             .filter(String::isNotEmpty)
             .map(::File)
             .filter(File::isFile)

+ 6 - 7
app/src/main/java/io/legado/app/ui/config/customTheme/CustomThemeScreen.kt

@@ -1,14 +1,12 @@
 package io.legado.app.ui.config.customTheme
 
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
 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.border
 import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.ui.draw.clip
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.runtime.Composable
@@ -18,6 +16,7 @@ import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.input.nestedscroll.nestedScroll
 import androidx.compose.ui.platform.LocalContext
@@ -33,13 +32,13 @@ import io.legado.app.ui.config.themeConfig.ThemeConfig
 import io.legado.app.ui.theme.adaptiveContentPadding
 import io.legado.app.ui.widget.components.AppScaffold
 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.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.topbar.GlassMediumFlexibleTopAppBar
 import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
+import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
 
 @OptIn(ExperimentalMaterial3Api::class)
 @Composable
@@ -140,7 +139,7 @@ fun CustomThemeScreen(
                 }
             } else {
                 item {
-                    SplicedColumnGroup(title = stringResource(R.string.custom_theme)) {
+                    SplicedColumnGroup(title = stringResource(R.string.custom_theme_colors)) {
                         ClickableSettingItem(
                             title = stringResource(R.string.seed_color),
                             description = stringResource(R.string.day),

+ 6 - 11
app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigScreen.kt

@@ -322,6 +322,11 @@ fun ThemeConfigScreen(
                             }
                         )
                     }
+                    ClickableSettingItem(
+                        title = stringResource(R.string.theme_pack),
+                        description = stringResource(R.string.theme_pack_s),
+                        onClick = onNavigateToThemeManage
+                    )
                 }
 
                 SplicedColumnGroup {
@@ -331,7 +336,7 @@ fun ThemeConfigScreen(
                     )
                     if (selectedTheme == "12" && (!isMiuixEngine || useMiuixMonet)) {
                         ClickableSettingItem(
-                            title = stringResource(R.string.custom_theme),
+                            title = stringResource(R.string.custom_theme_colors),
                             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
-                    )
-                }
-            }
         }
     }
 

+ 20 - 9
app/src/main/java/io/legado/app/ui/config/themeManage/ThemeManageScreen.kt

@@ -115,6 +115,14 @@ fun ThemeManageScreen(
     LaunchedEffect(Unit) {
         viewModel.effects.collectLatest { effect ->
             when (effect) {
+                ThemeManageEffect.SavedThemesChanged -> {
+                    savedThemesVersion++
+                }
+
+                ThemeManageEffect.RestartRequired -> {
+                    showRestartDialog = true
+                }
+
                 is ThemeManageEffect.ShowResult -> {
                     val message = buildString {
                         append(context.getString(effect.messageRes))
@@ -124,6 +132,9 @@ fun ThemeManageScreen(
                         }
                     }
                     context.toastOnUi(message)
+                    if (effect.savedThemesChanged) {
+                        savedThemesVersion++
+                    }
                     if (effect.restartRequired) {
                         showRestartDialog = true
                     }
@@ -249,8 +260,7 @@ fun ThemeManageScreen(
         confirmText = stringResource(R.string.theme_manage_save),
         onConfirm = {
             if (newThemeName.isNotBlank()) {
-                ThemeImportExport.saveCurrentAsTheme(newThemeName)
-                savedThemesVersion++
+                viewModel.onIntent(ThemeManageIntent.SaveTheme(newThemeName))
                 showSaveDialog = false
             }
         },
@@ -275,8 +285,7 @@ fun ThemeManageScreen(
         confirmText = stringResource(R.string.theme_manage_apply),
         onConfirm = {
             applyTarget?.let { theme ->
-                ThemeImportExport.applySavedTheme(theme)
-                showRestartDialog = true
+                viewModel.onIntent(ThemeManageIntent.ApplySavedTheme(theme))
             }
             applyTarget = null
         },
@@ -310,11 +319,13 @@ fun ThemeManageScreen(
         themeName = editTarget?.name ?: "",
         onDismissRequest = { editTarget = null },
         onSave = { newName, newData ->
-            editTarget?.let { old ->
-                ThemeImportExport.deleteSavedTheme(old)
-            }
-            ThemeImportExport.saveCurrentAsTheme(newName, newData)
-            savedThemesVersion++
+            viewModel.onIntent(
+                ThemeManageIntent.SaveTheme(
+                    name = newName,
+                    data = newData,
+                    replacedTheme = editTarget,
+                )
+            )
             editTarget = null
         }
     )

+ 58 - 0
app/src/main/java/io/legado/app/ui/config/themeManage/ThemeManageViewModel.kt

@@ -5,8 +5,11 @@ import androidx.annotation.StringRes
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewModelScope
 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.ThemeImportExport
 import io.legado.app.help.config.ThemePackageManager
+import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.asSharedFlow
 import kotlinx.coroutines.launch
@@ -23,6 +26,49 @@ class ThemeManageViewModel(
             is ThemeManageIntent.ExportPackage -> exportPackage(intent)
             is ThemeManageIntent.ImportPackage -> importPackage(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(
                     messageRes = R.string.theme_manage_import_success,
                     restartRequired = true,
+                    savedThemesChanged = true,
                 )
             } else {
                 ThemeManageEffect.ShowResult(
@@ -84,12 +131,23 @@ sealed interface ThemeManageIntent {
 
     data class ImportPackage(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 {
+    data object SavedThemesChanged : ThemeManageEffect
+    data object RestartRequired : ThemeManageEffect
+
     data class ShowResult(
         @param:StringRes val messageRes: Int,
         val detail: String? = null,
         val restartRequired: Boolean = false,
+        val savedThemesChanged: Boolean = false,
     ) : ThemeManageEffect
 }

+ 2 - 0
app/src/main/res/values-zh-rCN/strings.xml

@@ -1436,6 +1436,8 @@
     <string name="theme_manage_export_failed">主题导出失败</string>
     <string name="theme_manage_import_success">主题导入成功,部分设置需要重启生效</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_summary">保存当前主题配置为新主题</string>
     <string name="theme_manage_export_current">导出当前主题</string>

+ 2 - 0
app/src/main/res/values-zh-rHK/strings.xml

@@ -2114,6 +2114,8 @@
     <string name="theme_manage_import_config_summary">從舊版 JSON 檔案匯入主題設定</string>
     <string name="theme_manage_import_failed">主題匯入失敗</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_visibility">標籤可見性</string>
     <string name="theme_manage_name_hint">輸入主題名稱</string>

+ 2 - 0
app/src/main/res/values-zh-rTW/strings.xml

@@ -1583,6 +1583,8 @@
     <string name="theme_manage_export_failed">無法匯出主題</string>
     <string name="theme_manage_import_success">主題已匯入。部分變更需要重新啟動。</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_summary">將目前外觀設定儲存為主題</string>
     <string name="theme_manage_export_current">匯出目前主題</string>

+ 2 - 0
app/src/main/res/values/strings.xml

@@ -1471,6 +1471,8 @@
     <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_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_summary">Save the current appearance settings as a theme</string>
     <string name="theme_manage_export_current">Export Current Theme</string>