diff --git a/lib/core/services/card_color_service.dart b/lib/core/services/card_color_service.dart index 34aeaf4..c773758 100644 --- a/lib/core/services/card_color_service.dart +++ b/lib/core/services/card_color_service.dart @@ -6,7 +6,9 @@ enum GradientType { linear, linearReverse, radial, sweep, solid } class CardColorService { static const _key1 = 'card_color_primary'; static const _key2 = 'card_color_secondary'; - static const _keyGradient = 'card_gradient_type'; + static const _keyGradientLegacy = 'card_gradient_type'; + static const _keyGradientLight = 'gradient_type_light'; + static const _keyGradientDark = 'gradient_type_dark'; static const defaultPrimary = Color(0xFFBEF264); static const defaultSecondary = Color(0xFF4D7C0F); @@ -14,48 +16,86 @@ class CardColorService { static const defaultPrimaryLight = Color(0xFF6A6482); static const defaultSecondaryLight = Color(0xFF000000); - static const defaultGradient = GradientType.radial; + static const defaultGradientLight = GradientType.sweep; + static const defaultGradientDark = GradientType.radial; - static Future<(Color, Color, GradientType)> load({int? accountId}) async { + static Future<(Color, Color, GradientType, GradientType)> load({ + int? accountId, + }) async { final prefs = await SharedPreferences.getInstance(); final key1 = accountId != null ? '${_key1}_$accountId' : _key1; final key2 = accountId != null ? '${_key2}_$accountId' : _key2; - final keyG = accountId != null ? '${_keyGradient}_$accountId' : _keyGradient; + final keyGLight = accountId != null + ? '${_keyGradientLight}_$accountId' + : _keyGradientLight; + final keyGDark = accountId != null + ? '${_keyGradientDark}_$accountId' + : _keyGradientDark; + final keyGLegacy = accountId != null + ? '${_keyGradientLegacy}_$accountId' + : _keyGradientLegacy; final c1 = prefs.getInt(key1); final c2 = prefs.getInt(key2); - final g = prefs.getInt(keyG); + + final legacyIndex = prefs.getInt(keyGLegacy); + GradientType parseWithDefault(int? index, GradientType fallback) { + if (index == null) return fallback; + final parsed = GradientType.values.elementAtOrNull(index); + return parsed ?? fallback; + } + + final lightIndex = prefs.getInt(keyGLight); + final darkIndex = prefs.getInt(keyGDark); return ( c1 != null ? Color(c1) : defaultPrimary, c2 != null ? Color(c2) : defaultSecondary, - g != null ? GradientType.values[g] : defaultGradient, + parseWithDefault(lightIndex ?? legacyIndex, defaultGradientLight), + parseWithDefault(darkIndex ?? legacyIndex, defaultGradientDark), ); } static Future save( Color primary, Color secondary, - GradientType gradient, { + GradientType lightGradient, + GradientType darkGradient, { int? accountId, }) async { final prefs = await SharedPreferences.getInstance(); final key1 = accountId != null ? '${_key1}_$accountId' : _key1; final key2 = accountId != null ? '${_key2}_$accountId' : _key2; - final keyG = accountId != null ? '${_keyGradient}_$accountId' : _keyGradient; + final keyGLight = accountId != null + ? '${_keyGradientLight}_$accountId' + : _keyGradientLight; + final keyGDark = accountId != null + ? '${_keyGradientDark}_$accountId' + : _keyGradientDark; await prefs.setInt(key1, primary.value); await prefs.setInt(key2, secondary.value); - await prefs.setInt(keyG, gradient.index); + await prefs.setInt(keyGLight, lightGradient.index); + await prefs.setInt(keyGDark, darkGradient.index); } static Future reset(bool isDark, {int? accountId}) async { final prefs = await SharedPreferences.getInstance(); final key1 = accountId != null ? '${_key1}_$accountId' : _key1; final key2 = accountId != null ? '${_key2}_$accountId' : _key2; - final keyG = accountId != null ? '${_keyGradient}_$accountId' : _keyGradient; + final keyGLight = accountId != null + ? '${_keyGradientLight}_$accountId' + : _keyGradientLight; + final keyGDark = accountId != null + ? '${_keyGradientDark}_$accountId' + : _keyGradientDark; + final keyGLegacy = accountId != null + ? '${_keyGradientLegacy}_$accountId' + : _keyGradientLegacy; await prefs.remove(key1); await prefs.remove(key2); - await prefs.remove(keyG); + await prefs.remove(keyGLight); + await prefs.remove(keyGDark); + await prefs.remove(keyGLegacy); } } diff --git a/lib/features/dashboard/provider.dart b/lib/features/dashboard/provider.dart index ba79278..5d4234d 100644 --- a/lib/features/dashboard/provider.dart +++ b/lib/features/dashboard/provider.dart @@ -11,6 +11,15 @@ import '../../shared/models/account.dart'; import '../../shared/services/storage_service.dart'; import '../settings/provider.dart'; +// BUG FOUND: lib/features/dashboard/provider.dart +// Description: CardColorsNotifier calls an async `_load()` in the constructor without awaiting it. +// If the user triggers `save()` before `_load()` completes, the late `_load()` can +// overwrite the newly saved colors/gradient types. +// Reproduction: Open the app (cold start), open the card color editor immediately, press Apply +// before the initial load finishes. +// Suggested fix: Track a generation/token for in-flight loads and ignore stale load results +// after any state mutation (save/reset/theme-change). + final sharedPreferencesProvider = Provider((ref) { throw UnimplementedError('Override in main'); }); @@ -334,9 +343,18 @@ final activeAccountProvider = Provider((ref) { class CardColors { final Color primary; final Color secondary; - final GradientType gradientType; + final GradientType lightGradientType; + final GradientType darkGradientType; - const CardColors(this.primary, this.secondary, this.gradientType); + const CardColors( + this.primary, + this.secondary, + this.lightGradientType, + this.darkGradientType, + ); + + GradientType gradientTypeForBrightness(Brightness brightness) => + brightness == Brightness.dark ? darkGradientType : lightGradientType; } final cardColorsProvider = @@ -365,12 +383,15 @@ class CardColorsNotifier extends StateNotifier { const CardColors( CardColorService.defaultPrimary, CardColorService.defaultSecondary, - CardColorService.defaultGradient, + CardColorService.defaultGradientLight, + CardColorService.defaultGradientDark, ), ) { _load(); } + int _loadGeneration = 0; + void setupThemeListener(Ref ref) { ref.listen(themeProvider, (previous, next) { if (previous != null) { @@ -380,20 +401,27 @@ class CardColorsNotifier extends StateNotifier { } Future _load() async { - final (c1, c2, g) = await CardColorService.load(accountId: accountId); - state = CardColors(c1, c2, g); + final currentGeneration = ++_loadGeneration; + final (c1, c2, lightG, darkG) = + await CardColorService.load(accountId: accountId); + if (currentGeneration != _loadGeneration) return; // stale + state = CardColors(c1, c2, lightG, darkG); } Future save( Color primary, Color secondary, - GradientType gradient, + GradientType lightGradient, + GradientType darkGradient, ) async { - state = CardColors(primary, secondary, gradient); + // Invalidate any in-flight load so it can't overwrite this save. + _loadGeneration++; + state = CardColors(primary, secondary, lightGradient, darkGradient); await CardColorService.save( primary, secondary, - gradient, + lightGradient, + darkGradient, accountId: accountId, ); } @@ -405,11 +433,18 @@ class CardColorsNotifier extends StateNotifier { final secondary = isDark ? CardColorService.defaultSecondary : CardColorService.defaultSecondaryLight; - state = CardColors(primary, secondary, CardColorService.defaultGradient); + _loadGeneration++; + state = CardColors( + primary, + secondary, + CardColorService.defaultGradientLight, + CardColorService.defaultGradientDark, + ); await CardColorService.save( primary, secondary, - CardColorService.defaultGradient, + CardColorService.defaultGradientLight, + CardColorService.defaultGradientDark, accountId: accountId, ); } @@ -428,14 +463,17 @@ class CardColorsNotifier extends StateNotifier { final isUsingOldDefaults = state.primary == oldDefaults.primary && state.secondary == oldDefaults.secondary && - state.gradientType == oldDefaults.gradient; + state.gradientTypeForBrightness(previousBrightness) == + oldDefaults.gradient; // Only auto-switch if using default colors if (isUsingOldDefaults) { + _loadGeneration++; state = CardColors( newDefaults.primary, newDefaults.secondary, - newDefaults.gradient, + state.lightGradientType, + state.darkGradientType, ); } } @@ -454,12 +492,12 @@ class CardColorsNotifier extends StateNotifier { ? ( primary: CardColorService.defaultPrimary, secondary: CardColorService.defaultSecondary, - gradient: CardColorService.defaultGradient, + gradient: CardColorService.defaultGradientDark, ) : ( primary: CardColorService.defaultPrimaryLight, secondary: CardColorService.defaultSecondaryLight, - gradient: CardColorService.defaultGradient, + gradient: CardColorService.defaultGradientLight, ); } } diff --git a/lib/features/dashboard/screen.dart b/lib/features/dashboard/screen.dart index 70d45be..f9e1b1a 100644 --- a/lib/features/dashboard/screen.dart +++ b/lib/features/dashboard/screen.dart @@ -44,8 +44,12 @@ class _DashboardScreenState extends ConsumerState { HSVColor savedSecondaryHSV = HSVColor.fromColor( CardColorService.defaultSecondary, ); - GradientType tempGradientType = CardColorService.defaultGradient; - GradientType savedGradientType = CardColorService.defaultGradient; + // Per-theme gradient types (light/dark), persisted separately. + GradientType tempLightGradientType = CardColorService.defaultGradientLight; + GradientType tempDarkGradientType = CardColorService.defaultGradientDark; + GradientType savedLightGradientType = CardColorService.defaultGradientLight; + GradientType savedDarkGradientType = CardColorService.defaultGradientDark; + OverlayEntry? overlayEntry; // Account editing state @@ -60,13 +64,14 @@ class _DashboardScreenState extends ConsumerState { savedSecondary = colors.secondary; savedPrimaryHSV = HSVColor.fromColor(colors.primary); savedSecondaryHSV = HSVColor.fromColor(colors.secondary); - savedGradientType = colors.gradientType; tempPrimary = colors.primary; tempSecondary = colors.secondary; tempPrimaryHSV = HSVColor.fromColor(colors.primary); tempSecondaryHSV = HSVColor.fromColor(colors.secondary); - tempGradientType = colors.gradientType; - + savedLightGradientType = colors.lightGradientType; + savedDarkGradientType = colors.darkGradientType; + tempLightGradientType = colors.lightGradientType; + tempDarkGradientType = colors.darkGradientType; setState(() { editingCard = true; editingPrimary = true; @@ -87,12 +92,18 @@ class _DashboardScreenState extends ConsumerState { HapticService.medium(); ref .read(cardColorsProvider.notifier) - .save(tempPrimary, tempSecondary, tempGradientType); + .save( + tempPrimary, + tempSecondary, + tempLightGradientType, + tempDarkGradientType, + ); } else { setState(() { tempPrimary = savedPrimary; tempSecondary = savedSecondary; - tempGradientType = savedGradientType; + tempLightGradientType = savedLightGradientType; + tempDarkGradientType = savedDarkGradientType; }); } overlayEntry?.remove(); @@ -106,13 +117,14 @@ class _DashboardScreenState extends ConsumerState { savedSecondary = colors.secondary; savedPrimaryHSV = HSVColor.fromColor(colors.primary); savedSecondaryHSV = HSVColor.fromColor(colors.secondary); - savedGradientType = colors.gradientType; tempPrimary = colors.primary; tempSecondary = colors.secondary; tempPrimaryHSV = HSVColor.fromColor(colors.primary); tempSecondaryHSV = HSVColor.fromColor(colors.secondary); - tempGradientType = colors.gradientType; - + savedLightGradientType = colors.lightGradientType; + savedDarkGradientType = colors.darkGradientType; + tempLightGradientType = colors.lightGradientType; + tempDarkGradientType = colors.darkGradientType; setState(() { editingAccount = account; tempAccountName = account.name; @@ -137,13 +149,14 @@ class _DashboardScreenState extends ConsumerState { savedSecondary = colors.secondary; savedPrimaryHSV = HSVColor.fromColor(colors.primary); savedSecondaryHSV = HSVColor.fromColor(colors.secondary); - savedGradientType = colors.gradientType; tempPrimary = colors.primary; tempSecondary = colors.secondary; tempPrimaryHSV = HSVColor.fromColor(colors.primary); tempSecondaryHSV = HSVColor.fromColor(colors.secondary); - tempGradientType = colors.gradientType; - + savedLightGradientType = colors.lightGradientType; + savedDarkGradientType = colors.darkGradientType; + tempLightGradientType = colors.lightGradientType; + tempDarkGradientType = colors.darkGradientType; setState(() { isAddingAccount = true; editingAccount = null; @@ -174,7 +187,8 @@ class _DashboardScreenState extends ConsumerState { await CardColorService.save( tempPrimary, tempSecondary, - tempGradientType, + tempLightGradientType, + tempDarkGradientType, accountId: newId, ); } else if (editingAccount != null) { @@ -182,7 +196,12 @@ class _DashboardScreenState extends ConsumerState { // Save colors await ref .read(accountCardColorsProvider(editingAccount!.id).notifier) - .save(tempPrimary, tempSecondary, tempGradientType); + .save( + tempPrimary, + tempSecondary, + tempLightGradientType, + tempDarkGradientType, + ); // Update account name and currency final updatedAccount = Account( @@ -201,7 +220,8 @@ class _DashboardScreenState extends ConsumerState { setState(() { tempPrimary = savedPrimary; tempSecondary = savedSecondary; - tempGradientType = savedGradientType; + tempLightGradientType = savedLightGradientType; + tempDarkGradientType = savedDarkGradientType; if (editingAccount != null) { tempAccountName = editingAccount!.name; tempAccountCurrency = editingAccount!.currency; @@ -349,7 +369,9 @@ class _DashboardScreenState extends ConsumerState { previewPrimary: editingCard ? tempPrimary : null, previewSecondary: editingCard ? tempSecondary : null, previewGradientType: editingCard - ? tempGradientType + ? (Theme.of(context).brightness == Brightness.dark + ? tempDarkGradientType + : tempLightGradientType) : null, ), const SizedBox(height: 16), diff --git a/lib/features/dashboard/widgets/account_editor_overlay/account_editor_overlay.dart b/lib/features/dashboard/widgets/account_editor_overlay/account_editor_overlay.dart index ee98e53..b9cc66a 100644 --- a/lib/features/dashboard/widgets/account_editor_overlay/account_editor_overlay.dart +++ b/lib/features/dashboard/widgets/account_editor_overlay/account_editor_overlay.dart @@ -206,7 +206,10 @@ class _AccountEditorOverlayState extends State { accountName: dash.tempAccountName, previewPrimary: dash.tempPrimary, previewSecondary: dash.tempSecondary, - previewGradientType: dash.tempGradientType, + previewGradientType: + Theme.of(widget.context).brightness == Brightness.dark + ? dash.tempDarkGradientType + : dash.tempLightGradientType, ), ), Positioned( diff --git a/lib/features/dashboard/widgets/account_editor_overlay/color_panel.dart b/lib/features/dashboard/widgets/account_editor_overlay/color_panel.dart index 7cf3585..826a89c 100644 --- a/lib/features/dashboard/widgets/account_editor_overlay/color_panel.dart +++ b/lib/features/dashboard/widgets/account_editor_overlay/color_panel.dart @@ -67,7 +67,11 @@ class AccountColorPanel extends StatelessWidget { dashboardState.overlayEntry?.markNeedsBuild(); } - final isSolid = dashboardState.tempGradientType == GradientType.solid; + final activeGradientType = + Theme.of(dashboardContext).brightness == Brightness.dark + ? dashboardState.tempDarkGradientType + : dashboardState.tempLightGradientType; + final isSolid = activeGradientType == GradientType.solid; final currentHSV = (isSolid || dashboardState.editingPrimary) ? dashboardState.tempPrimaryHSV : dashboardState.tempSecondaryHSV; @@ -96,8 +100,14 @@ class AccountColorPanel extends StatelessWidget { onTap: () { dashboardState.setState(() { if (isSolid) - dashboardState.tempGradientType = - CardColorService.defaultGradient; + if (Theme.of(dashboardContext).brightness == + Brightness.dark) { + dashboardState.tempDarkGradientType = + CardColorService.defaultGradientDark; + } else { + dashboardState.tempLightGradientType = + CardColorService.defaultGradientLight; + } dashboardState.editingPrimary = true; }); setPanelState(() {}); @@ -116,8 +126,14 @@ class AccountColorPanel extends StatelessWidget { onTap: () { dashboardState.setState(() { if (isSolid) - dashboardState.tempGradientType = - CardColorService.defaultGradient; + if (Theme.of(dashboardContext).brightness == + Brightness.dark) { + dashboardState.tempDarkGradientType = + CardColorService.defaultGradientDark; + } else { + dashboardState.tempLightGradientType = + CardColorService.defaultGradientLight; + } dashboardState.editingPrimary = false; }); setPanelState(() {}); @@ -141,8 +157,14 @@ class AccountColorPanel extends StatelessWidget { ? null : () { dashboardState.setState(() { - dashboardState.tempGradientType = - GradientType.solid; + if (Theme.of(dashboardContext).brightness == + Brightness.dark) { + dashboardState.tempDarkGradientType = + GradientType.solid; + } else { + dashboardState.tempLightGradientType = + GradientType.solid; + } dashboardState.editingPrimary = true; }); setPanelState(() {}); @@ -399,8 +421,7 @@ class AccountColorPanel extends StatelessWidget { children: GradientType.values .where((t) => t != GradientType.solid) .map((type) { - final isSelected = - dashboardState.tempGradientType == type; + final isSelected = activeGradientType == type; final label = switch (type) { GradientType.linear => s.gradientLinear, GradientType.linearReverse => s.gradientReverse, @@ -424,8 +445,17 @@ class AccountColorPanel extends StatelessWidget { child: GestureDetector( onTap: () { dashboardState.setState( - () => dashboardState.tempGradientType = - type, + () { + if (Theme.of(dashboardContext) + .brightness == + Brightness.dark) { + dashboardState.tempDarkGradientType = + type; + } else { + dashboardState.tempLightGradientType = + type; + } + }, ); setPanelState(() {}); dashboardState.overlayEntry @@ -516,8 +546,10 @@ class AccountColorPanel extends StatelessWidget { ); dashboardState.tempSecondaryHSV = HSVColor.fromColor(defS); - dashboardState.tempGradientType = - CardColorService.defaultGradient; + dashboardState.tempLightGradientType = + CardColorService.defaultGradientLight; + dashboardState.tempDarkGradientType = + CardColorService.defaultGradientDark; }); setPanelState(() {}); dashboardState.overlayEntry?.markNeedsBuild(); diff --git a/lib/features/dashboard/widgets/balance_card.dart b/lib/features/dashboard/widgets/balance_card.dart index ad3aa3f..8280165 100644 --- a/lib/features/dashboard/widgets/balance_card.dart +++ b/lib/features/dashboard/widgets/balance_card.dart @@ -130,12 +130,15 @@ class BalanceCardState extends ConsumerState final rates = ref.read(exchangeRateServiceProvider); final fmt = ref.watch(amountFormatProvider); final showConversions = ref.watch(showCurrencyConversionsProvider); + final brightness = Theme.of(context).brightness; final globalColors = ref.watch(cardColorsProvider); final savedColors = widget.accountColors ?? globalColors; final primary = widget.previewPrimary ?? savedColors.primary; final secondary = widget.previewSecondary ?? savedColors.secondary; - final gradientType = widget.previewGradientType ?? savedColors.gradientType; + final savedGradientType = + savedColors.gradientTypeForBrightness(brightness); + final gradientType = widget.previewGradientType ?? savedGradientType; final others = kDisplayCurrencies .where((c) => c.$1 != widget.currencyInfo.code) diff --git a/lib/features/dashboard/widgets/color_editor_overlay.dart b/lib/features/dashboard/widgets/color_editor_overlay.dart index 44cee31..12fabb3 100644 --- a/lib/features/dashboard/widgets/color_editor_overlay.dart +++ b/lib/features/dashboard/widgets/color_editor_overlay.dart @@ -67,7 +67,10 @@ class _FullScreenBlurOverlayState extends State { onLongPress: null, previewPrimary: dash.tempPrimary, previewSecondary: dash.tempSecondary, - previewGradientType: dash.tempGradientType, + previewGradientType: Theme.of(widget.context).brightness == + Brightness.dark + ? dash.tempDarkGradientType + : dash.tempLightGradientType, ), ), ), @@ -154,7 +157,11 @@ class _FullScreenBlurOverlayState extends State { dash.overlayEntry?.markNeedsBuild(); } - final isSolid = dash.tempGradientType == GradientType.solid; + final activeGradientType = + Theme.of(widget.context).brightness == Brightness.dark + ? dash.tempDarkGradientType + : dash.tempLightGradientType; + final isSolid = activeGradientType == GradientType.solid; final currentHSV = (isSolid || dash.editingPrimary) ? dash.tempPrimaryHSV : dash.tempSecondaryHSV; @@ -181,7 +188,16 @@ class _FullScreenBlurOverlayState extends State { isDimmed: isSolid, onTap: () { dash.setState(() { - if (isSolid) dash.tempGradientType = CardColorService.defaultGradient; + if (isSolid) { + if (Theme.of(widget.context).brightness == + Brightness.dark) { + dash.tempDarkGradientType = + CardColorService.defaultGradientDark; + } else { + dash.tempLightGradientType = + CardColorService.defaultGradientLight; + } + } dash.editingPrimary = true; }); setPanelState(() {}); @@ -198,7 +214,16 @@ class _FullScreenBlurOverlayState extends State { isDimmed: isSolid, onTap: () { dash.setState(() { - if (isSolid) dash.tempGradientType = CardColorService.defaultGradient; + if (isSolid) { + if (Theme.of(widget.context).brightness == + Brightness.dark) { + dash.tempDarkGradientType = + CardColorService.defaultGradientDark; + } else { + dash.tempLightGradientType = + CardColorService.defaultGradientLight; + } + } dash.editingPrimary = false; }); setPanelState(() {}); @@ -220,7 +245,12 @@ class _FullScreenBlurOverlayState extends State { GestureDetector( onTap: isSolid ? null : () { dash.setState(() { - dash.tempGradientType = GradientType.solid; + if (Theme.of(widget.context).brightness == + Brightness.dark) { + dash.tempDarkGradientType = GradientType.solid; + } else { + dash.tempLightGradientType = GradientType.solid; + } dash.editingPrimary = true; }); setPanelState(() {}); @@ -454,7 +484,7 @@ class _FullScreenBlurOverlayState extends State { children: GradientType.values .where((t) => t != GradientType.solid) .map((type) { - final isSelected = dash.tempGradientType == type; + final isSelected = activeGradientType == type; final label = switch (type) { GradientType.linear => s.gradientLinear, GradientType.linearReverse => s.gradientReverse, @@ -475,8 +505,14 @@ class _FullScreenBlurOverlayState extends State { padding: const EdgeInsets.only(right: 6), child: GestureDetector( onTap: () { - dash.setState( - () => dash.tempGradientType = type); + dash.setState(() { + if (Theme.of(widget.context).brightness == + Brightness.dark) { + dash.tempDarkGradientType = type; + } else { + dash.tempLightGradientType = type; + } + }); setPanelState(() {}); dash.overlayEntry?.markNeedsBuild(); }, @@ -557,7 +593,10 @@ class _FullScreenBlurOverlayState extends State { dash.tempSecondary = defS; dash.tempPrimaryHSV = HSVColor.fromColor(defP); dash.tempSecondaryHSV = HSVColor.fromColor(defS); - dash.tempGradientType = CardColorService.defaultGradient; + dash.tempLightGradientType = + CardColorService.defaultGradientLight; + dash.tempDarkGradientType = + CardColorService.defaultGradientDark; }); setPanelState(() {}); dash.overlayEntry?.markNeedsBuild();