| 1 | import 'package:cake_wallet/entities/preferences_key.dart'; |
| 2 | import 'package:cake_wallet/utils/device_info.dart'; |
| 3 | import 'package:cake_wallet/utils/responsive_layout_util.dart'; |
| 4 | import 'package:flutter/material.dart'; |
| 5 | import 'package:mobx/mobx.dart'; |
| 6 | import 'package:cake_wallet/themes/utils/theme_list.dart'; |
| 7 | import 'package:shared_preferences/shared_preferences.dart'; |
| 8 | |
| 9 | import 'material_base_theme.dart'; |
| 10 | import 'package:cake_wallet/themes/theme_classes/black_theme.dart'; |
| 11 | |
| 12 | part 'theme_store.g.dart'; |
| 13 | |
| 14 | /// MobX store for managing theme state |
| 15 | class ThemeStore = ThemeStoreBase with _$ThemeStore; |
| 16 | |
| 17 | abstract class ThemeStoreBase with Store { |
| 18 | @observable |
| 19 | MaterialThemeBase _currentTheme = ThemeList.lightTheme; |
| 20 | |
| 21 | @observable |
| 22 | ThemeMode _themeMode = ThemeMode.system; |
| 23 | |
| 24 | @observable |
| 25 | bool _isOled = false; |
| 26 | |
| 27 | @computed |
| 28 | MaterialThemeBase get currentTheme => _currentTheme; |
| 29 | |
| 30 | @computed |
| 31 | ThemeMode get themeMode => _themeMode; |
| 32 | |
| 33 | @computed |
| 34 | bool get isDarkMode => _currentTheme.isDark; |
| 35 | |
| 36 | @computed |
| 37 | bool get isOled => _isOled; |
| 38 | |
| 39 | @computed |
| 40 | bool get hasCustomTheme => sharedPreferences.getInt(PreferencesKey.currentTheme) != null; |
| 41 | |
| 42 | @computed |
| 43 | MaterialThemeBase? get savedCustomTheme { |
| 44 | final raw = sharedPreferences.getInt(PreferencesKey.currentTheme); |
| 45 | return raw != null ? ThemeList.deserialize(raw: raw) : null; |
| 46 | } |
| 47 | |
| 48 | @computed |
| 49 | MaterialThemeBase? get savedDarkTheme { |
| 50 | try { |
| 51 | final raw = sharedPreferences.getInt(PreferencesKey.savedDarkTheme); |
| 52 | return raw != null ? ThemeList.deserialize(raw: raw) : null; |
| 53 | } catch (e) { |
| 54 | return null; |
| 55 | } |
| 56 | } |
| 57 | |
| 58 | @computed |
| 59 | MaterialThemeBase? get savedLightTheme { |
| 60 | try { |
| 61 | final raw = sharedPreferences.getInt(PreferencesKey.savedLightTheme); |
| 62 | return raw != null ? ThemeList.deserialize(raw: raw) : null; |
| 63 | } catch (e) { |
| 64 | return null; |
| 65 | } |
| 66 | } |
| 67 | |
| 68 | late SharedPreferences sharedPreferences; |
| 69 | |
| 70 | @action |
| 71 | Future<void> setTheme(MaterialThemeBase theme) async { |
| 72 | if (_currentTheme == theme) return; |
| 73 | |
| 74 | _currentTheme = theme; |
| 75 | await sharedPreferences.setInt(PreferencesKey.currentTheme, theme.raw); |
| 76 | _isOled = theme is BlackTheme ? theme.isOled : false; |
| 77 | await sharedPreferences.setBool(PreferencesKey.blackThemeOled, _isOled); |
| 78 | |
| 79 | // Save preferred themes for system mode |
| 80 | if (theme.isDark) { |
| 81 | await sharedPreferences.setInt(PreferencesKey.savedDarkTheme, theme.raw); |
| 82 | } else { |
| 83 | await sharedPreferences.setInt(PreferencesKey.savedLightTheme, theme.raw); |
| 84 | } |
| 85 | } |
| 86 | |
| 87 | @action |
| 88 | Future<void> setThemeMode(ThemeMode mode) async { |
| 89 | if (_themeMode == mode) return; |
| 90 | |
| 91 | _themeMode = mode; |
| 92 | await _saveThemeModeToPrefs(mode); |
| 93 | |
| 94 | if (mode == ThemeMode.system) { |
| 95 | // We'll always use getThemeFromSystem() for system mode to respect saved themes |
| 96 | await setTheme(getThemeFromSystem()); |
| 97 | return; |
| 98 | } |
| 99 | |
| 100 | if (!hasCustomTheme) return; |
| 101 | |
| 102 | final savedTheme = savedCustomTheme; |
| 103 | if (savedTheme == null) return; |
| 104 | |
| 105 | if (_isThemeCompatibleWithMode(savedTheme, mode)) { |
| 106 | await setTheme(savedTheme); |
| 107 | } |
| 108 | } |
| 109 | |
| 110 | @action |
| 111 | Future<void> setOledEnabled(bool value) async { |
| 112 | if (_isOled == value) return; |
| 113 | _isOled = value; |
| 114 | await sharedPreferences.setBool(PreferencesKey.blackThemeOled, value); |
| 115 | |
| 116 | if (_currentTheme is BlackTheme) { |
| 117 | final current = _currentTheme as BlackTheme; |
| 118 | await setTheme(BlackTheme(current.accentColor, isOled: value)); |
| 119 | } |
| 120 | } |
| 121 | |
| 122 | Future<void> _saveThemeModeToPrefs(ThemeMode mode) async { |
| 123 | await sharedPreferences.setString(PreferencesKey.themeMode, mode.toString()); |
| 124 | } |
| 125 | |
| 126 | /// Loads the saved theme preferences |
| 127 | Future<void> loadThemePreferences() async { |
| 128 | sharedPreferences = await SharedPreferences.getInstance(); |
| 129 | |
| 130 | await _loadThemeMode(); |
| 131 | await _loadAndSetTheme(); |
| 132 | |
| 133 | _setupThemeReaction(); |
| 134 | } |
| 135 | |
| 136 | /// Loads the saved theme mode from SharedPreferences |
| 137 | Future<void> _loadThemeMode() async { |
| 138 | final savedThemeMode = sharedPreferences.getString(PreferencesKey.themeMode); |
| 139 | if (savedThemeMode != null) { |
| 140 | _themeMode = ThemeMode.values.firstWhere( |
| 141 | (mode) => mode.toString() == savedThemeMode, |
| 142 | orElse: () => ThemeMode.system, |
| 143 | ); |
| 144 | } |
| 145 | } |
| 146 | |
| 147 | /// Loads and sets the appropriate theme based on platform and installation status |
| 148 | Future<void> _loadAndSetTheme() async { |
| 149 | final bool isNewInstall = sharedPreferences.getBool(PreferencesKey.isNewInstall) ?? true; |
| 150 | |
| 151 | bool shouldUseMobileTheme = |
| 152 | responsiveLayoutUtil.shouldRenderMobileUI && DeviceInfo.instance.isMobile; |
| 153 | |
| 154 | if (shouldUseMobileTheme) { |
| 155 | await _handleMobileTheme(isNewInstall); |
| 156 | } else { |
| 157 | await _handleNonMobileTheme(); |
| 158 | } |
| 159 | } |
| 160 | |
| 161 | /// Handles theme loading for non-mobile platforms |
| 162 | Future<void> _handleNonMobileTheme() async { |
| 163 | await setTheme(ThemeList.darkTheme); |
| 164 | await setThemeMode(ThemeMode.dark); |
| 165 | } |
| 166 | |
| 167 | /// Handles theme loading for mobile platforms |
| 168 | Future<void> _handleMobileTheme(bool isNewInstall) async { |
| 169 | if (isNewInstall) { |
| 170 | await _setSystemTheme(isNewInstall: isNewInstall); |
| 171 | } else { |
| 172 | await loadSavedTheme(); |
| 173 | } |
| 174 | } |
| 175 | |
| 176 | /// Loads the saved theme from SharedPreferences |
| 177 | Future<void> loadSavedTheme({bool isFromBackup = false}) async { |
| 178 | try { |
| 179 | _isOled = sharedPreferences.getBool(PreferencesKey.blackThemeOled) ?? false; |
| 180 | final theme = savedCustomTheme; |
| 181 | |
| 182 | if (!hasCustomTheme || theme == null) { |
| 183 | await _setSystemTheme(); |
| 184 | return; |
| 185 | } |
| 186 | |
| 187 | if (theme is BlackTheme) { |
| 188 | final adjusted = BlackTheme(theme.accentColor, isOled: _isOled); |
| 189 | if (_currentTheme != adjusted) { |
| 190 | await setTheme(adjusted); |
| 191 | } |
| 192 | } else if (_currentTheme != theme) { |
| 193 | await setTheme(theme); |
| 194 | } |
| 195 | |
| 196 | final newThemeMode = _getThemeModeOnStartUp(theme, isFromBackup); |
| 197 | |
| 198 | if (newThemeMode == ThemeMode.system) { |
| 199 | await _setSystemTheme(); |
| 200 | return; |
| 201 | } |
| 202 | |
| 203 | if (_themeMode != newThemeMode) { |
| 204 | await setThemeMode(newThemeMode); |
| 205 | } |
| 206 | } catch (e) { |
| 207 | await _setSystemTheme(); |
| 208 | } |
| 209 | } |
| 210 | |
| 211 | Future<void> _setSystemTheme({bool isNewInstall = false}) async { |
| 212 | if (!isNewInstall && hasCustomTheme) return; |
| 213 | |
| 214 | final systemTheme = getThemeFromSystem(); |
| 215 | |
| 216 | if (_currentTheme != systemTheme) { |
| 217 | await setTheme(systemTheme); |
| 218 | } |
| 219 | |
| 220 | if (isNewInstall) { |
| 221 | await _saveThemeModeToPrefs(ThemeMode.system); |
| 222 | if (_themeMode != ThemeMode.system) { |
| 223 | await setThemeMode(ThemeMode.system); |
| 224 | } |
| 225 | } |
| 226 | } |
| 227 | |
| 228 | ThemeMode _getThemeModeOnStartUp(MaterialThemeBase theme, bool isFromBackup) { |
| 229 | if (isFromBackup || _themeMode != ThemeMode.system) { |
| 230 | return theme.isDark ? ThemeMode.dark : ThemeMode.light; |
| 231 | } |
| 232 | |
| 233 | return ThemeMode.system; |
| 234 | } |
| 235 | |
| 236 | void _setupThemeReaction() { |
| 237 | reaction( |
| 238 | (_) => currentTheme, |
| 239 | (MaterialThemeBase theme) => sharedPreferences.setInt(PreferencesKey.currentTheme, theme.raw), |
| 240 | ); |
| 241 | } |
| 242 | |
| 243 | MaterialThemeBase getThemeFromSystem() { |
| 244 | final systemBrightness = WidgetsBinding.instance.platformDispatcher.platformBrightness; |
| 245 | |
| 246 | if (systemBrightness == Brightness.dark) { |
| 247 | // Prefer saved dark theme when available |
| 248 | try { |
| 249 | final savedDark = savedDarkTheme; |
| 250 | if (savedDark != null) { |
| 251 | if (savedDark is BlackTheme) { |
| 252 | // We'll use the OLED flag from the saved theme itself, not from separate preference |
| 253 | return BlackTheme(savedDark.accentColor, isOled: savedDark.isOled); |
| 254 | } |
| 255 | return savedDark; |
| 256 | } |
| 257 | } catch (_) {} |
| 258 | |
| 259 | // If no saved dark theme, check if current theme is BlackTheme with accent colors |
| 260 | // and preserve it when switching to dark mode |
| 261 | if (_currentTheme is BlackTheme) { |
| 262 | final currentBlackTheme = _currentTheme as BlackTheme; |
| 263 | return BlackTheme(currentBlackTheme.accentColor, isOled: currentBlackTheme.isOled); |
| 264 | } |
| 265 | |
| 266 | return ThemeList.darkTheme; |
| 267 | } |
| 268 | |
| 269 | // Light system preference: prefer saved light theme when available |
| 270 | try { |
| 271 | final savedLight = savedLightTheme; |
| 272 | if (savedLight != null) return savedLight; |
| 273 | } catch (_) {} |
| 274 | return ThemeList.lightTheme; |
| 275 | } |
| 276 | |
| 277 | /// Checks if a theme is compatible with a theme mode |
| 278 | bool _isThemeCompatibleWithMode(MaterialThemeBase theme, ThemeMode mode) { |
| 279 | switch (mode) { |
| 280 | case ThemeMode.light: |
| 281 | return !theme.isDark; |
| 282 | case ThemeMode.dark: |
| 283 | return theme.isDark; |
| 284 | case ThemeMode.system: |
| 285 | return true; // All themes are compatible with system mode |
| 286 | } |
| 287 | } |
| 288 | } |