From 86727d6141283332627da4516b08f855b8b7cf28 Mon Sep 17 00:00:00 2001 From: bhoomik-codes <2007bhoomiksevta11@gmail.com> Date: Fri, 30 Oct 2026 20:37:32 +0530 Subject: [PATCH 1/2] feat(settings): add custom theme import and dynamic theming support --- .../futon/core/prefs/AppSettings.kt | 37 +++++ .../futon/core/prefs/ColorScheme.kt | 6 +- .../futon/core/prefs/ImportedTheme.kt | 99 +++++++++++++ .../futon/core/ui/BaseActivity.kt | 26 +++- .../settings/AppearanceSettingsFragment.kt | 137 ++++++++++++++++++ .../settings/utils/ThemeChooserPreference.kt | 30 +++- app/src/main/res/values/strings.xml | 9 ++ app/src/main/res/values/themes.xml | 2 + app/src/main/res/xml/pref_appearance.xml | 15 ++ 9 files changed, 353 insertions(+), 8 deletions(-) create mode 100644 app/src/main/kotlin/io/github/landwarderer/futon/core/prefs/ImportedTheme.kt diff --git a/app/src/main/kotlin/io/github/landwarderer/futon/core/prefs/AppSettings.kt b/app/src/main/kotlin/io/github/landwarderer/futon/core/prefs/AppSettings.kt index 240f4d9ee5..a021ce5eb3 100644 --- a/app/src/main/kotlin/io/github/landwarderer/futon/core/prefs/AppSettings.kt +++ b/app/src/main/kotlin/io/github/landwarderer/futon/core/prefs/AppSettings.kt @@ -84,6 +84,41 @@ class AppSettings internal constructor( val colorScheme: ColorScheme get() = prefs.getEnumValue(KEY_COLOR_THEME, ColorScheme.default) + @Volatile + private var cachedImportedThemes: List? = null + + /** Parsed imported themes, cached in memory and invalidated on write. */ + val importedThemes: List + get() = cachedImportedThemes ?: ImportedThemeFile + .parseStored(prefs.getString(KEY_CUSTOM_THEMES, "{}") ?: "{}") + .also { cachedImportedThemes = it } + + var selectedCustomThemeId: String? + get() = prefs.getString(KEY_CUSTOM_THEME, null) + set(value) = prefs.edit { putString(KEY_CUSTOM_THEME, value) } + + val selectedCustomTheme: ImportedTheme? + get() = importedThemes.firstOrNull { it.id == selectedCustomThemeId } + + fun importCustomThemes(source: String): List { + val imported = ImportedThemeFile.parseImport(source) + val stored = prefs.getString(KEY_CUSTOM_THEMES, "{}") ?: "{}" + prefs.edit { putString(KEY_CUSTOM_THEMES, ImportedThemeFile.mergeStored(stored, imported)) } + cachedImportedThemes = null + return imported + } + + fun deleteCustomTheme(id: String) { + val themes = importedThemes.filterNot { it.id == id } + prefs.edit { + putString(KEY_CUSTOM_THEMES, ImportedThemeFile.mergeStored("{}", themes)) + if (selectedCustomThemeId == id) { + remove(KEY_CUSTOM_THEME) + } + } + cachedImportedThemes = null + } + val isAmoledTheme: Boolean get() = prefs.getBoolean(KEY_THEME_AMOLED, false) @@ -773,6 +808,8 @@ class AppSettings internal constructor( const val KEY_THEME = "theme" const val KEY_COLOR_THEME = "color_theme" const val KEY_THEME_AMOLED = "amoled_theme" + const val KEY_CUSTOM_THEMES = "custom_themes" + const val KEY_CUSTOM_THEME = "custom_theme" const val KEY_OFFLINE_DISABLED = "no_offline" const val KEY_PAGES_CACHE_CLEAR = "pages_cache_clear" const val KEY_HTTP_CACHE_CLEAR = "http_cache_clear" diff --git a/app/src/main/kotlin/io/github/landwarderer/futon/core/prefs/ColorScheme.kt b/app/src/main/kotlin/io/github/landwarderer/futon/core/prefs/ColorScheme.kt index 0cf650cb84..0408d72748 100644 --- a/app/src/main/kotlin/io/github/landwarderer/futon/core/prefs/ColorScheme.kt +++ b/app/src/main/kotlin/io/github/landwarderer/futon/core/prefs/ColorScheme.kt @@ -24,6 +24,7 @@ enum class ColorScheme( MAMIMI(R.style.ThemeOverlay_Futon_Mamimi, R.string.theme_name_mamimi), KANADE(R.style.ThemeOverlay_Futon_Kanade, R.string.theme_name_kanade), ITSUKA(R.style.ThemeOverlay_Futon_Itsuka, R.string.theme_name_itsuka), + CUSTOM(R.style.ThemeOverlay_Futon_Custom_Light, R.string.theme_name_custom), ; companion object { @@ -35,8 +36,11 @@ enum class ColorScheme( DEFAULT } - fun getAvailableList(): List { + fun getAvailableList(includeCustom: Boolean = false): List { val list = ColorScheme.entries.toMutableList() + if (!includeCustom) { + list.remove(CUSTOM) + } if (!DynamicColors.isDynamicColorAvailable()) { list.remove(MONET) list.remove(EXPRESSIVE) diff --git a/app/src/main/kotlin/io/github/landwarderer/futon/core/prefs/ImportedTheme.kt b/app/src/main/kotlin/io/github/landwarderer/futon/core/prefs/ImportedTheme.kt new file mode 100644 index 0000000000..08e629525c --- /dev/null +++ b/app/src/main/kotlin/io/github/landwarderer/futon/core/prefs/ImportedTheme.kt @@ -0,0 +1,99 @@ +package io.github.landwarderer.futon.core.prefs + +import org.json.JSONObject +import java.util.Locale + +/** A validated Idle Fantasy-compatible theme with its original definition retained for storage. */ +data class ImportedTheme( + val id: String, + val displayName: String, + val isDark: Boolean, + val primaryColor: Int, + val onPrimaryColor: Int?, + val json: String, +) + +object ImportedThemeFile { + const val MAX_FILE_SIZE = 256 * 1024 + + fun parseImport(source: String): List { + require(source.length <= MAX_FILE_SIZE) { "Theme file is too large" } + return parse(JSONObject(source), requireNonEmpty = true) + } + + fun parseStored(source: String): List = runCatching { + parse(JSONObject(source), requireNonEmpty = false) + }.getOrDefault(emptyList()) + + fun mergeStored(current: String, imported: List): String { + val root = runCatching { JSONObject(current) }.getOrElse { JSONObject() } + for (theme in imported) { + root.put(theme.id, JSONObject(theme.json)) + } + return root.toString() + } + + private fun parse(root: JSONObject, requireNonEmpty: Boolean): List { + val result = ArrayList(root.length()) + val keys = root.keys() + while (keys.hasNext()) { + val sourceName = keys.next() + val id = sourceName.trim().lowercase(Locale.ROOT) + .replace(Regex("[^\\p{L}\\p{N}]+"), "_") + .trim('_') + require(id.isNotBlank() && id.length <= 80) { "Theme name is missing or too long" } + + val theme = root.getJSONObject(sourceName) + val base = theme.getString("base").lowercase(Locale.ROOT) + require(base == "light" || base == "dark") { "Theme '$sourceName' has an unsupported base" } + val colours = theme.getJSONObject("colours") + val scheme = theme.getJSONObject("colour_scheme") + require(colours.length() in 1..100) { "Theme '$sourceName' has no usable colours" } + require(scheme.length() in 1..100) { "Theme '$sourceName' has no colour roles" } + + val parsedColours = HashMap(colours.length()) + val colourNames = colours.keys() + while (colourNames.hasNext()) { + val name = colourNames.next() + parsedColours[name] = parseArgb(colours.getString(name)) + } + + val roleNames = HashMap(scheme.length()) + val roles = scheme.keys() + while (roles.hasNext()) { + val role = roles.next() + val colourName = scheme.getString(role) + require(parsedColours.containsKey(colourName)) { + "Theme '$sourceName' refers to missing colour '$colourName'" + } + roleNames[role] = colourName + } + val primaryName = roleNames["primary"] + ?: throw IllegalArgumentException("Theme '$sourceName' has no primary colour") + val onPrimary = roleNames["on_primary"]?.let(parsedColours::get) + val displayName = theme.optString("display_name").trim().ifEmpty { + sourceName.replace('_', ' ').replaceFirstChar { it.uppercase() } + } + + result += ImportedTheme( + id = id, + displayName = displayName, + isDark = base == "dark", + primaryColor = parsedColours.getValue(primaryName), + onPrimaryColor = onPrimary, + json = theme.toString(), + ) + } + require(!requireNonEmpty || result.isNotEmpty()) { "Theme file contains no themes" } + return result + } + + private fun parseArgb(value: String): Int { + val digits = value.trim().removePrefix("0x").removePrefix("0X").removePrefix("#") + require(digits.matches(Regex("[0-9a-fA-F]{6}([0-9a-fA-F]{2})?"))) { + "Invalid ARGB colour value" + } + val argb = if (digits.length == 6) "FF$digits" else digits + return argb.toLong(16).toInt() + } +} diff --git a/app/src/main/kotlin/io/github/landwarderer/futon/core/ui/BaseActivity.kt b/app/src/main/kotlin/io/github/landwarderer/futon/core/ui/BaseActivity.kt index 72afedc34f..aaa852519c 100644 --- a/app/src/main/kotlin/io/github/landwarderer/futon/core/ui/BaseActivity.kt +++ b/app/src/main/kotlin/io/github/landwarderer/futon/core/ui/BaseActivity.kt @@ -29,7 +29,10 @@ import io.github.landwarderer.futon.core.nav.AppRouter import io.github.landwarderer.futon.core.ui.util.ActionModeDelegate import io.github.landwarderer.futon.core.util.ext.isWebViewUnavailable import io.github.landwarderer.futon.main.ui.protect.ScreenshotPolicyHelper +import io.github.landwarderer.futon.core.prefs.ColorScheme import androidx.appcompat.R as appcompatR +import com.google.android.material.color.DynamicColors +import com.google.android.material.color.DynamicColorsOptions abstract class BaseActivity : AppCompatActivity(), @@ -60,7 +63,27 @@ abstract class BaseActivity : override fun onCreate(savedInstanceState: Bundle?) { val settings = entryPoint.settings isAmoledTheme = settings.isAmoledTheme - setTheme(settings.colorScheme.styleResId) + val importedTheme = settings.selectedCustomTheme.takeIf { settings.colorScheme == ColorScheme.CUSTOM } + // Content-based dynamic theming (setContentBasedSource) uses Material Color Utilities + // internally, which works on all API 23+ -- no Monet/API-31 requirement. + // Only fall back to the default when CUSTOM is selected but no theme is imported. + val colorScheme = when { + importedTheme == null && settings.colorScheme == ColorScheme.CUSTOM -> ColorScheme.default + else -> settings.colorScheme + } + setTheme(colorScheme.styleResId) + if (importedTheme != null) { + setTheme( + if (importedTheme.isDark) R.style.ThemeOverlay_Futon_Custom_Dark + else R.style.ThemeOverlay_Futon_Custom_Light, + ) + DynamicColors.applyToActivityIfAvailable( + this, + DynamicColorsOptions.Builder() + .setContentBasedSource(importedTheme.primaryColor) + .build(), + ) + } if (isAmoledTheme) { setTheme(R.style.ThemeOverlay_Futon_Amoled) } @@ -70,6 +93,7 @@ abstract class BaseActivity : super.onCreate(savedInstanceState) } + override fun onPostCreate(savedInstanceState: Bundle?) { super.onPostCreate(savedInstanceState) onBackPressedDispatcher.addCallback(actionModeDelegate) diff --git a/app/src/main/kotlin/io/github/landwarderer/futon/settings/AppearanceSettingsFragment.kt b/app/src/main/kotlin/io/github/landwarderer/futon/settings/AppearanceSettingsFragment.kt index 04aa5fbbf7..cfe521b852 100644 --- a/app/src/main/kotlin/io/github/landwarderer/futon/settings/AppearanceSettingsFragment.kt +++ b/app/src/main/kotlin/io/github/landwarderer/futon/settings/AppearanceSettingsFragment.kt @@ -7,15 +7,24 @@ import android.os.Build import android.os.Bundle import android.provider.Settings import android.view.View +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatDelegate import androidx.preference.ListPreference import androidx.preference.MultiSelectListPreference import androidx.preference.Preference import androidx.preference.TwoStatePreference +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.koitharu.kotatsu.parsers.util.runCatchingCancellable +import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import io.github.landwarderer.futon.R import io.github.landwarderer.futon.core.os.AppShortcutManager import io.github.landwarderer.futon.core.prefs.AppSettings +import io.github.landwarderer.futon.core.prefs.ColorScheme import io.github.landwarderer.futon.core.prefs.HomeBackgroundFont import io.github.landwarderer.futon.core.prefs.ListMode import io.github.landwarderer.futon.core.prefs.ProgressIndicatorMode @@ -27,6 +36,7 @@ import io.github.landwarderer.futon.core.ui.util.ActivityRecreationHandle import io.github.landwarderer.futon.core.util.LocaleComparator import io.github.landwarderer.futon.core.util.ext.getLocalesConfig import io.github.landwarderer.futon.core.util.ext.postDelayed +import io.github.landwarderer.futon.core.util.ext.printStackTraceDebug import io.github.landwarderer.futon.core.util.ext.setDefaultValueCompat import io.github.landwarderer.futon.core.util.ext.sortedWithSafe import io.github.landwarderer.futon.core.util.ext.toList @@ -38,6 +48,7 @@ import io.github.landwarderer.futon.settings.utils.ActivityListPreference import io.github.landwarderer.futon.settings.utils.MultiSummaryProvider import io.github.landwarderer.futon.settings.utils.PercentSummaryProvider import io.github.landwarderer.futon.settings.utils.SliderPreference +import io.github.landwarderer.futon.settings.utils.ThemeChooserPreference import javax.inject.Inject @AndroidEntryPoint @@ -45,6 +56,10 @@ class AppearanceSettingsFragment : BasePreferenceFragment(R.string.appearance), SharedPreferences.OnSharedPreferenceChangeListener { + private val customThemeFilePicker = registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri -> + if (uri != null) importCustomTheme(uri) + } + @Inject lateinit var activityRecreationHandle: ActivityRecreationHandle @@ -97,6 +112,19 @@ class AppearanceSettingsFragment : pref.summaryProvider = MultiSummaryProvider(R.string.none) pref.values = settings.searchSuggestionTypes.mapToSet { it.name } } + findPreference(KEY_CUSTOM_THEMES_MANAGEMENT)?.setOnPreferenceClickListener { + showCustomThemes() + true + } + findPreference(KEY_CUSTOM_THEME_IMPORT)?.setOnPreferenceClickListener { + customThemeFilePicker.launch(arrayOf("*/*")) + true + } + findPreference(KEY_CUSTOM_THEME_DELETE)?.setOnPreferenceClickListener { + confirmDeleteCustomTheme() + true + } + updateCustomThemeSummaries() bindNavSummary() } @@ -122,6 +150,14 @@ class AppearanceSettingsFragment : postRestart() } + AppSettings.KEY_CUSTOM_THEME -> { + updateCustomThemeSummaries() + // Restart so the newly selected custom theme is applied to all activities + if (settings.colorScheme == ColorScheme.CUSTOM) postRestart() + } + + AppSettings.KEY_CUSTOM_THEMES -> updateCustomThemeSummaries() + AppSettings.KEY_APP_LOCALE -> { AppCompatDelegate.setApplicationLocales(settings.appLocales) } @@ -160,6 +196,101 @@ class AppearanceSettingsFragment : } } + private fun showCustomThemes() { + val themes = settings.importedThemes + if (themes.isEmpty()) { + customThemeFilePicker.launch(arrayOf("*/*")) + return + } + + MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.custom_themes) + .setSingleChoiceItems( + themes.map { it.displayName }.toTypedArray(), + themes.indexOfFirst { it.id == settings.selectedCustomThemeId }, + ) { dialog, index -> + settings.selectedCustomThemeId = themes[index].id + findPreference(AppSettings.KEY_COLOR_THEME)?.value = + ColorScheme.CUSTOM.name + // Force restart in case the preference value didn't change (was already CUSTOM) + // so the newly selected theme is actually applied + postRestart() + updateCustomThemeSummaries() + dialog.dismiss() + } + .setNegativeButton(android.R.string.cancel, null) + .show() + } + + private fun importCustomTheme(uri: Uri) { + viewLifecycleOwner.lifecycleScope.launch { + val result = runCatchingCancellable { + // Read from ContentResolver on IO thread to avoid blocking the main thread + // when importing from cloud storage providers (Drive, Nextcloud, etc.) + val json = withContext(Dispatchers.IO) { readThemeFile(uri) } + val imported = settings.importCustomThemes(json) + settings.selectedCustomThemeId = imported.last().id + findPreference(AppSettings.KEY_COLOR_THEME)?.value = + ColorScheme.CUSTOM.name + findPreference(AppSettings.KEY_COLOR_THEME)?.refresh() + updateCustomThemeSummaries() + Toast.makeText(requireContext(), R.string.custom_theme_imported, Toast.LENGTH_SHORT).show() + } + result.onFailure { + it.printStackTraceDebug() + Toast.makeText(requireContext(), R.string.custom_theme_import_failed, Toast.LENGTH_LONG).show() + } + } + } + + private fun readThemeFile(uri: Uri): String { + val input = requireContext().contentResolver.openInputStream(uri) + ?: throw IllegalArgumentException("Cannot open theme file") + return input.bufferedReader(Charsets.UTF_8).use { reader -> + val result = StringBuilder() + val buffer = CharArray(4096) + while (true) { + val length = reader.read(buffer) + if (length < 0) break + require(result.length + length <= io.github.landwarderer.futon.core.prefs.ImportedThemeFile.MAX_FILE_SIZE) { + "Theme file is too large" + } + result.append(buffer, 0, length) + } + result.toString() + } + } + + private fun confirmDeleteCustomTheme() { + val selected = settings.selectedCustomTheme + if (selected == null) { + Toast.makeText(requireContext(), R.string.custom_themes_none, Toast.LENGTH_SHORT).show() + return + } + MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.custom_theme_delete) + .setMessage(getString(R.string.custom_theme_delete_confirm, selected.displayName)) + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(android.R.string.ok) { _, _ -> + settings.deleteCustomTheme(selected.id) + if (settings.colorScheme == ColorScheme.CUSTOM) { + findPreference(AppSettings.KEY_COLOR_THEME)?.value = + ColorScheme.default.name + } + findPreference(AppSettings.KEY_COLOR_THEME)?.refresh() + updateCustomThemeSummaries() + } + .show() + } + + private fun updateCustomThemeSummaries() { + val customTheme = settings.selectedCustomTheme + val emptySummary = getString(R.string.custom_themes_none) + findPreference(KEY_CUSTOM_THEMES_MANAGEMENT)?.summary = + customTheme?.displayName ?: emptySummary + findPreference(KEY_CUSTOM_THEME_DELETE)?.summary = customTheme?.displayName ?: emptySummary + } + private fun initLocalePicker(preference: ListPreference) { val locales = preference.context.getLocalesConfig() .toList() @@ -187,4 +318,10 @@ class AppearanceSettingsFragment : getString(it.title) } } + + private companion object { + const val KEY_CUSTOM_THEMES_MANAGEMENT = "custom_themes_management" + const val KEY_CUSTOM_THEME_IMPORT = "custom_theme_import" + const val KEY_CUSTOM_THEME_DELETE = "custom_theme_delete" + } } diff --git a/app/src/main/kotlin/io/github/landwarderer/futon/settings/utils/ThemeChooserPreference.kt b/app/src/main/kotlin/io/github/landwarderer/futon/settings/utils/ThemeChooserPreference.kt index 0279fd40d4..4865621662 100644 --- a/app/src/main/kotlin/io/github/landwarderer/futon/settings/utils/ThemeChooserPreference.kt +++ b/app/src/main/kotlin/io/github/landwarderer/futon/settings/utils/ThemeChooserPreference.kt @@ -19,6 +19,7 @@ import androidx.preference.Preference import androidx.preference.PreferenceViewHolder import io.github.landwarderer.futon.R import io.github.landwarderer.futon.core.prefs.ColorScheme +import io.github.landwarderer.futon.core.prefs.AppSettings import io.github.landwarderer.futon.databinding.ItemColorSchemeBinding import io.github.landwarderer.futon.databinding.PreferenceThemeBinding import java.lang.ref.WeakReference @@ -31,7 +32,9 @@ class ThemeChooserPreference @JvmOverloads constructor( defStyleRes: Int = R.style.Preference_ThemeChooser, ) : Preference(context, attrs, defStyleAttr, defStyleRes) { - private val entries = ColorScheme.getAvailableList() + private val settings = AppSettings(context) + private val entries: List + get() = ColorScheme.getAvailableList(includeCustom = settings.importedThemes.isNotEmpty()) private var currentValue: ColorScheme = ColorScheme.default private val lastScrollPosition = intArrayOf(-1) private val itemClickListener = View.OnClickListener { @@ -53,18 +56,30 @@ class ThemeChooserPreference @JvmOverloads constructor( } binding.linear.removeAllViews() for (theme in entries) { - val context = ContextThemeWrapper(context, theme.styleResId) - val item = - ItemColorSchemeBinding.inflate(LayoutInflater.from(context), binding.linear, false) + val customTheme = settings.selectedCustomTheme.takeIf { theme == ColorScheme.CUSTOM } + val styleResId = when { + theme == ColorScheme.CUSTOM && customTheme?.isDark == true -> R.style.ThemeOverlay_Futon_Custom_Dark + else -> theme.styleResId + } + val themedContext = ContextThemeWrapper(context, styleResId) + val item = ItemColorSchemeBinding.inflate(LayoutInflater.from(themedContext), binding.linear, false) if (binding.linear.isEmpty()) { item.root.updatePaddingRelative(start = 0) } val isSelected = theme == currentValue item.card.isChecked = isSelected - item.card.strokeWidth = if (isSelected) context.resources.getDimensionPixelSize( + item.card.strokeWidth = if (isSelected) themedContext.resources.getDimensionPixelSize( materialR.dimen.m3_comp_outlined_card_outline_width, ) else 0 - item.textViewTitle.setText(theme.titleResId) + item.textViewTitle.text = customTheme?.displayName ?: themedContext.getString(theme.titleResId) + if (customTheme != null) { + // Tint the preview shapes with the user's seed color instead of flattening + // the card background (which would erase shape_1/shape_2 visual indicators). + // Leave textViewTitle using its default ?android:attr/textColorPrimary so the + // label stays legible regardless of light/dark mode. + item.shape1.backgroundTintList = android.content.res.ColorStateList.valueOf(customTheme.primaryColor) + item.shape2.backgroundTintList = android.content.res.ColorStateList.valueOf(customTheme.primaryColor) + } item.root.tag = theme item.card.tag = theme item.imageViewCheck.isVisible = theme == currentValue @@ -123,6 +138,9 @@ class ThemeChooserPreference @JvmOverloads constructor( lastScrollPosition[0] = state.scrollPosition } + /** Externally refresh this preference's view when custom-theme data has changed. */ + fun refresh() = notifyChanged() + private fun setValueInternal(enumName: String, notifyChanged: Boolean) { val newValue = ColorScheme.safeValueOf(enumName) ?: return if (newValue != currentValue) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6ecca0ef00..6c03fcff28 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -365,6 +365,15 @@ Dynamic Expressive (Test) Color scheme + Custom + Custom themes + No custom themes imported + Import theme file + Import an Idle Fantasy compatible Material 3 theme JSON + Theme imported and applied + Could not import this theme file + Delete selected theme + Delete ā€œ%1$sā€? Show in grid view Miku Asuka diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 9cb548a6e6..7fcd3af316 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -84,6 +84,8 @@