diff --git a/app/src/main/java/io/legado/app/help/config/ThemeImportExport.kt b/app/src/main/java/io/legado/app/help/config/ThemeImportExport.kt index a9391a965..9f42d2c13 100644 --- a/app/src/main/java/io/legado/app/help/config/ThemeImportExport.kt +++ b/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) { + private fun applyAssets( + assets: Map, + applyCoverAssets: Boolean, + ): AppliedThemeAssets { val coverPaths = mutableMapOf>() 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 = + 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 = emptyList(), + val darkCoverPaths: List = emptyList(), +) diff --git a/app/src/main/java/io/legado/app/help/config/ThemePackageManager.kt b/app/src/main/java/io/legado/app/help/config/ThemePackageManager.kt index cd2f92a7b..049d26ffb 100644 --- a/app/src/main/java/io/legado/app/help/config/ThemePackageManager.kt +++ b/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 = + 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() 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 = - split(",") - .map(String::trim) + private fun List.toExistingFiles(): List = + map(String::trim) .filter(String::isNotEmpty) .map(::File) .filter(File::isFile) diff --git a/app/src/main/java/io/legado/app/ui/config/customTheme/CustomThemeScreen.kt b/app/src/main/java/io/legado/app/ui/config/customTheme/CustomThemeScreen.kt index c7738779e..eb04dbf18 100644 --- a/app/src/main/java/io/legado/app/ui/config/customTheme/CustomThemeScreen.kt +++ b/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.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.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size 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), diff --git a/app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigScreen.kt b/app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigScreen.kt index dda9a028d..99167e424 100644 --- a/app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigScreen.kt +++ b/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 - ) - } - } } } diff --git a/app/src/main/java/io/legado/app/ui/config/themeManage/ThemeManageScreen.kt b/app/src/main/java/io/legado/app/ui/config/themeManage/ThemeManageScreen.kt index c830503a7..7981872fd 100644 --- a/app/src/main/java/io/legado/app/ui/config/themeManage/ThemeManageScreen.kt +++ b/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 } ) diff --git a/app/src/main/java/io/legado/app/ui/config/themeManage/ThemeManageViewModel.kt b/app/src/main/java/io/legado/app/ui/config/themeManage/ThemeManageViewModel.kt index dce3b3e4a..60a8b0773 100644 --- a/app/src/main/java/io/legado/app/ui/config/themeManage/ThemeManageViewModel.kt +++ b/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 } diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 8c65590d5..86151c4f5 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -1436,6 +1436,8 @@ 主题导出失败 主题导入成功,部分设置需要重启生效 主题导入失败 + 主题保存失败 + 主题应用失败 保存当前设置 保存当前主题配置为新主题 导出当前主题 diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index c0dea4870..f35b50b23 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -2114,6 +2114,8 @@ 從舊版 JSON 檔案匯入主題設定 主題匯入失敗 主題已匯入。部分更改需要重啟。 + 主題儲存失敗 + 主題套用失敗 標籤容器色 標籤可見性 輸入主題名稱 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index de1b88f56..20bc308bd 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -1583,6 +1583,8 @@ 無法匯出主題 主題已匯入。部分變更需要重新啟動。 無法匯入主題 + 無法儲存主題 + 無法套用主題 儲存目前主題 將目前外觀設定儲存為主題 匯出目前主題 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0e8a8f0e8..9751e4ad2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1471,6 +1471,8 @@ Could not export theme Theme imported. Some changes require a restart. Could not import theme + Could not save theme + Could not apply theme Save Current Theme Save the current appearance settings as a theme Export Current Theme