diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.kt b/AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.kt index c18795a28ad5..b058b096bb43 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.kt @@ -1443,7 +1443,7 @@ abstract class AbstractFlashcardViewer : } internal val isInNightMode: Boolean - get() = Themes.currentTheme.isNightMode + get() = Themes.isNightTheme private fun updateCard(content: RenderedCard) { Timber.d("updateCard()") diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/AnkiActivity.kt b/AnkiDroid/src/main/java/com/ichi2/anki/AnkiActivity.kt index c77a530a3b53..df68b219cd73 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/AnkiActivity.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/AnkiActivity.kt @@ -493,7 +493,7 @@ open class AnkiActivity( get() = if (AppCompatDelegate.getDefaultNightMode() == AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) { COLOR_SCHEME_SYSTEM - } else if (Themes.currentTheme.isNightMode) { + } else if (Themes.isNightTheme) { COLOR_SCHEME_DARK } else { COLOR_SCHEME_LIGHT diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/DrawingFragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/DrawingFragment.kt index 24ca9d30c97c..b304c593745f 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/DrawingFragment.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/DrawingFragment.kt @@ -85,7 +85,7 @@ class DrawingFragment : Fragment(R.layout.drawing_fragment) { val canvas = Canvas(bitmap) val backgroundColor = - if (Themes.currentTheme.isNightMode) { + if (Themes.isNightTheme) { Color.BLACK } else { Color.WHITE diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt b/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt index d4f928cb6c8b..188ff2b8336d 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt @@ -113,6 +113,7 @@ import com.ichi2.anki.scheduling.registerOnForgetHandler import com.ichi2.anki.servicelayer.NoteService.isMarked import com.ichi2.anki.servicelayer.NoteService.toggleMark import com.ichi2.anki.settings.Prefs +import com.ichi2.anki.settings.enums.DayTheme import com.ichi2.anki.snackbar.showSnackbar import com.ichi2.anki.ui.internationalization.toSentenceCase import com.ichi2.anki.ui.windows.reviewer.ReviewerFragment @@ -1684,7 +1685,7 @@ open class Reviewer : } whiteboard.onPaintColorChangeListener = OnPaintColorChangeListener { color -> - MetaDB.storeWhiteboardPenColor(this@Reviewer, parentDid, !currentTheme.isNightMode, color) + MetaDB.storeWhiteboardPenColor(this@Reviewer, parentDid, currentTheme is DayTheme, color) } whiteboard.setOnTouchListener { v: View, event: MotionEvent? -> if (event == null) return@setOnTouchListener false diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/Whiteboard.kt b/AnkiDroid/src/main/java/com/ichi2/anki/Whiteboard.kt index 51423ee3408b..eeb063756ddc 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/Whiteboard.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/Whiteboard.kt @@ -45,6 +45,7 @@ import com.ichi2.anki.common.time.Time import com.ichi2.anki.common.time.getTimestamp import com.ichi2.anki.dialogs.WhiteBoardWidthDialog import com.ichi2.anki.preferences.sharedPrefs +import com.ichi2.anki.settings.enums.NightTheme import com.ichi2.compat.CompatHelper import com.ichi2.themes.Themes.currentTheme import com.ichi2.utils.DisplayUtils.getDisplayDimensions @@ -584,7 +585,7 @@ class Whiteboard( handleMultiTouch: Boolean, whiteboardMultiTouchMethods: WhiteboardMultiTouchMethods?, ): Whiteboard { - val whiteboard = Whiteboard(context, handleMultiTouch, currentTheme.isNightMode) + val whiteboard = Whiteboard(context, handleMultiTouch, currentTheme is NightTheme) Companion.whiteboardMultiTouchMethods = whiteboardMultiTouchMethods val lp2 = FrameLayout.LayoutParams( diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/browser/BrowserMultiColumnAdapter.kt b/AnkiDroid/src/main/java/com/ichi2/anki/browser/BrowserMultiColumnAdapter.kt index 9a546abd0548..8c58b5dffd5d 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/browser/BrowserMultiColumnAdapter.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/browser/BrowserMultiColumnAdapter.kt @@ -164,7 +164,7 @@ class BrowserMultiColumnAdapter( fun setColor( @ColorInt color: Int, ) { - val nightMode = Themes.currentTheme.isNightMode + val nightMode = Themes.isNightTheme val pressedColor: Int val focusedColor: Int diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/cardviewer/CardAppearance.kt b/AnkiDroid/src/main/java/com/ichi2/anki/cardviewer/CardAppearance.kt index 033b67c529ae..ddbe50307843 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/cardviewer/CardAppearance.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/cardviewer/CardAppearance.kt @@ -18,7 +18,8 @@ package com.ichi2.anki.cardviewer import android.content.SharedPreferences import androidx.annotation.CheckResult import com.ichi2.anki.reviewer.ReviewerCustomFonts -import com.ichi2.themes.Theme +import com.ichi2.anki.settings.enums.DayTheme +import com.ichi2.anki.settings.enums.NightTheme import com.ichi2.themes.Themes.currentTheme /** Responsible for calculating CSS and element styles and modifying content on a flashcard */ @@ -47,17 +48,17 @@ class CardAppearance( if (centerVertically) { cardClass.append(" vertically_centered") } - if (currentTheme.isNightMode) { + if (currentTheme is NightTheme) { // Enable the night-mode class cardClass.append(" night_mode nightMode") // Emit the dark_mode selector to allow dark theme overrides - if (currentTheme == Theme.DARK) { + if (currentTheme == NightTheme.DARK) { cardClass.append(" ankidroid_dark_mode") } } else { // Emit the plain_mode selector to allow plain theme overrides - if (currentTheme == Theme.PLAIN) { + if (currentTheme == DayTheme.PLAIN) { cardClass.append(" ankidroid_plain_mode") } } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/model/WhiteboardPenColor.kt b/AnkiDroid/src/main/java/com/ichi2/anki/model/WhiteboardPenColor.kt index 68997049e5cc..99888581eae6 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/model/WhiteboardPenColor.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/model/WhiteboardPenColor.kt @@ -17,14 +17,14 @@ package com.ichi2.anki.model import androidx.annotation.CheckResult -import com.ichi2.themes.Themes.currentTheme +import com.ichi2.themes.Themes class WhiteboardPenColor( val lightPenColor: Int?, val darkPenColor: Int?, ) { fun fromPreferences(): Int? = - if (currentTheme.isNightMode) { + if (Themes.isNightTheme) { darkPenColor } else { lightPenColor diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/pages/PageFragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/pages/PageFragment.kt index d1f50338ea38..657268d1a74f 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/pages/PageFragment.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/pages/PageFragment.kt @@ -125,7 +125,7 @@ abstract class PageFragment( setupBridgeCommand(pageWebViewClient) onWebViewCreated() } - val nightMode = if (Themes.currentTheme.isNightMode) "#night" else "" + val nightMode = if (Themes.isNightTheme) "#night" else "" val url = "${server.baseUrl()}$pagePath$nightMode".toUri() Timber.i("Loading $url") webViewLayout.loadUrl(url.toString()) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/preferences/AppearanceSettingsFragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/preferences/AppearanceSettingsFragment.kt index 0c2fafcc6d73..35c7b8d88758 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/preferences/AppearanceSettingsFragment.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/preferences/AppearanceSettingsFragment.kt @@ -29,10 +29,10 @@ import com.ichi2.anki.deckpicker.BackgroundImage import com.ichi2.anki.deckpicker.BackgroundImage.FileSizeResult import com.ichi2.anki.launchCatchingTask import com.ichi2.anki.settings.Prefs +import com.ichi2.anki.settings.enums.AppTheme import com.ichi2.anki.showThemedToast import com.ichi2.anki.snackbar.showSnackbar import com.ichi2.anki.utils.CollectionPreferences -import com.ichi2.themes.Theme import com.ichi2.themes.Themes import com.ichi2.themes.Themes.systemIsInNightMode import com.ichi2.themes.Themes.updateCurrentTheme @@ -72,56 +72,6 @@ class AppearanceSettingsFragment : SettingsFragment() { // Initially update visibility based on whether a background exists updateRemoveBackgroundVisibility() - val appThemePref = requirePreference(R.string.app_theme_key) - val dayThemePref = requirePreference(R.string.day_theme_key) - val nightThemePref = requirePreference(R.string.night_theme_key) - val themeIsFollowSystem = appThemePref.value == Themes.FOLLOW_SYSTEM_MODE - - // Remove follow system options in android versions which do not have system dark mode - // When minSdk reaches 29, the only necessary change is to remove this if-block - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { - dayThemePref.isVisible = false - nightThemePref.isVisible = false - - // Drop "Follow system" option (the first one) - appThemePref.entries = resources.getStringArray(R.array.app_theme_labels).drop(1).toTypedArray() - appThemePref.entryValues = resources.getStringArray(R.array.app_theme_values).drop(1).toTypedArray() - if (themeIsFollowSystem) { - appThemePref.value = Theme.fallback.id - } - } - dayThemePref.isEnabled = themeIsFollowSystem - nightThemePref.isEnabled = themeIsFollowSystem - - appThemePref.setOnPreferenceChangeListener { newValue -> - val selectedThemeIsFollowSystem = newValue == Themes.FOLLOW_SYSTEM_MODE - dayThemePref.isEnabled = selectedThemeIsFollowSystem - nightThemePref.isEnabled = selectedThemeIsFollowSystem - - // Only restart if theme has changed - if (newValue != appThemePref.value) { - val previousThemeId = Themes.currentTheme.id - appThemePref.value = newValue - updateCurrentTheme(requireContext()) - - if (previousThemeId != Themes.currentTheme.id) { - ActivityCompat.recreate(requireActivity()) - } - } - } - - dayThemePref.setOnPreferenceChangeListener { newValue -> - if (newValue != dayThemePref.value && !systemIsInNightMode(requireContext()) && newValue != Themes.currentTheme.id) { - ActivityCompat.recreate(requireActivity()) - } - } - - nightThemePref.setOnPreferenceChangeListener { newValue -> - if (newValue != nightThemePref.value && systemIsInNightMode(requireContext()) && newValue != Themes.currentTheme.id) { - ActivityCompat.recreate(requireActivity()) - } - } - // Show estimate time // Represents the collection pref "estTime": i.e. // whether the buttons should indicate the duration of the interval if we click on them. @@ -151,6 +101,7 @@ class AppearanceSettingsFragment : SettingsFragment() { } } + setupThemePreferences() setupNewStudyScreenSettings() } @@ -173,6 +124,54 @@ class AppearanceSettingsFragment : SettingsFragment() { } } + private fun setupThemePreferences() { + val appTheme = Prefs.appTheme + val appThemePref = requirePreference(R.string.app_theme_key) + val dayThemePref = requirePreference(R.string.day_theme_key) + val nightThemePref = requirePreference(R.string.night_theme_key) + + // Remove follow system options in android versions which do not have system dark mode + // When minSdk reaches 29, the only necessary change is to remove this if-block + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + // Drop "Follow system" option (the first one) + appThemePref.entries = resources.getStringArray(R.array.app_theme_labels).drop(1).toTypedArray() + appThemePref.entryValues = resources.getStringArray(R.array.app_theme_values).drop(1).toTypedArray() + if (appTheme == AppTheme.FOLLOW_SYSTEM) { + appThemePref.value = getString(Themes.currentTheme.entryResId) + } + } + + appThemePref.setOnPreferenceChangeListener { newValue -> + if (newValue != appThemePref.value) { + val previousThemeId = Themes.currentTheme.styleResId + appThemePref.value = newValue + updateCurrentTheme(requireContext()) + + if (previousThemeId != Themes.currentTheme.styleResId) { + ActivityCompat.recreate(requireActivity()) + } + } + } + + dayThemePref.setOnPreferenceChangeListener { newValue -> + if ( + newValue != dayThemePref.value && + (appTheme == AppTheme.DAY || (appTheme == AppTheme.FOLLOW_SYSTEM && !systemIsInNightMode(requireContext()))) + ) { + ActivityCompat.recreate(requireActivity()) + } + } + + nightThemePref.setOnPreferenceChangeListener { newValue -> + if ( + newValue != nightThemePref.value && + (appTheme == AppTheme.NIGHT || (appTheme == AppTheme.FOLLOW_SYSTEM && systemIsInNightMode(requireContext()))) + ) { + ActivityCompat.recreate(requireActivity()) + } + } + } + private val backgroundImageResultLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { selectedImage -> if (selectedImage == null) { diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/previewer/CardViewerFragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/previewer/CardViewerFragment.kt index 9d34da162c04..71f4f22e3a5b 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/previewer/CardViewerFragment.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/previewer/CardViewerFragment.kt @@ -91,7 +91,7 @@ abstract class CardViewerFragment( protected open fun onLoadInitialHtml(): String = stdHtml( context = requireContext(), - nightMode = Themes.currentTheme.isNightMode, + nightMode = Themes.isNightTheme, ) private fun setupWebView(savedInstanceState: Bundle?) { diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/previewer/PreviewerHelpers.kt b/AnkiDroid/src/main/java/com/ichi2/anki/previewer/PreviewerHelpers.kt index b0848c4486cf..378e9b7fdd43 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/previewer/PreviewerHelpers.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/previewer/PreviewerHelpers.kt @@ -88,7 +88,7 @@ fun stdHtml( */ fun bodyClassForCardOrd( cardOrd: Int, - nightMode: Boolean = Themes.currentTheme.isNightMode, + nightMode: Boolean = Themes.isNightTheme, ): String = "card card${cardOrd + 1} ${bodyClass(nightMode)} mathjax-rendered" -private fun bodyClass(nightMode: Boolean = Themes.currentTheme.isNightMode): String = if (nightMode) "nightMode night_mode" else "" +private fun bodyClass(nightMode: Boolean = Themes.isNightTheme): String = if (nightMode) "nightMode night_mode" else "" diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/servicelayer/PreferenceUpgradeService.kt b/AnkiDroid/src/main/java/com/ichi2/anki/servicelayer/PreferenceUpgradeService.kt index d3edadbff11e..905b0e4ba6b2 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/servicelayer/PreferenceUpgradeService.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/servicelayer/PreferenceUpgradeService.kt @@ -133,6 +133,7 @@ object PreferenceUpgradeService { yield(RemoveHostNum()) yield(UpgradeHideAnswerButtons()) yield(UpgradeToggleBacksideOnlyControl()) + yield(UpgradeThemes()) } /** Returns a list of preference upgrade classes which have not been applied */ @@ -750,6 +751,44 @@ object PreferenceUpgradeService { } } } + + internal class UpgradeThemes : PreferenceUpgrade(26) { + companion object { + const val KEY_APP_THEME = "appTheme" + const val KEY_DAY_THEME = "dayTheme" + const val KEY_NIGHT_THEME = "nightTheme" + + const val THEME_FOLLOW_SYSTEM = "0" + const val THEME_LIGHT = "1" + const val THEME_PLAIN = "2" + const val THEME_BLACK = "3" + const val THEME_DARK = "4" + + const val THEME_DAY = "1" + const val THEME_NIGHT = "2" + } + + @Suppress("MoveVariableDeclarationIntoWhen") + override fun upgrade(preferences: SharedPreferences) { + val appTheme = preferences.getString(KEY_APP_THEME, THEME_FOLLOW_SYSTEM) + + when (appTheme) { + THEME_FOLLOW_SYSTEM -> return + THEME_LIGHT, THEME_PLAIN -> { + preferences.edit { + putString(KEY_APP_THEME, THEME_DAY) + putString(KEY_DAY_THEME, appTheme) + } + } + THEME_BLACK, THEME_DARK -> { + preferences.edit { + putString(KEY_APP_THEME, THEME_NIGHT) + putString(KEY_NIGHT_THEME, appTheme) + } + } + } + } + } } } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/settings/Prefs.kt b/AnkiDroid/src/main/java/com/ichi2/anki/settings/Prefs.kt index f6b48851ddd4..1bab710194fb 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/settings/Prefs.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/settings/Prefs.kt @@ -27,8 +27,11 @@ import com.ichi2.anki.R import com.ichi2.anki.cardviewer.TapGestureMode import com.ichi2.anki.common.utils.isRunningAsUnitTest import com.ichi2.anki.preferences.sharedPrefs +import com.ichi2.anki.settings.enums.AppTheme +import com.ichi2.anki.settings.enums.DayTheme import com.ichi2.anki.settings.enums.FrameStyle import com.ichi2.anki.settings.enums.HideSystemBars +import com.ichi2.anki.settings.enums.NightTheme import com.ichi2.anki.settings.enums.PrefEnum import com.ichi2.anki.settings.enums.ShouldFetchMedia import com.ichi2.anki.settings.enums.ToolbarPosition @@ -43,6 +46,8 @@ open class PrefsRepository( val sharedPrefs: SharedPreferences, private val resources: Resources, ) { + constructor(context: Context) : this(context.sharedPrefs(), context.resources) + @VisibleForTesting fun key( @StringRes resId: Int, @@ -289,6 +294,14 @@ open class PrefsRepository( val hideSystemBars: HideSystemBars by enumPref(R.string.hide_system_bars_key, HideSystemBars.NONE) val toolbarPosition: ToolbarPosition by enumPref(R.string.reviewer_toolbar_position_key, ToolbarPosition.TOP) + //region Appearance + + val appTheme: AppTheme by enumPref(R.string.app_theme_key, AppTheme.FOLLOW_SYSTEM) + val dayTheme: DayTheme by enumPref(R.string.day_theme_key, DayTheme.LIGHT) + val nightTheme: NightTheme by enumPref(R.string.night_theme_key, NightTheme.DARK) + + //endregion + // **************************************** Controls **************************************** // //region Controls diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/settings/enums/AppTheme.kt b/AnkiDroid/src/main/java/com/ichi2/anki/settings/enums/AppTheme.kt new file mode 100644 index 000000000000..2a949efd7795 --- /dev/null +++ b/AnkiDroid/src/main/java/com/ichi2/anki/settings/enums/AppTheme.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2025 Brayan Oliveira <69634269+brayandso@users.noreply.github.com> + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +package com.ichi2.anki.settings.enums + +import com.ichi2.anki.R + +/** [R.array.app_theme_values] */ +enum class AppTheme( + override val entryResId: Int, +) : PrefEnum { + FOLLOW_SYSTEM(R.string.theme_follow_system_value), + DAY(R.string.theme_day_scheme_value), + NIGHT(R.string.theme_night_scheme_value), +} diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/settings/enums/DayTheme.kt b/AnkiDroid/src/main/java/com/ichi2/anki/settings/enums/DayTheme.kt new file mode 100644 index 000000000000..bc5117eec1f0 --- /dev/null +++ b/AnkiDroid/src/main/java/com/ichi2/anki/settings/enums/DayTheme.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2025 Brayan Oliveira <69634269+brayandso@users.noreply.github.com> + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +package com.ichi2.anki.settings.enums + +import androidx.annotation.StyleRes +import com.ichi2.anki.R + +sealed interface Theme : PrefEnum { + @get:StyleRes + val styleResId: Int +} + +/** [R.array.day_theme_values] */ +enum class DayTheme( + override val entryResId: Int, + override val styleResId: Int, +) : Theme { + LIGHT(R.string.theme_light_value, R.style.Theme_Light), + PLAIN(R.string.theme_plain_value, R.style.Theme_Light_Plain), +} diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/settings/enums/NightTheme.kt b/AnkiDroid/src/main/java/com/ichi2/anki/settings/enums/NightTheme.kt new file mode 100644 index 000000000000..0dfbc8451716 --- /dev/null +++ b/AnkiDroid/src/main/java/com/ichi2/anki/settings/enums/NightTheme.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2025 Brayan Oliveira <69634269+brayandso@users.noreply.github.com> + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +package com.ichi2.anki.settings.enums + +import com.ichi2.anki.R + +/** [R.array.night_theme_values] */ +enum class NightTheme( + override val entryResId: Int, + override val styleResId: Int, +) : Theme { + BLACK(R.string.theme_black_value, R.style.Theme_Dark_Black), + DARK(R.string.theme_dark_value, R.style.Theme_Dark), +} diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/reviewer/ReviewerFragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/reviewer/ReviewerFragment.kt index 43135acdc4cf..160bb4c9edd2 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/reviewer/ReviewerFragment.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/ui/windows/reviewer/ReviewerFragment.kt @@ -138,7 +138,7 @@ class ReviewerFragment : stdHtml( context = requireContext(), extraJsAssets = listOf("scripts/ankidroid-reviewer.js"), - nightMode = Themes.currentTheme.isNightMode, + nightMode = Themes.isNightTheme, ) override fun onStart() { diff --git a/AnkiDroid/src/main/java/com/ichi2/themes/Theme.kt b/AnkiDroid/src/main/java/com/ichi2/themes/Theme.kt deleted file mode 100644 index 3cb6e17c8627..000000000000 --- a/AnkiDroid/src/main/java/com/ichi2/themes/Theme.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (c) 2022 Brayan Oliveira - * - * This program is free software; you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by the Free Software - * Foundation; either version 3 of the License, or (at your option) any later - * version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A - * PARTICULAR PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with - * this program. If not, see . - */ -package com.ichi2.themes - -import androidx.annotation.StyleRes -import com.ichi2.anki.R - -enum class Theme( - val id: String, - @StyleRes val resId: Int, - val isNightMode: Boolean, -) { - // IDs must correspond to the ones at @array/app_theme_values on res/values/constants.xml - // Follow system is "0", so it starts at "1" - LIGHT("1", R.style.Theme_Light, false), - PLAIN("2", R.style.Theme_Light_Plain, false), - BLACK("3", R.style.Theme_Dark_Black, true), - DARK("4", R.style.Theme_Dark, true), - ; - - companion object { - val fallback: Theme - get() = LIGHT - - fun ofId(id: String): Theme = entries.find { it.id == id } ?: fallback - } -} diff --git a/AnkiDroid/src/main/java/com/ichi2/themes/Themes.kt b/AnkiDroid/src/main/java/com/ichi2/themes/Themes.kt index 2ae45b21cebc..f287435c6ed0 100644 --- a/AnkiDroid/src/main/java/com/ichi2/themes/Themes.kt +++ b/AnkiDroid/src/main/java/com/ichi2/themes/Themes.kt @@ -19,7 +19,6 @@ package com.ichi2.themes import android.content.Context -import android.content.SharedPreferences import android.content.res.Configuration import android.graphics.Color import androidx.annotation.ColorInt @@ -31,9 +30,12 @@ import androidx.fragment.app.FragmentActivity import com.google.android.material.color.MaterialColors import com.ichi2.anki.AnkiDroidApp import com.ichi2.anki.R -import com.ichi2.anki.preferences.sharedPrefs +import com.ichi2.anki.settings.PrefsRepository +import com.ichi2.anki.settings.enums.AppTheme +import com.ichi2.anki.settings.enums.DayTheme +import com.ichi2.anki.settings.enums.NightTheme +import com.ichi2.anki.settings.enums.Theme import com.ichi2.ui.AppCompatPreferenceActivity -import timber.log.Timber /** * Helper methods to configure things related to AnkiDroid's themes @@ -42,17 +44,12 @@ object Themes { const val ALPHA_ICON_ENABLED_LIGHT = 255 // 100% const val ALPHA_ICON_DISABLED_LIGHT = 76 // 31% - const val FOLLOW_SYSTEM_MODE = "0" - private const val APP_THEME_KEY = "appTheme" - private const val DAY_THEME_KEY = "dayTheme" - private const val NIGHT_THEME_KEY = "nightTheme" - - var currentTheme: Theme = Theme.fallback + var currentTheme: Theme = DayTheme.LIGHT + val isNightTheme: Boolean get() = currentTheme is NightTheme fun setTheme(context: Context) { updateCurrentTheme(context) - Timber.i("Setting theme to %s", currentTheme.name) - context.setTheme(currentTheme.resId) + context.setTheme(currentTheme.styleResId) } fun setLegacyActionBar(context: Context) { @@ -69,27 +66,29 @@ object Themes { // AppCompatPreferenceActivity's sharedPreferences is initialized // after the time when the theme should be set // TODO (#5019): always use the context as the parameter for getSharedPrefs - val prefs = + val prefsContext = if (context is AppCompatPreferenceActivity<*>) { - AnkiDroidApp.instance.sharedPrefs() + AnkiDroidApp.instance } else { - context.sharedPrefs() + context } + val prefs = PrefsRepository(prefsContext) + val appTheme = prefs.appTheme + val themeIsDark = (appTheme == AppTheme.FOLLOW_SYSTEM && systemIsInNightMode(context)) || appTheme == AppTheme.NIGHT currentTheme = - if (themeFollowsSystem(prefs)) { - AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) - if (systemIsInNightMode(context)) { - Theme.ofId(prefs.getString(NIGHT_THEME_KEY, Theme.BLACK.id)!!) - } else { - Theme.ofId(prefs.getString(DAY_THEME_KEY, Theme.LIGHT.id)!!) - } + if (themeIsDark) { + prefs.nightTheme } else { - Theme.ofId(prefs.getString(APP_THEME_KEY, Theme.fallback.id)!!).also { - val mode = if (it.isNightMode) AppCompatDelegate.MODE_NIGHT_YES else AppCompatDelegate.MODE_NIGHT_NO - AppCompatDelegate.setDefaultNightMode(mode) - } + prefs.dayTheme + } + val defaultNightMode = + when (appTheme) { + AppTheme.FOLLOW_SYSTEM -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM + AppTheme.DAY -> AppCompatDelegate.MODE_NIGHT_NO + AppTheme.NIGHT -> AppCompatDelegate.MODE_NIGHT_YES } + AppCompatDelegate.setDefaultNightMode(defaultNightMode) } /** @@ -139,12 +138,6 @@ object Themes { return attrs } - /** - * @return if current selected theme is `Follow system` - */ - private fun themeFollowsSystem(sharedPreferences: SharedPreferences): Boolean = - sharedPreferences.getString(APP_THEME_KEY, FOLLOW_SYSTEM_MODE) == FOLLOW_SYSTEM_MODE - fun systemIsInNightMode(context: Context): Boolean = context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES @@ -153,7 +146,7 @@ object Themes { @Suppress("deprecation", "API35 properly handle edge-to-edge") fun FragmentActivity.setTransparentStatusBar() { WindowInsetsControllerCompat(window, window.decorView).isAppearanceLightStatusBars = - !Themes.currentTheme.isNightMode + Themes.currentTheme !is NightTheme window.statusBarColor = Color.TRANSPARENT } diff --git a/AnkiDroid/src/main/res/values/11-arrays.xml b/AnkiDroid/src/main/res/values/11-arrays.xml index 9321771fb008..518c21c33df7 100644 --- a/AnkiDroid/src/main/res/values/11-arrays.xml +++ b/AnkiDroid/src/main/res/values/11-arrays.xml @@ -29,6 +29,8 @@ Follow system + Day + Night Light diff --git a/AnkiDroid/src/main/res/values/constants.xml b/AnkiDroid/src/main/res/values/constants.xml index 24717a6d5a52..ef1734f0e14d 100644 --- a/AnkiDroid/src/main/res/values/constants.xml +++ b/AnkiDroid/src/main/res/values/constants.xml @@ -164,29 +164,35 @@ @string/full_screen_system @string/full_screen_complete + @string/theme_follow_system - @string/day_theme_light - @string/day_theme_plain - @string/night_theme_black - @string/night_theme_dark + @string/theme_day + @string/theme_night + + 0 + 1 + 2 + + 1 + 2 + 3 + 4 + - 0 - 1 - 2 - 3 - 4 + @string/theme_follow_system_value + @string/theme_day_scheme_value + @string/theme_night_scheme_value - - 1 - 2 + @string/theme_light_value + @string/theme_plain_value - 3 - 4 + @string/theme_black_value + @string/theme_dark_value diff --git a/AnkiDroid/src/main/res/xml/preferences_appearance.xml b/AnkiDroid/src/main/res/xml/preferences_appearance.xml index 123c322c5e1b..e9e1151c5baa 100644 --- a/AnkiDroid/src/main/res/xml/preferences_appearance.xml +++ b/AnkiDroid/src/main/res/xml/preferences_appearance.xml @@ -29,7 +29,7 @@ android:key="@string/pref_appearance_screen_key"> Should remain Follow System, no sub-themes set + prefs.edit { putString(UpgradeThemes.KEY_APP_THEME, UpgradeThemes.THEME_FOLLOW_SYSTEM) } + upgrade.performUpgrade(prefs) + assertThat(prefs.getString(UpgradeThemes.KEY_APP_THEME, null), equalTo(UpgradeThemes.THEME_FOLLOW_SYSTEM)) + assertThat(prefs.contains(UpgradeThemes.KEY_DAY_THEME), equalTo(false)) + assertThat(prefs.contains(UpgradeThemes.KEY_NIGHT_THEME), equalTo(false)) + + // Light -> Day + dayTheme: Light + prefs.edit { + putString(UpgradeThemes.KEY_APP_THEME, UpgradeThemes.THEME_LIGHT) + remove(UpgradeThemes.KEY_DAY_THEME) + } + upgrade.performUpgrade(prefs) + assertThat(prefs.getString(UpgradeThemes.KEY_APP_THEME, null), equalTo(UpgradeThemes.THEME_DAY)) + assertThat(prefs.getString(UpgradeThemes.KEY_DAY_THEME, null), equalTo(UpgradeThemes.THEME_LIGHT)) + + // Plain -> Day + dayTheme: Plain + prefs.edit { + putString(UpgradeThemes.KEY_APP_THEME, UpgradeThemes.THEME_PLAIN) + remove(UpgradeThemes.KEY_DAY_THEME) + } + upgrade.performUpgrade(prefs) + assertThat(prefs.getString(UpgradeThemes.KEY_APP_THEME, null), equalTo(UpgradeThemes.THEME_DAY)) + assertThat(prefs.getString(UpgradeThemes.KEY_DAY_THEME, null), equalTo(UpgradeThemes.THEME_PLAIN)) + + // Black -> Night + nightTheme: Black + prefs.edit { + putString(UpgradeThemes.KEY_APP_THEME, UpgradeThemes.THEME_BLACK) + remove(UpgradeThemes.KEY_NIGHT_THEME) + } + upgrade.performUpgrade(prefs) + assertThat(prefs.getString(UpgradeThemes.KEY_APP_THEME, null), equalTo(UpgradeThemes.THEME_NIGHT)) + assertThat(prefs.getString(UpgradeThemes.KEY_NIGHT_THEME, null), equalTo(UpgradeThemes.THEME_BLACK)) + + // Dark -> Night + nightTheme: Dark + prefs.edit { + putString(UpgradeThemes.KEY_APP_THEME, UpgradeThemes.THEME_DARK) + remove(UpgradeThemes.KEY_NIGHT_THEME) + } + upgrade.performUpgrade(prefs) + assertThat(prefs.getString(UpgradeThemes.KEY_APP_THEME, null), equalTo(UpgradeThemes.THEME_NIGHT)) + assertThat(prefs.getString(UpgradeThemes.KEY_NIGHT_THEME, null), equalTo(UpgradeThemes.THEME_DARK)) + } } diff --git a/AnkiDroid/src/test/java/com/ichi2/themes/ThemeTest.kt b/AnkiDroid/src/test/java/com/ichi2/themes/ThemeTest.kt deleted file mode 100644 index fc5451c31cf8..000000000000 --- a/AnkiDroid/src/test/java/com/ichi2/themes/ThemeTest.kt +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (c) 2022 Brayan Oliveira - * - * This program is free software; you can redistribute it and/or modify it under - * the terms of the GNU General Public License as published by the Free Software - * Foundation; either version 3 of the License, or (at your option) any later - * version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT ANY - * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A - * PARTICULAR PURPOSE. See the GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along with - * this program. If not, see . - */ -package com.ichi2.themes - -import org.hamcrest.MatcherAssert.assertThat -import org.hamcrest.Matchers.equalTo -import org.junit.jupiter.api.Test -import org.junit.jupiter.params.ParameterizedTest -import org.junit.jupiter.params.provider.EnumSource - -class ThemeTest { - @ParameterizedTest - @EnumSource(value = Theme::class) - fun test_ofId_returns_theme(theme: Theme) { - assertThat(Theme.ofId(theme.id), equalTo(theme)) - } - - @Test - fun test_ofId_returns_fallback_if_id_is_invalid() { - assertThat(Theme.ofId("999"), equalTo(Theme.fallback)) - } -} diff --git a/AnkiDroid/src/test/java/com/ichi2/ui/KeyPickerTest.kt b/AnkiDroid/src/test/java/com/ichi2/ui/KeyPickerTest.kt index 586cc33d7c87..1a7c6ac1574b 100644 --- a/AnkiDroid/src/test/java/com/ichi2/ui/KeyPickerTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/ui/KeyPickerTest.kt @@ -19,8 +19,8 @@ package com.ichi2.ui import androidx.test.ext.junit.runners.AndroidJUnit4 import com.ichi2.anki.RobolectricTest import com.ichi2.anki.dialogs.KeySelectionDialogUtils +import com.ichi2.anki.settings.enums.DayTheme import com.ichi2.testutils.KeyEventUtils -import com.ichi2.themes.Theme import org.hamcrest.CoreMatchers.not import org.hamcrest.CoreMatchers.notNullValue import org.hamcrest.CoreMatchers.nullValue @@ -33,7 +33,7 @@ import org.junit.runner.RunWith class KeyPickerTest : RobolectricTest() { private var keyPicker: KeyPicker = run { - targetContext.setTheme(Theme.LIGHT.resId) + targetContext.setTheme(DayTheme.LIGHT.styleResId) KeyPicker.inflate(targetContext) }