From bca0c8f9d37af7651b7896025df6315088608063 Mon Sep 17 00:00:00 2001 From: kolo Date: Mon, 23 Mar 2026 23:42:52 +0300 Subject: [PATCH] update --- lib/core/services/card_color_service.dart | 39 +- lib/data/database/app_database.dart | 9 + lib/data/repositories/account_repository.dart | 11 + lib/features/dashboard/provider.dart | 17 +- lib/features/dashboard/screen.dart | 79 ++ .../widgets/account_editor_overlay.dart | 826 ++++++++++++++++++ .../dashboard/widgets/balance_card.dart | 8 +- .../widgets/balance_card_carousel.dart | 15 +- .../widgets/color_editor_overlay.dart | 4 + 9 files changed, 988 insertions(+), 20 deletions(-) create mode 100644 lib/features/dashboard/widgets/account_editor_overlay.dart diff --git a/lib/core/services/card_color_service.dart b/lib/core/services/card_color_service.dart index a695d53..88e89d8 100644 --- a/lib/core/services/card_color_service.dart +++ b/lib/core/services/card_color_service.dart @@ -16,11 +16,15 @@ class CardColorService { static const defaultGradient = GradientType.sweep; - static Future<(Color, Color, GradientType)> load() async { + static Future<(Color, Color, GradientType)> load({int? accountId}) async { final prefs = await SharedPreferences.getInstance(); - final c1 = prefs.getInt(_key1); - final c2 = prefs.getInt(_key2); - final g = prefs.getInt(_keyGradient); + final key1 = accountId != null ? '${_key1}_$accountId' : _key1; + final key2 = accountId != null ? '${_key2}_$accountId' : _key2; + final keyG = accountId != null ? '${_keyGradient}_$accountId' : _keyGradient; + + final c1 = prefs.getInt(key1); + final c2 = prefs.getInt(key2); + final g = prefs.getInt(keyG); return ( c1 != null ? Color(c1) : defaultPrimary, c2 != null ? Color(c2) : defaultSecondary, @@ -31,18 +35,27 @@ class CardColorService { static Future save( Color primary, Color secondary, - GradientType gradient, - ) async { + GradientType gradient, { + int? accountId, + }) async { final prefs = await SharedPreferences.getInstance(); - await prefs.setInt(_key1, primary.value); - await prefs.setInt(_key2, secondary.value); - await prefs.setInt(_keyGradient, gradient.index); + final key1 = accountId != null ? '${_key1}_$accountId' : _key1; + final key2 = accountId != null ? '${_key2}_$accountId' : _key2; + final keyG = accountId != null ? '${_keyGradient}_$accountId' : _keyGradient; + + await prefs.setInt(key1, primary.value); + await prefs.setInt(key2, secondary.value); + await prefs.setInt(keyG, gradient.index); } - static Future reset(bool isDark) async { + static Future reset(bool isDark, {int? accountId}) async { final prefs = await SharedPreferences.getInstance(); - await prefs.remove(_key1); - await prefs.remove(_key2); - await prefs.remove(_keyGradient); + final key1 = accountId != null ? '${_key1}_$accountId' : _key1; + final key2 = accountId != null ? '${_key2}_$accountId' : _key2; + final keyG = accountId != null ? '${_keyGradient}_$accountId' : _keyGradient; + + await prefs.remove(key1); + await prefs.remove(key2); + await prefs.remove(keyG); } } diff --git a/lib/data/database/app_database.dart b/lib/data/database/app_database.dart index 3b7caf1..d5b9299 100644 --- a/lib/data/database/app_database.dart +++ b/lib/data/database/app_database.dart @@ -297,6 +297,15 @@ class AppDatabase extends _$AppDatabase { return totals; } + + // ============================================================================ + // ACCOUNTS + // ============================================================================ + + /// Update an account + Future updateAccount(AccountsCompanion account) async { + await update(accounts).replace(account); + } } LazyDatabase _openConnection() { diff --git a/lib/data/repositories/account_repository.dart b/lib/data/repositories/account_repository.dart index 5c456e9..cab3bed 100644 --- a/lib/data/repositories/account_repository.dart +++ b/lib/data/repositories/account_repository.dart @@ -160,4 +160,15 @@ class AccountRepository { createdAt: DateTime.now(), ); } + + Future update(model.Account account) async { + await _db.updateAccount(AccountsCompanion( + id: Value(account.id), + name: Value(account.name), + isMain: Value(account.isMain), + sortOrder: Value(account.sortOrder), + currency: Value(account.currency), + createdAt: Value(account.createdAt), + )); + } } diff --git a/lib/features/dashboard/provider.dart b/lib/features/dashboard/provider.dart index da7e1df..e82bac1 100644 --- a/lib/features/dashboard/provider.dart +++ b/lib/features/dashboard/provider.dart @@ -280,8 +280,18 @@ final cardColorsProvider = return notifier; }); +// Account-specific color provider +final accountCardColorsProvider = + StateNotifierProvider.family((ref, accountId) { + final notifier = CardColorsNotifier(accountId: accountId); + notifier.setupThemeListener(ref); + return notifier; + }); + class CardColorsNotifier extends StateNotifier { - CardColorsNotifier() + final int? accountId; + + CardColorsNotifier({this.accountId}) : super( const CardColors( CardColorService.defaultPrimary, @@ -301,7 +311,7 @@ class CardColorsNotifier extends StateNotifier { } Future _load() async { - final (c1, c2, g) = await CardColorService.load(); + final (c1, c2, g) = await CardColorService.load(accountId: accountId); state = CardColors(c1, c2, g); } @@ -311,7 +321,7 @@ class CardColorsNotifier extends StateNotifier { GradientType gradient, ) async { state = CardColors(primary, secondary, gradient); - await CardColorService.save(primary, secondary, gradient); + await CardColorService.save(primary, secondary, gradient, accountId: accountId); } Future reset(bool isDark) async { @@ -326,6 +336,7 @@ class CardColorsNotifier extends StateNotifier { primary, secondary, CardColorService.defaultGradient, + accountId: accountId, ); } diff --git a/lib/features/dashboard/screen.dart b/lib/features/dashboard/screen.dart index 58a398c..b93c8f9 100644 --- a/lib/features/dashboard/screen.dart +++ b/lib/features/dashboard/screen.dart @@ -5,8 +5,10 @@ import 'package:intl/intl.dart'; import '../../core/l10n/locale_provider.dart'; import '../../core/services/card_color_service.dart'; import '../../core/services/haptic_service.dart'; +import '../../shared/models/account.dart'; import '../settings/provider.dart'; import 'provider.dart'; +import 'widgets/account_editor_overlay.dart'; import 'widgets/balance_card_carousel.dart'; import 'widgets/budget_progress.dart'; import 'widgets/color_editor_overlay.dart'; @@ -40,6 +42,11 @@ class _DashboardScreenState extends ConsumerState { GradientType savedGradientType = CardColorService.defaultGradient; OverlayEntry? overlayEntry; + // Account editing state + Account? editingAccount; + String tempAccountName = ''; + String tempAccountCurrency = 'USD'; + void _onCardLongPress() { final colors = ref.read(cardColorsProvider); savedPrimary = colors.primary; @@ -86,6 +93,77 @@ class _DashboardScreenState extends ConsumerState { setState(() => editingCard = false); } + void _onAccountCardLongPress(Account account) { + final colors = ref.read(accountCardColorsProvider(account.id)); + savedPrimary = colors.primary; + 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; + + setState(() { + editingAccount = account; + tempAccountName = account.name; + tempAccountCurrency = account.currency; + editingCard = true; + editingPrimary = true; + }); + _showAccountOverlay(); + } + + void _showAccountOverlay() { + overlayEntry = OverlayEntry( + builder: (overlayContext) => AccountEditorOverlay( + dashboardState: this, + context: context, + ), + ); + Overlay.of(context, rootOverlay: true).insert(overlayEntry!); + } + + void closeAccountOverlay({required bool apply}) async { + if (apply && editingAccount != null) { + HapticService.medium(); + + // Save colors + await ref.read(accountCardColorsProvider(editingAccount!.id).notifier).save( + tempPrimary, + tempSecondary, + tempGradientType, + ); + + // Update account name and currency + final updatedAccount = Account( + id: editingAccount!.id, + name: tempAccountName, + isMain: editingAccount!.isMain, + sortOrder: editingAccount!.sortOrder, + currency: tempAccountCurrency, + createdAt: editingAccount!.createdAt, + ); + + await ref.read(accountRepositoryProvider).update(updatedAccount); + } else { + setState(() { + tempPrimary = savedPrimary; + tempSecondary = savedSecondary; + tempGradientType = savedGradientType; + }); + } + + overlayEntry?.remove(); + overlayEntry = null; + setState(() { + editingCard = false; + editingAccount = null; + }); + } + @override void initState() { super.initState(); @@ -187,6 +265,7 @@ class _DashboardScreenState extends ConsumerState { balance: balance, currencyInfo: currencyInfo, onLongPress: _onCardLongPress, + onAccountLongPress: _onAccountCardLongPress, previewPrimary: editingCard ? tempPrimary : null, previewSecondary: editingCard ? tempSecondary : null, previewGradientType: editingCard ? tempGradientType : null, diff --git a/lib/features/dashboard/widgets/account_editor_overlay.dart b/lib/features/dashboard/widgets/account_editor_overlay.dart new file mode 100644 index 0000000..fbf18b4 --- /dev/null +++ b/lib/features/dashboard/widgets/account_editor_overlay.dart @@ -0,0 +1,826 @@ +import 'dart:ui'; +import 'package:flutter/material.dart'; +import 'package:flutter_colorpicker/flutter_colorpicker.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../../core/l10n/app_strings.dart'; +import '../../../core/l10n/locale_provider.dart'; +import '../../../core/services/card_color_service.dart'; +import '../../settings/provider.dart'; +import '../provider.dart'; +import 'balance_card.dart'; + +String _colorToHex(Color color) { + final hex = color.value.toRadixString(16).padLeft(8, '0').toUpperCase(); + return hex.substring(2); +} + +class AccountEditorOverlay extends StatefulWidget { + final dynamic dashboardState; + final BuildContext context; + + const AccountEditorOverlay({ + super.key, + required this.dashboardState, + required this.context, + }); + + @override + State createState() => _AccountEditorOverlayState(); +} + +class _AccountEditorOverlayState extends State { + dynamic get dash => widget.dashboardState; + late TextEditingController _nameController; + late String _selectedCurrency; + + @override + void initState() { + super.initState(); + _nameController = TextEditingController(text: dash.tempAccountName); + _selectedCurrency = dash.tempAccountCurrency; + _nameController.addListener(() { + dash.tempAccountName = _nameController.text; + }); + } + + @override + void dispose() { + _nameController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final mq = MediaQuery.of(widget.context); + final cardTop = mq.padding.top + kToolbarHeight + 16; + const cardHeight = 220.0; + const editorPanelHeight = 140.0; // Increased to accommodate currency picker + final editorPanelTop = cardTop + cardHeight + 20; + final colorPanelTop = editorPanelTop + editorPanelHeight + 12; + const colorPanelHeight = 410.0; + + return Material( + color: Colors.transparent, + child: Stack( + children: [ + Positioned.fill( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20), + child: Container(color: Colors.black.withOpacity(0.6)), + ), + ), + Positioned.fill( + child: GestureDetector( + onTap: () => dash.closeAccountOverlay(apply: false), + behavior: HitTestBehavior.translucent, + child: const SizedBox.expand(), + ), + ), + // Preview Card + Positioned( + top: cardTop, + left: 20, + right: 20, + child: Consumer( + builder: (ctx, ref, _) => BalanceCard( + balance: ref.read(totalBalanceProvider), + currencyInfo: CurrencyInfo( + currencyMap[dash.tempAccountCurrency]?.symbol ?? '\$', + dash.tempAccountCurrency, + ), + onLongPress: null, + accountName: dash.tempAccountName, + previewPrimary: dash.tempPrimary, + previewSecondary: dash.tempSecondary, + previewGradientType: dash.tempGradientType, + ), + ), + ), + // Account Editor Panel + Positioned( + top: editorPanelTop, + left: 20, + right: 20, + child: GestureDetector( + onTap: () {}, + behavior: HitTestBehavior.opaque, + child: _buildEditorPanel(editorPanelHeight), + ), + ), + // Color Picker Panel + Positioned( + top: colorPanelTop, + left: 20, + right: 20, + child: GestureDetector( + onTap: () {}, + behavior: HitTestBehavior.opaque, + child: _buildColorPanel(colorPanelHeight), + ), + ), + // Close Button - Top Right + Positioned( + top: mq.padding.top + 8, + right: 20, + child: SafeArea( + child: GestureDetector( + onTap: () => dash.closeAccountOverlay(apply: false), + child: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: Theme.of(widget.context).colorScheme.surface, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.3), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Icon( + Icons.close_rounded, + size: 24, + color: Theme.of(widget.context).colorScheme.onSurface, + ), + ), + ), + ), + ), + ], + ), + ); + } + + Widget _buildEditorPanel(double panelHeight) { + final currencies = [ + ('USD', '\$'), + ('EUR', '€'), + ('BYN', 'Br'), + ('RUB', '₽'), + ]; + + return Container( + height: panelHeight, + decoration: BoxDecoration( + color: Theme.of(widget.context).colorScheme.surface, + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: Theme.of(widget.context).colorScheme.onSurface.withOpacity(0.1), + width: 1.5, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.3), + blurRadius: 24, + offset: const Offset(0, 8), + ), + ], + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Account Name', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: Theme.of(widget.context).colorScheme.onSurface.withOpacity(0.6), + ), + ), + const SizedBox(height: 6), + TextField( + controller: _nameController, + maxLength: 17, + buildCounter: (context, {required currentLength, required isFocused, maxLength}) => null, + decoration: InputDecoration( + hintText: 'Enter account name', + filled: true, + fillColor: Theme.of(widget.context).colorScheme.onSurface.withOpacity(0.05), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none, + ), + contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + ), + ), + const SizedBox(height: 12), + Text( + 'Currency', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: Theme.of(widget.context).colorScheme.onSurface.withOpacity(0.6), + ), + ), + const SizedBox(height: 6), + Row( + children: currencies.map((c) { + final isSelected = c.$1 == _selectedCurrency; + return Expanded( + child: GestureDetector( + onTap: () { + setState(() { + _selectedCurrency = c.$1; + dash.tempAccountCurrency = c.$1; + }); + }, + child: Container( + margin: EdgeInsets.only( + right: c.$1 == currencies.last.$1 ? 0 : 8, + ), + padding: const EdgeInsets.symmetric(vertical: 8), + decoration: BoxDecoration( + color: isSelected + ? const Color(0xFF7C6DED).withOpacity(0.15) + : Theme.of(widget.context).colorScheme.onSurface.withOpacity(0.05), + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: isSelected + ? const Color(0xFF7C6DED) + : Colors.transparent, + width: 1.5, + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + c.$2, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: isSelected + ? const Color(0xFF7C6DED) + : Theme.of(widget.context).colorScheme.onSurface, + ), + ), + const SizedBox(height: 2), + Text( + c.$1, + style: TextStyle( + fontSize: 9, + color: Theme.of(widget.context).colorScheme.onSurface.withOpacity(0.5), + ), + ), + ], + ), + ), + ), + ); + }).toList(), + ), + ], + ), + ), + ); + } + + Widget _buildColorPanel(double panelHeight) { + return Container( + height: panelHeight, + decoration: BoxDecoration( + color: Theme.of(widget.context).colorScheme.surface, + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: Theme.of(widget.context).colorScheme.onSurface.withOpacity(0.1), + width: 1.5, + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.3), + blurRadius: 24, + offset: const Offset(0, 8), + ), + ], + ), + child: StatefulBuilder( + builder: (ctx, setPanelState) { + final s = AppStrings( + ProviderScope.containerOf(widget.context).read(localeProvider), + ); + + void onHSVChanged(HSVColor hsv) { + setPanelState(() {}); + dash.setState(() { + if (dash.editingPrimary) { + dash.tempPrimaryHSV = hsv; + dash.tempPrimary = hsv.toColor(); + } else { + dash.tempSecondaryHSV = hsv; + dash.tempSecondary = hsv.toColor(); + } + }); + dash.overlayEntry?.markNeedsBuild(); + } + + final isSolid = dash.tempGradientType == GradientType.solid; + final currentHSV = (isSolid || dash.editingPrimary) + ? dash.tempPrimaryHSV + : dash.tempSecondaryHSV; + + return Padding( + padding: const EdgeInsets.fromLTRB(16, 14, 16, 22), + child: Column( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + IntrinsicHeight( + child: Row( + children: [ + Expanded( + child: PanelTab( + label: s.colorPrimary, + isSelected: dash.editingPrimary, + color: dash.tempPrimary, + isDimmed: isSolid, + onTap: () { + dash.setState(() { + if (isSolid) dash.tempGradientType = CardColorService.defaultGradient; + dash.editingPrimary = true; + }); + setPanelState(() {}); + dash.overlayEntry?.markNeedsBuild(); + }, + ), + ), + const SizedBox(width: 6), + Expanded( + child: PanelTab( + label: s.colorSecondary, + isSelected: !dash.editingPrimary, + color: dash.tempSecondary, + isDimmed: isSolid, + onTap: () { + dash.setState(() { + if (isSolid) dash.tempGradientType = CardColorService.defaultGradient; + dash.editingPrimary = false; + }); + setPanelState(() {}); + dash.overlayEntry?.markNeedsBuild(); + }, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Container( + width: 1, + color: Theme.of(widget.context) + .colorScheme + .onSurface + .withOpacity(0.15), + margin: const EdgeInsets.symmetric(vertical: 4), + ), + ), + GestureDetector( + onTap: isSolid ? null : () { + dash.setState(() { + dash.tempGradientType = GradientType.solid; + dash.editingPrimary = true; + }); + setPanelState(() {}); + dash.overlayEntry?.markNeedsBuild(); + }, + child: Container( + height: double.infinity, + padding: const EdgeInsets.symmetric( + horizontal: 10, vertical: 6), + decoration: BoxDecoration( + color: isSolid + ? const Color(0xFF7C6DED).withOpacity(0.15) + : Colors.transparent, + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: isSolid + ? const Color(0xFF7C6DED) + : Theme.of(widget.context) + .colorScheme + .onSurface + .withOpacity(0.2), + width: 1.5, + ), + ), + child: Center( + child: Text( + s.colorSolid, + style: TextStyle( + fontSize: 11, + fontWeight: isSolid + ? FontWeight.w600 + : FontWeight.normal, + color: isSolid + ? const Color(0xFF7C6DED) + : Theme.of(widget.context) + .colorScheme + .onSurface + .withOpacity(0.5), + ), + ), + ), + ), + ), + ], + ), + ), + const SizedBox(height: 10), + Expanded( + child: LayoutBuilder( + builder: (lbCtx, constraints) { + const reservedBelow = 78.0; + final spectrumH = + (constraints.maxHeight - reservedBelow).clamp( + 40.0, double.infinity); + + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(10), + child: SizedBox( + height: spectrumH, + child: ColorPickerArea( + currentHSV, + onHSVChanged, + PaletteType.hsvWithHue, + ), + ), + ), + const SizedBox(height: 8), + SizedBox( + height: 36, + child: ColorPickerSlider( + TrackType.hue, + currentHSV, + onHSVChanged, + displayThumbColor: true, + ), + ), + const SizedBox(height: 8), + IgnorePointer( + ignoring: isSolid, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 200), + opacity: isSolid ? 0.4 : 1.0, + child: SizedBox( + height: 26, + child: Row( + children: [ + GestureDetector( + onTap: () { + dash.setState( + () => dash.editingPrimary = true); + setPanelState(() {}); + dash.overlayEntry?.markNeedsBuild(); + }, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 22, + height: 22, + decoration: BoxDecoration( + color: dash.tempPrimary, + borderRadius: + BorderRadius.circular(6), + border: dash.editingPrimary + ? Border.all( + color: Colors.white, + width: 2) + : Border.all( + color: Colors.transparent, + width: 2), + ), + ), + const SizedBox(width: 5), + Text( + '#${_colorToHex(dash.tempPrimary)}', + style: TextStyle( + fontSize: 11, + fontFamily: 'monospace', + fontWeight: dash.editingPrimary + ? FontWeight.w600 + : FontWeight.normal, + color: Theme.of(widget.context) + .colorScheme + .onSurface + .withOpacity( + dash.editingPrimary + ? 0.8 + : 0.4), + ), + ), + ], + ), + ), + const Spacer(), + if (!isSolid) + GestureDetector( + onTap: () { + dash.setState(() => + dash.editingPrimary = false); + setPanelState(() {}); + dash.overlayEntry?.markNeedsBuild(); + }, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '#${_colorToHex(dash.tempSecondary)}', + style: TextStyle( + fontSize: 11, + fontFamily: 'monospace', + fontWeight: !dash.editingPrimary + ? FontWeight.w600 + : FontWeight.normal, + color: Theme.of(widget.context) + .colorScheme + .onSurface + .withOpacity( + !dash.editingPrimary + ? 0.8 + : 0.4), + ), + ), + const SizedBox(width: 5), + Container( + width: 22, + height: 22, + decoration: BoxDecoration( + color: dash.tempSecondary, + borderRadius: + BorderRadius.circular(6), + border: !dash.editingPrimary + ? Border.all( + color: Colors.white, + width: 2) + : Border.all( + color: + Colors.transparent, + width: 2), + ), + ), + ], + ), + ), + ], + ), + ), + ), + ), + ], + ); + }, + ), + ), + const SizedBox(height: 8), + IgnorePointer( + ignoring: isSolid, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 200), + opacity: isSolid ? 0.3 : 1.0, + child: Row( + children: GradientType.values + .where((t) => t != GradientType.solid) + .map((type) { + final isSelected = dash.tempGradientType == type; + final label = switch (type) { + GradientType.linear => s.gradientLinear, + GradientType.linearReverse => s.gradientReverse, + GradientType.radial => s.gradientRadial, + GradientType.sweep => s.gradientSweep, + GradientType.solid => '', + }; + final icon = switch (type) { + GradientType.linear => Icons.trending_flat_rounded, + GradientType.linearReverse => + Icons.swap_horiz_rounded, + GradientType.radial => Icons.blur_circular_rounded, + GradientType.sweep => Icons.rotate_right_rounded, + GradientType.solid => Icons.square_rounded, + }; + return Expanded( + child: Padding( + padding: const EdgeInsets.only(right: 6), + child: GestureDetector( + onTap: () { + dash.setState( + () => dash.tempGradientType = type); + setPanelState(() {}); + dash.overlayEntry?.markNeedsBuild(); + }, + child: AnimatedContainer( + duration: const Duration(milliseconds: 150), + padding: + const EdgeInsets.symmetric(vertical: 5), + decoration: BoxDecoration( + color: isSelected + ? const Color(0xFF7C6DED) + .withOpacity(0.15) + : Theme.of(widget.context) + .colorScheme + .onSurface + .withOpacity(0.05), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: isSelected + ? const Color(0xFF7C6DED) + : Colors.transparent, + width: 1.5, + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, + size: 15, + color: isSelected + ? const Color(0xFF7C6DED) + : Theme.of(widget.context) + .colorScheme + .onSurface + .withOpacity(0.45)), + const SizedBox(height: 2), + Text( + label, + style: TextStyle( + fontSize: 9, + fontWeight: isSelected + ? FontWeight.w600 + : FontWeight.normal, + color: isSelected + ? const Color(0xFF7C6DED) + : Theme.of(widget.context) + .colorScheme + .onSurface + .withOpacity(0.45), + ), + ), + ], + ), + ), + ), + ), + ); + }).toList(), + ), + ), + ), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: OutlinedButton.icon( + onPressed: () { + final isDarkTheme = + Theme.of(widget.context).brightness == + Brightness.dark; + final defP = isDarkTheme + ? CardColorService.defaultPrimary + : CardColorService.defaultPrimaryLight; + final defS = isDarkTheme + ? CardColorService.defaultSecondary + : CardColorService.defaultSecondaryLight; + dash.setState(() { + dash.tempPrimary = defP; + dash.tempSecondary = defS; + dash.tempPrimaryHSV = HSVColor.fromColor(defP); + dash.tempSecondaryHSV = HSVColor.fromColor(defS); + dash.tempGradientType = CardColorService.defaultGradient; + }); + setPanelState(() {}); + dash.overlayEntry?.markNeedsBuild(); + }, + icon: const Icon(Icons.restart_alt_rounded, size: 15), + label: Text(s.reset, + style: const TextStyle(fontSize: 13)), + style: OutlinedButton.styleFrom( + foregroundColor: Theme.of(widget.context) + .colorScheme + .onSurface + .withOpacity(0.7), + side: BorderSide( + color: Theme.of(widget.context) + .colorScheme + .onSurface + .withOpacity(0.2), + ), + padding: const EdgeInsets.symmetric(vertical: 10), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12)), + ), + ), + ), + const SizedBox(width: 10), + Expanded( + flex: 2, + child: ElevatedButton( + onPressed: () => dash.closeAccountOverlay(apply: true), + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF7C6DED), + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 10), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12)), + ), + child: Text(s.apply, + style: const TextStyle( + fontWeight: FontWeight.w700, fontSize: 14)), + ), + ), + ], + ), + ], + ), + ); + }, + ), + ); + } +} + +class PanelTab extends StatelessWidget { + final String label; + final bool isSelected; + final Color color; + final bool isDimmed; + final VoidCallback onTap; + + const PanelTab({ + super.key, + required this.label, + required this.isSelected, + required this.color, + required this.isDimmed, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + final isDark = Theme.of(context).brightness == Brightness.dark; + final borderColor = isSelected + ? const Color(0xFF7C6DED) + : (isDark ? Colors.white24 : const Color(0xFFCCCCDD)); + final textColor = isSelected + ? const Color(0xFF7C6DED) + : (isDark ? Colors.white60 : Theme.of(context).colorScheme.onSurface.withOpacity(0.5)); + + return GestureDetector( + onTap: onTap, + child: AnimatedOpacity( + duration: const Duration(milliseconds: 200), + opacity: isDimmed ? 0.5 : 1.0, + child: AnimatedContainer( + duration: const Duration(milliseconds: 150), + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), + decoration: BoxDecoration( + color: isSelected ? const Color(0xFF7C6DED).withOpacity(0.15) : Colors.transparent, + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: borderColor, + width: 1.5, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 10, + height: 10, + decoration: BoxDecoration( + color: color, + shape: BoxShape.circle, + border: Border.all( + color: isDark ? Colors.white30 : Colors.black12, + width: 1, + ), + ), + ), + const SizedBox(width: 6), + Flexible( + child: Text( + label, + overflow: TextOverflow.ellipsis, + maxLines: 1, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontWeight: + isSelected ? FontWeight.w600 : FontWeight.normal, + color: textColor, + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/features/dashboard/widgets/balance_card.dart b/lib/features/dashboard/widgets/balance_card.dart index f40eb90..3e9aac5 100644 --- a/lib/features/dashboard/widgets/balance_card.dart +++ b/lib/features/dashboard/widgets/balance_card.dart @@ -35,6 +35,7 @@ class BalanceCard extends ConsumerStatefulWidget { final Color? previewSecondary; final GradientType? previewGradientType; final String? accountName; + final CardColors? accountColors; const BalanceCard({ super.key, @@ -45,6 +46,7 @@ class BalanceCard extends ConsumerStatefulWidget { this.previewSecondary, this.previewGradientType, this.accountName, + this.accountColors, }); @override @@ -134,10 +136,14 @@ class BalanceCardState extends ConsumerState final s = ref.watch(stringsProvider); final rates = ref.read(exchangeRateServiceProvider); final fmt = ref.watch(amountFormatProvider); - final savedColors = ref.watch(cardColorsProvider); + + // Use account-specific colors if provided, otherwise use global colors + 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 allCurrencies = [ ('USD', r'$'), ('EUR', '€'), diff --git a/lib/features/dashboard/widgets/balance_card_carousel.dart b/lib/features/dashboard/widgets/balance_card_carousel.dart index baf6b84..2a3fc09 100644 --- a/lib/features/dashboard/widgets/balance_card_carousel.dart +++ b/lib/features/dashboard/widgets/balance_card_carousel.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../core/services/card_color_service.dart'; import '../../../core/services/haptic_service.dart'; +import '../../../shared/models/account.dart'; import '../../../shared/models/transaction.dart'; import '../../settings/provider.dart'; import '../provider.dart'; @@ -11,6 +12,7 @@ class BalanceCardCarousel extends ConsumerStatefulWidget { final double balance; final CurrencyInfo currencyInfo; final VoidCallback? onLongPress; + final void Function(Account)? onAccountLongPress; final Color? previewPrimary; final Color? previewSecondary; final GradientType? previewGradientType; @@ -20,6 +22,7 @@ class BalanceCardCarousel extends ConsumerStatefulWidget { required this.balance, required this.currencyInfo, this.onLongPress, + this.onAccountLongPress, this.previewPrimary, this.previewSecondary, this.previewGradientType, @@ -87,11 +90,17 @@ class _BalanceCardCarouselState extends ConsumerState { ); } else if (index <= accounts.length) { final account = accounts[index - 1]; + final accountColors = ref.watch(accountCardColorsProvider(account.id)); + cardWidget = BalanceCard( - balance: widget.balance, // TODO: Calculate per-account balance - currencyInfo: widget.currencyInfo, - onLongPress: null, + balance: widget.balance, + currencyInfo: CurrencyInfo( + currencyMap[account.currency]?.symbol ?? '\$', + account.currency, + ), + onLongPress: () => widget.onAccountLongPress?.call(account), accountName: account.name, + accountColors: accountColors, ); } else { cardWidget = AddAccountCard( diff --git a/lib/features/dashboard/widgets/color_editor_overlay.dart b/lib/features/dashboard/widgets/color_editor_overlay.dart index 464b399..f975926 100644 --- a/lib/features/dashboard/widgets/color_editor_overlay.dart +++ b/lib/features/dashboard/widgets/color_editor_overlay.dart @@ -92,6 +92,10 @@ class _FullScreenBlurOverlayState extends State { decoration: BoxDecoration( color: Theme.of(widget.context).colorScheme.surface, borderRadius: BorderRadius.circular(20), + border: Border.all( + color: Theme.of(widget.context).colorScheme.onSurface.withOpacity(0.1), + width: 1.5, + ), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.3),