From 6c10359251edbb086dc42f7dbc0a6064bc09a463 Mon Sep 17 00:00:00 2001 From: kolo Date: Wed, 25 Mar 2026 01:23:36 +0300 Subject: [PATCH] update --- lib/features/dashboard/screen.dart | 7 +- .../widgets/account_editor_overlay.dart | 1277 ----------------- .../account_editor_overlay.dart | 424 ++++++ .../account_editor_overlay/color_panel.dart | 578 ++++++++ .../account_editor_overlay/delete_dialog.dart | 71 + .../account_editor_overlay/editor_panel.dart | 198 +++ .../account_editor_overlay/panel_tab.dart | 85 ++ .../widgets/account_editor_overlay/utils.dart | 6 + 8 files changed, 1367 insertions(+), 1279 deletions(-) delete mode 100644 lib/features/dashboard/widgets/account_editor_overlay.dart create mode 100644 lib/features/dashboard/widgets/account_editor_overlay/account_editor_overlay.dart create mode 100644 lib/features/dashboard/widgets/account_editor_overlay/color_panel.dart create mode 100644 lib/features/dashboard/widgets/account_editor_overlay/delete_dialog.dart create mode 100644 lib/features/dashboard/widgets/account_editor_overlay/editor_panel.dart create mode 100644 lib/features/dashboard/widgets/account_editor_overlay/panel_tab.dart create mode 100644 lib/features/dashboard/widgets/account_editor_overlay/utils.dart diff --git a/lib/features/dashboard/screen.dart b/lib/features/dashboard/screen.dart index 6cfdcd2..5564783 100644 --- a/lib/features/dashboard/screen.dart +++ b/lib/features/dashboard/screen.dart @@ -8,7 +8,7 @@ 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/account_editor_overlay/account_editor_overlay.dart'; import 'widgets/balance_card_carousel.dart'; import 'widgets/budget_progress.dart'; import 'widgets/color_editor_overlay.dart'; @@ -315,7 +315,10 @@ class _DashboardScreenState extends ConsumerState { backgroundColor: const Color(0xFF7C6DED), foregroundColor: Colors.white, icon: const Icon(Icons.add), - label: Text(s.addTransactionDashboard, style: const TextStyle(fontWeight: FontWeight.w600)), + label: Text( + s.addTransactionDashboard, + style: const TextStyle(fontWeight: FontWeight.w600), + ), ), floatingActionButtonLocation: FloatingActionButtonLocation.endFloat, body: SafeArea( diff --git a/lib/features/dashboard/widgets/account_editor_overlay.dart b/lib/features/dashboard/widgets/account_editor_overlay.dart deleted file mode 100644 index c966915..0000000 --- a/lib/features/dashboard/widgets/account_editor_overlay.dart +++ /dev/null @@ -1,1277 +0,0 @@ -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 '../../../core/services/haptic_service.dart'; -import '../../../shared/models/account.dart'; -import '../../../shared/models/transaction.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; - bool _showCurrencyDropdown = false; - bool _showLimitError = false; - bool _showDeleteDialog = false; - bool _showDuplicateError = false; - - @override - void initState() { - super.initState(); - _nameController = TextEditingController(text: dash.tempAccountName); - _selectedCurrency = dash.tempAccountCurrency; - _nameController.addListener(() { - final text = _nameController.text; - - // Check if empty or exceeds limit - if (text.trim().isEmpty || text.length > 20) { - setState(() => _showLimitError = true); - Future.delayed(const Duration(seconds: 2), () { - if (mounted) setState(() => _showLimitError = false); - }); - } - - // Truncate if exceeds 20 characters - if (text.length > 20) { - _nameController.text = text.substring(0, 20); - _nameController.selection = TextSelection.fromPosition( - const TextPosition(offset: 20), - ); - return; // Skip updating dash to avoid out-of-bounds - } - - // Real-time update on card - dash.setState(() { - dash.tempAccountName = _nameController.text; - }); - dash.overlayEntry?.markNeedsBuild(); - }); - } - - @override - void dispose() { - _nameController.dispose(); - super.dispose(); - } - - bool _isDuplicateName(List accounts, String name) { - final trimmed = name.trim().toLowerCase(); - return accounts.any((a) { - // When editing: ignore the account being edited itself - if (dash.editingAccount != null && a.id == dash.editingAccount!.id) { - return false; - } - return a.name.trim().toLowerCase() == trimmed; - }); - } - - @override - Widget build(BuildContext context) { - final mq = MediaQuery.of(widget.context); - final cardTop = mq.padding.top + kToolbarHeight + 16; - const cardHeight = 220.0; - const editorPanelHeight = 102.0; // Increased from 90 to prevent overflow - final editorPanelTop = cardTop + cardHeight + 20; - final colorPanelTop = editorPanelTop + editorPanelHeight + 12; - const colorPanelHeight = 410.0; - - return Consumer( - builder: (context, ref, _) { - final exchangeService = ref.watch(exchangeRateServiceProvider); - - // Fix: If adding a new account, the balance is strictly 0.0 - double previewBalance = 0.0; - if (!dash.isAddingAccount) { - if (dash.editingAccount != null) { - final txs = ref.watch(accountFilteredTransactionsProvider); - final accountTxs = txs.where( - (t) => t.accountId == dash.editingAccount!.id, - ); - previewBalance = accountTxs.fold(0.0, (sum, t) { - final converted = exchangeService.convert( - t.amount, - t.currencyCode, - dash.tempAccountCurrency, // convert directly from tx currency to selected dropdown currency - ); - return t.type == TransactionType.income - ? sum + converted - : sum - converted; - }); - } else { - // Fallback for edge cases - previewBalance = ref.read(totalBalanceProvider); - } - } - - 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: () { - if (_showCurrencyDropdown) { - setState(() { - _showCurrencyDropdown = false; - }); - } else { - dash.closeAccountOverlay(apply: false); - } - }, - behavior: HitTestBehavior.translucent, - child: const SizedBox.expand(), - ), - ), - // Duplicate Name Error Alert (Top) - if (_showDuplicateError) - Positioned( - top: mq.padding.top + 8, - left: 20, - right: 20, - child: GestureDetector( - onTap: () {}, // Prevent tap-through - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - decoration: BoxDecoration( - color: Colors.red.shade50, - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: Colors.red.shade400, - width: 2, - ), - boxShadow: [ - BoxShadow( - color: Colors.red.withOpacity(0.3), - blurRadius: 12, - offset: const Offset(0, 4), - ), - ], - ), - child: Row( - children: [ - Icon( - Icons.error_outline_rounded, - color: Colors.red.shade700, - size: 22, - ), - const SizedBox(width: 12), - Expanded( - child: Text( - 'Account name already exists', - style: TextStyle( - fontSize: 13, - color: Colors.red.shade900, - fontWeight: FontWeight.w600, - ), - ), - ), - ], - ), - ), - ), - ), - // Preview Card - Positioned( - top: cardTop, - left: 20, - right: 20, - child: BalanceCard( - balance: previewBalance, - 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: () { - if (_showCurrencyDropdown) { - setState(() { - _showCurrencyDropdown = false; - }); - } - }, - behavior: HitTestBehavior.opaque, - child: _buildColorPanel(colorPanelHeight), - ), - ), - // Currency Dropdown - Above everything - if (_showCurrencyDropdown) - Positioned( - top: editorPanelTop + 62, - right: 34, // 20 (panel padding) + 14 (inner padding) - width: - (MediaQuery.of(context).size.width - 68) * - 0.25, // 25% of the inner row width - child: Material( - elevation: 12, - borderRadius: BorderRadius.circular(12), - child: Container( - decoration: BoxDecoration( - color: Theme.of(widget.context).colorScheme.surface, - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: Theme.of( - widget.context, - ).colorScheme.onSurface.withOpacity(0.15), - width: 1.5, - ), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: - [ - ('USD', '\$'), - ('EUR', '€'), - ('BYN', 'Br'), - ('RUB', '₽'), - ].map((entry) { - final isSelected = entry.$1 == _selectedCurrency; - return InkWell( - onTap: () { - setState(() { - _selectedCurrency = entry.$1; - // Update temp currency for preview - dash.setState(() { - dash.tempAccountCurrency = entry.$1; - }); - dash.overlayEntry?.markNeedsBuild(); - _showCurrencyDropdown = false; - }); - }, - borderRadius: BorderRadius.circular(12), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 8, - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - entry.$2, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - color: isSelected - ? const Color(0xFF7C6DED) - : null, - ), - ), - const SizedBox(width: 6), - Text( - entry.$1, - style: TextStyle( - fontSize: 11, - color: isSelected - ? const Color(0xFF7C6DED) - : Theme.of(widget.context) - .colorScheme - .onSurface - .withOpacity(0.6), - ), - ), - const SizedBox(width: 4), - if (isSelected) - const Icon( - Icons.check_rounded, - size: 14, - color: Color(0xFF7C6DED), - ), - ], - ), - ), - ); - }).toList(), - ), - ), - ), - ), - // Top Right Buttons (Delete & Close) - Positioned( - top: - cardTop - - 20, // Center buttons exactly on the top edge of the card - right: 20, - child: Row( - // REMOVED SafeArea to fix the vertical offset - mainAxisSize: MainAxisSize.min, - children: [ - if (!dash.isAddingAccount && - (ref.watch(accountsProvider).valueOrNull?.length ?? 0) > - 1) ...[ - GestureDetector( - onTap: () => setState(() => _showDeleteDialog = true), - child: Container( - width: 40, - height: 40, - decoration: BoxDecoration( - color: Theme.of(widget.context).colorScheme.surface, - shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - color: Colors.red.withOpacity(0.2), - blurRadius: 8, - offset: const Offset(0, 2), - ), - ], - ), - child: const Icon( - Icons.delete_outline_rounded, - size: 22, - color: Colors.red, - ), - ), - ), - const SizedBox(width: 12), - ], - 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, - ), - ), - ), - ], - ), - ), - // Custom Dialog Overlay - if (_showDeleteDialog) - Positioned.fill( - child: Container( - color: Colors.black.withOpacity(0.4), - child: Center( - child: Material( - color: Colors.transparent, - child: AlertDialog( - backgroundColor: Theme.of( - widget.context, - ).colorScheme.surface, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - ), - title: const Text('Delete Account?'), - content: const Text( - 'Are you sure you want to delete this account? All associated transactions will also be permanently deleted.', - ), - actions: [ - TextButton( - onPressed: () => - setState(() => _showDeleteDialog = false), - child: const Text('Cancel'), - ), - TextButton( - onPressed: () async { - if (dash.editingAccount == null) return; - - final accountId = dash.editingAccount!.id; - - dash.closeAccountOverlay(apply: false); - - final txs = - ref - .read(transactionsProvider) - .valueOrNull ?? - []; - final accountTxs = txs - .where((t) => t.accountId == accountId) - .toList(); - for (final t in accountTxs) { - await ref - .read(transactionsProvider.notifier) - .delete(t.id); - } - - await ref - .read(accountRepositoryProvider) - .delete(accountId); - - if (ref.read(hapticEnabledProvider)) { - HapticService.medium(); - } - }, - child: const Text( - 'Delete', - style: TextStyle(color: Colors.red), - ), - ), - ], - ), - ), - ), - ), - ), - ], - ), - ); - }, - ); - } - - Widget _buildEditorPanel(double panelHeight) { - return Consumer( - builder: (context, ref, _) { - 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: 14, vertical: 12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - 'Account Settings', - style: TextStyle( - fontSize: 11, - fontWeight: FontWeight.w600, - color: Theme.of( - widget.context, - ).colorScheme.onSurface.withOpacity(0.6), - ), - ), - const SizedBox(height: 8), - IntrinsicHeight( - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - flex: 3, - child: TextField( - controller: _nameController, - buildCounter: - ( - context, { - required currentLength, - required isFocused, - maxLength, - }) => null, - style: const TextStyle(fontSize: 13), - decoration: InputDecoration( - hintText: 'Account name', - hintStyle: TextStyle( - fontSize: 13, - color: Theme.of( - widget.context, - ).colorScheme.onSurface.withOpacity(0.4), - ), - filled: true, - fillColor: Theme.of( - widget.context, - ).colorScheme.onSurface.withOpacity(0.05), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide( - color: - (_showLimitError || - _showDuplicateError || - _nameController.text.trim().isEmpty) - ? Colors.red - : Theme.of( - widget.context, - ).colorScheme.onSurface.withOpacity(0.15), - width: 1.5, - ), - ), - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide( - color: - (_showLimitError || - _showDuplicateError || - _nameController.text.trim().isEmpty) - ? Colors.red - : Theme.of( - widget.context, - ).colorScheme.onSurface.withOpacity(0.15), - width: 1.5, - ), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide( - color: - (_showLimitError || - _showDuplicateError || - _nameController.text.trim().isEmpty) - ? Colors.red - : const Color(0xFF7C6DED), - width: 1.5, - ), - ), - contentPadding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 12, - ), - ), - ), - ), - const SizedBox(width: 8), - Expanded( - child: GestureDetector( - onTap: () { - setState(() { - _showCurrencyDropdown = !_showCurrencyDropdown; - }); - }, - child: Container( - height: double.infinity, - decoration: BoxDecoration( - color: Theme.of( - widget.context, - ).colorScheme.onSurface.withOpacity(0.05), - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: _showCurrencyDropdown - ? const Color(0xFF7C6DED) - : Theme.of( - widget.context, - ).colorScheme.onSurface.withOpacity(0.15), - width: 1.5, - ), - ), - padding: const EdgeInsets.symmetric(horizontal: 10), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - [ - ('USD', '\$'), - ('EUR', '€'), - ('BYN', 'Br'), - ('RUB', '₽'), - ] - .firstWhere( - (c) => c.$1 == _selectedCurrency, - ) - .$2, - style: const TextStyle( - fontSize: 15, - fontWeight: FontWeight.w600, - ), - ), - const SizedBox(width: 4), - Icon( - _showCurrencyDropdown - ? Icons.arrow_drop_up - : Icons.arrow_drop_down, - size: 20, - color: Theme.of( - widget.context, - ).colorScheme.onSurface.withOpacity(0.6), - ), - ], - ), - ), - ), - ), - ], - ), - ), - ], - ), - ), - ); - }, - ); - } - - 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), - ), - ), - Expanded( - child: 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: 4, - 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: _nameController.text.trim().isEmpty - ? null - : () { - final accounts = - ProviderScope.containerOf( - widget.context, - ).read(accountsProvider).valueOrNull ?? - []; - - if (_isDuplicateName( - accounts, - _nameController.text, - )) { - setState(() => _showDuplicateError = true); - Future.delayed( - const Duration(seconds: 3), - () { - if (mounted) - setState( - () => _showDuplicateError = false, - ); - }, - ); - return; // block saving - } - - HapticService.light(); - dash.closeAccountOverlay(apply: true); - }, - style: ElevatedButton.styleFrom( - backgroundColor: const Color(0xFF7C6DED), - foregroundColor: Colors.white, - disabledBackgroundColor: Theme.of( - widget.context, - ).colorScheme.onSurface.withOpacity(0.12), - disabledForegroundColor: Theme.of( - widget.context, - ).colorScheme.onSurface.withOpacity(0.38), - padding: const EdgeInsets.symmetric(vertical: 10), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - ), - child: Text( - dash.isAddingAccount ? s.addAccount : 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: 4, 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/account_editor_overlay/account_editor_overlay.dart b/lib/features/dashboard/widgets/account_editor_overlay/account_editor_overlay.dart new file mode 100644 index 0000000..99e9bdf --- /dev/null +++ b/lib/features/dashboard/widgets/account_editor_overlay/account_editor_overlay.dart @@ -0,0 +1,424 @@ +import 'dart:ui'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../../../shared/models/account.dart'; +import '../../../../shared/models/transaction.dart'; +import '../../../settings/provider.dart'; +import '../../provider.dart'; +import '../balance_card.dart'; +import './color_panel.dart'; +import './delete_dialog.dart'; +import './editor_panel.dart'; + +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; + bool _showCurrencyDropdown = false; + bool _showLimitError = false; + bool _showDeleteDialog = false; + bool _showDuplicateError = false; + + @override + void initState() { + super.initState(); + _nameController = TextEditingController(text: dash.tempAccountName); + _selectedCurrency = dash.tempAccountCurrency; + _nameController.addListener(() { + final text = _nameController.text; + + if (text.trim().isEmpty || text.length > 20) { + setState(() => _showLimitError = true); + Future.delayed(const Duration(seconds: 2), () { + if (mounted) setState(() => _showLimitError = false); + }); + } + + if (text.length > 20) { + _nameController.text = text.substring(0, 20); + _nameController.selection = TextSelection.fromPosition( + const TextPosition(offset: 20), + ); + return; + } + + dash.setState(() { + dash.tempAccountName = _nameController.text; + }); + dash.overlayEntry?.markNeedsBuild(); + }); + } + + @override + void dispose() { + _nameController.dispose(); + super.dispose(); + } + + bool _isDuplicateName(List accounts, String name) { + final trimmed = name.trim().toLowerCase(); + return accounts.any((a) { + if (dash.editingAccount != null && a.id == dash.editingAccount!.id) { + return false; + } + return a.name.trim().toLowerCase() == trimmed; + }); + } + + @override + Widget build(BuildContext context) { + final mq = MediaQuery.of(widget.context); + final cardTop = mq.padding.top + kToolbarHeight + 16; + const cardHeight = 220.0; + const editorPanelHeight = 102.0; + final editorPanelTop = cardTop + cardHeight + 20; + final colorPanelTop = editorPanelTop + editorPanelHeight + 12; + const colorPanelHeight = 410.0; + + return Consumer( + builder: (context, ref, _) { + final exchangeService = ref.watch(exchangeRateServiceProvider); + + double previewBalance = 0.0; + if (!dash.isAddingAccount) { + if (dash.editingAccount != null) { + final txs = ref.watch(accountFilteredTransactionsProvider); + final accountTxs = txs.where( + (t) => t.accountId == dash.editingAccount!.id, + ); + previewBalance = accountTxs.fold(0.0, (sum, t) { + final converted = exchangeService.convert( + t.amount, + t.currencyCode, + dash.tempAccountCurrency, + ); + return t.type == TransactionType.income + ? sum + converted + : sum - converted; + }); + } else { + previewBalance = ref.read(totalBalanceProvider); + } + } + + 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: () { + if (_showCurrencyDropdown) { + setState(() { + _showCurrencyDropdown = false; + }); + } else { + dash.closeAccountOverlay(apply: false); + } + }, + behavior: HitTestBehavior.translucent, + child: const SizedBox.expand(), + ), + ), + if (_showDuplicateError) + Positioned( + top: mq.padding.top + 8, + left: 20, + right: 20, + child: GestureDetector( + onTap: () {}, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + decoration: BoxDecoration( + color: Colors.red.shade50, + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: Colors.red.shade400, + width: 2, + ), + boxShadow: [ + BoxShadow( + color: Colors.red.withOpacity(0.3), + blurRadius: 12, + offset: const Offset(0, 4), + ), + ], + ), + child: Row( + children: [ + Icon( + Icons.error_outline_rounded, + color: Colors.red.shade700, + size: 22, + ), + const SizedBox(width: 12), + Expanded( + child: Text( + 'Account name already exists', + style: TextStyle( + fontSize: 13, + color: Colors.red.shade900, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ), + ), + ), + Positioned( + top: cardTop, + left: 20, + right: 20, + child: BalanceCard( + balance: previewBalance, + currencyInfo: CurrencyInfo( + currencyMap[dash.tempAccountCurrency]?.symbol ?? '\$', + dash.tempAccountCurrency, + ), + onLongPress: null, + accountName: dash.tempAccountName, + previewPrimary: dash.tempPrimary, + previewSecondary: dash.tempSecondary, + previewGradientType: dash.tempGradientType, + ), + ), + Positioned( + top: editorPanelTop, + left: 20, + right: 20, + child: GestureDetector( + onTap: () {}, + behavior: HitTestBehavior.opaque, + child: AccountEditorPanel( + nameController: _nameController, + selectedCurrency: _selectedCurrency, + showCurrencyDropdown: _showCurrencyDropdown, + showLimitError: _showLimitError, + showDuplicateError: _showDuplicateError, + onCurrencyDropdownToggle: () { + setState(() { + _showCurrencyDropdown = !_showCurrencyDropdown; + }); + }, + dashboardContext: widget.context, + panelHeight: editorPanelHeight, + ), + ), + ), + Positioned( + top: colorPanelTop, + left: 20, + right: 20, + child: GestureDetector( + onTap: () { + if (_showCurrencyDropdown) { + setState(() { + _showCurrencyDropdown = false; + }); + } + }, + behavior: HitTestBehavior.opaque, + child: AccountColorPanel( + dashboardState: dash, + dashboardContext: widget.context, + panelHeight: colorPanelHeight, + isDuplicateName: _isDuplicateName, + onDuplicateError: () { + setState(() => _showDuplicateError = true); + Future.delayed(const Duration(seconds: 3), () { + if (mounted) { + setState(() => _showDuplicateError = false); + } + }); + }, + ), + ), + ), + if (_showCurrencyDropdown) + Positioned( + top: editorPanelTop + 62, + right: 34, + width: (MediaQuery.of(context).size.width - 68) * 0.25, + child: Material( + elevation: 12, + borderRadius: BorderRadius.circular(12), + child: Container( + decoration: BoxDecoration( + color: Theme.of(widget.context).colorScheme.surface, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: Theme.of( + widget.context, + ).colorScheme.onSurface.withOpacity(0.15), + width: 1.5, + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: + [ + ('USD', '\$'), + ('EUR', '€'), + ('BYN', 'Br'), + ('RUB', '₽'), + ].map((entry) { + final isSelected = entry.$1 == _selectedCurrency; + return InkWell( + onTap: () { + setState(() { + _selectedCurrency = entry.$1; + dash.setState(() { + dash.tempAccountCurrency = entry.$1; + }); + dash.overlayEntry?.markNeedsBuild(); + _showCurrencyDropdown = false; + }); + }, + borderRadius: BorderRadius.circular(12), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 8, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + entry.$2, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: isSelected + ? const Color(0xFF7C6DED) + : null, + ), + ), + const SizedBox(width: 6), + Text( + entry.$1, + style: TextStyle( + fontSize: 11, + color: isSelected + ? const Color(0xFF7C6DED) + : Theme.of(widget.context) + .colorScheme + .onSurface + .withOpacity(0.6), + ), + ), + const SizedBox(width: 4), + if (isSelected) + const Icon( + Icons.check_rounded, + size: 14, + color: Color(0xFF7C6DED), + ), + ], + ), + ), + ); + }).toList(), + ), + ), + ), + ), + Positioned( + top: cardTop - 20, + right: 20, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (!dash.isAddingAccount && + (ref.watch(accountsProvider).valueOrNull?.length ?? 0) > + 1) ...[ + GestureDetector( + onTap: () => setState(() => _showDeleteDialog = true), + child: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: Theme.of(widget.context).colorScheme.surface, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.red.withOpacity(0.2), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: const Icon( + Icons.delete_outline_rounded, + size: 22, + color: Colors.red, + ), + ), + ), + const SizedBox(width: 12), + ], + 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, + ), + ), + ), + ], + ), + ), + if (_showDeleteDialog) + Positioned.fill( + child: AccountDeleteDialog( + editingAccount: dash.editingAccount, + onCancel: () => setState(() => _showDeleteDialog = false), + onConfirm: () => dash.closeAccountOverlay(apply: false), + ), + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/features/dashboard/widgets/account_editor_overlay/color_panel.dart b/lib/features/dashboard/widgets/account_editor_overlay/color_panel.dart new file mode 100644 index 0000000..420c22f --- /dev/null +++ b/lib/features/dashboard/widgets/account_editor_overlay/color_panel.dart @@ -0,0 +1,578 @@ +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 '../../../../core/services/haptic_service.dart'; +import '../../../../shared/models/account.dart'; +import '../../provider.dart'; +import './panel_tab.dart'; +import './utils.dart' as utils; + +class AccountColorPanel extends StatelessWidget { + final dynamic dashboardState; + final BuildContext dashboardContext; + final double panelHeight; + final bool Function(List, String) isDuplicateName; + final VoidCallback onDuplicateError; + + const AccountColorPanel({ + super.key, + required this.dashboardState, + required this.dashboardContext, + required this.panelHeight, + required this.isDuplicateName, + required this.onDuplicateError, + }); + + @override + Widget build(BuildContext context) { + return Container( + height: panelHeight, + decoration: BoxDecoration( + color: Theme.of(dashboardContext).colorScheme.surface, + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: Theme.of( + dashboardContext, + ).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(dashboardContext).read(localeProvider), + ); + + void onHSVChanged(HSVColor hsv) { + setPanelState(() {}); + dashboardState.setState(() { + if (dashboardState.editingPrimary) { + dashboardState.tempPrimaryHSV = hsv; + dashboardState.tempPrimary = hsv.toColor(); + } else { + dashboardState.tempSecondaryHSV = hsv; + dashboardState.tempSecondary = hsv.toColor(); + } + }); + dashboardState.overlayEntry?.markNeedsBuild(); + } + + final isSolid = dashboardState.tempGradientType == GradientType.solid; + final currentHSV = (isSolid || dashboardState.editingPrimary) + ? dashboardState.tempPrimaryHSV + : dashboardState.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: dashboardState.editingPrimary, + color: dashboardState.tempPrimary, + isDimmed: isSolid, + onTap: () { + dashboardState.setState(() { + if (isSolid) + dashboardState.tempGradientType = + CardColorService.defaultGradient; + dashboardState.editingPrimary = true; + }); + setPanelState(() {}); + dashboardState.overlayEntry?.markNeedsBuild(); + }, + ), + ), + const SizedBox(width: 6), + Expanded( + child: PanelTab( + label: s.colorSecondary, + isSelected: !dashboardState.editingPrimary, + color: dashboardState.tempSecondary, + isDimmed: isSolid, + onTap: () { + dashboardState.setState(() { + if (isSolid) + dashboardState.tempGradientType = + CardColorService.defaultGradient; + dashboardState.editingPrimary = false; + }); + setPanelState(() {}); + dashboardState.overlayEntry?.markNeedsBuild(); + }, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Container( + width: 1, + color: Theme.of( + dashboardContext, + ).colorScheme.onSurface.withOpacity(0.15), + margin: const EdgeInsets.symmetric(vertical: 4), + ), + ), + Expanded( + child: GestureDetector( + onTap: isSolid + ? null + : () { + dashboardState.setState(() { + dashboardState.tempGradientType = + GradientType.solid; + dashboardState.editingPrimary = true; + }); + setPanelState(() {}); + dashboardState.overlayEntry?.markNeedsBuild(); + }, + child: Container( + height: double.infinity, + padding: const EdgeInsets.symmetric( + horizontal: 4, + 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( + dashboardContext, + ).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(dashboardContext) + .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: () { + dashboardState.setState( + () => dashboardState.editingPrimary = + true, + ); + setPanelState(() {}); + dashboardState.overlayEntry + ?.markNeedsBuild(); + }, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 22, + height: 22, + decoration: BoxDecoration( + color: dashboardState.tempPrimary, + borderRadius: + BorderRadius.circular(6), + border: + dashboardState.editingPrimary + ? Border.all( + color: Colors.white, + width: 2, + ) + : Border.all( + color: Colors.transparent, + width: 2, + ), + ), + ), + const SizedBox(width: 5), + Text( + '#${utils.colorToHex(dashboardState.tempPrimary)}', + style: TextStyle( + fontSize: 11, + fontFamily: 'monospace', + fontWeight: + dashboardState.editingPrimary + ? FontWeight.w600 + : FontWeight.normal, + color: Theme.of(dashboardContext) + .colorScheme + .onSurface + .withOpacity( + dashboardState + .editingPrimary + ? 0.8 + : 0.4, + ), + ), + ), + ], + ), + ), + const Spacer(), + if (!isSolid) + GestureDetector( + onTap: () { + dashboardState.setState( + () => + dashboardState.editingPrimary = + false, + ); + setPanelState(() {}); + dashboardState.overlayEntry + ?.markNeedsBuild(); + }, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '#${utils.colorToHex(dashboardState.tempSecondary)}', + style: TextStyle( + fontSize: 11, + fontFamily: 'monospace', + fontWeight: + !dashboardState + .editingPrimary + ? FontWeight.w600 + : FontWeight.normal, + color: Theme.of(dashboardContext) + .colorScheme + .onSurface + .withOpacity( + !dashboardState + .editingPrimary + ? 0.8 + : 0.4, + ), + ), + ), + const SizedBox(width: 5), + Container( + width: 22, + height: 22, + decoration: BoxDecoration( + color: dashboardState + .tempSecondary, + borderRadius: + BorderRadius.circular(6), + border: + !dashboardState + .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 = + dashboardState.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: () { + dashboardState.setState( + () => dashboardState.tempGradientType = + type, + ); + setPanelState(() {}); + dashboardState.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(dashboardContext) + .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(dashboardContext) + .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(dashboardContext) + .colorScheme + .onSurface + .withOpacity(0.45), + ), + ), + ], + ), + ), + ), + ), + ); + }) + .toList(), + ), + ), + ), + const SizedBox(height: 8), + Row( + children: [ + Expanded( + child: OutlinedButton.icon( + onPressed: () { + final isDarkTheme = + Theme.of(dashboardContext).brightness == + Brightness.dark; + final defP = isDarkTheme + ? CardColorService.defaultPrimary + : CardColorService.defaultPrimaryLight; + final defS = isDarkTheme + ? CardColorService.defaultSecondary + : CardColorService.defaultSecondaryLight; + dashboardState.setState(() { + dashboardState.tempPrimary = defP; + dashboardState.tempSecondary = defS; + dashboardState.tempPrimaryHSV = HSVColor.fromColor( + defP, + ); + dashboardState.tempSecondaryHSV = + HSVColor.fromColor(defS); + dashboardState.tempGradientType = + CardColorService.defaultGradient; + }); + setPanelState(() {}); + dashboardState.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( + dashboardContext, + ).colorScheme.onSurface.withOpacity(0.7), + side: BorderSide( + color: Theme.of( + dashboardContext, + ).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: Consumer( + builder: (context, ref, _) { + return ElevatedButton( + onPressed: + dashboardState.tempAccountName.trim().isEmpty + ? null + : () { + final accounts = + ProviderScope.containerOf( + dashboardContext, + ).read(accountsProvider).valueOrNull ?? + []; + + if (isDuplicateName( + accounts, + dashboardState.tempAccountName, + )) { + onDuplicateError(); + return; + } + + HapticService.light(); + dashboardState.closeAccountOverlay( + apply: true, + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF7C6DED), + foregroundColor: Colors.white, + disabledBackgroundColor: Theme.of( + dashboardContext, + ).colorScheme.onSurface.withOpacity(0.12), + disabledForegroundColor: Theme.of( + dashboardContext, + ).colorScheme.onSurface.withOpacity(0.38), + padding: const EdgeInsets.symmetric(vertical: 10), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: Text( + dashboardState.isAddingAccount + ? s.addAccount + : s.apply, + style: const TextStyle( + fontWeight: FontWeight.w700, + fontSize: 14, + ), + ), + ); + }, + ), + ), + ], + ), + ], + ), + ); + }, + ), + ); + } +} diff --git a/lib/features/dashboard/widgets/account_editor_overlay/delete_dialog.dart b/lib/features/dashboard/widgets/account_editor_overlay/delete_dialog.dart new file mode 100644 index 0000000..be7570d --- /dev/null +++ b/lib/features/dashboard/widgets/account_editor_overlay/delete_dialog.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../../../core/services/haptic_service.dart'; +import '../../../../shared/models/account.dart'; +import '../../provider.dart'; +import '../../../settings/provider.dart'; + +class AccountDeleteDialog extends ConsumerWidget { + final Account? editingAccount; + final VoidCallback onCancel; + final VoidCallback onConfirm; + + const AccountDeleteDialog({ + super.key, + required this.editingAccount, + required this.onCancel, + required this.onConfirm, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Container( + color: Colors.black.withOpacity(0.4), + child: Center( + child: Material( + color: Colors.transparent, + child: AlertDialog( + backgroundColor: Theme.of(context).colorScheme.surface, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + title: const Text('Delete Account?'), + content: const Text( + 'Are you sure you want to delete this account? All associated transactions will also be permanently deleted.', + ), + actions: [ + TextButton(onPressed: onCancel, child: const Text('Cancel')), + TextButton( + onPressed: () async { + if (editingAccount == null) return; + + final accountId = editingAccount!.id; + + onConfirm(); + + final txs = ref.read(transactionsProvider).valueOrNull ?? []; + final accountTxs = txs + .where((t) => t.accountId == accountId) + .toList(); + for (final t in accountTxs) { + await ref.read(transactionsProvider.notifier).delete(t.id); + } + + await ref.read(accountRepositoryProvider).delete(accountId); + + if (ref.read(hapticEnabledProvider)) { + HapticService.medium(); + } + }, + child: const Text( + 'Delete', + style: TextStyle(color: Colors.red), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/features/dashboard/widgets/account_editor_overlay/editor_panel.dart b/lib/features/dashboard/widgets/account_editor_overlay/editor_panel.dart new file mode 100644 index 0000000..4da5b9c --- /dev/null +++ b/lib/features/dashboard/widgets/account_editor_overlay/editor_panel.dart @@ -0,0 +1,198 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class AccountEditorPanel extends ConsumerWidget { + final TextEditingController nameController; + final String selectedCurrency; + final bool showCurrencyDropdown; + final bool showLimitError; + final bool showDuplicateError; + final VoidCallback onCurrencyDropdownToggle; + final BuildContext dashboardContext; + final double panelHeight; + + const AccountEditorPanel({ + super.key, + required this.nameController, + required this.selectedCurrency, + required this.showCurrencyDropdown, + required this.showLimitError, + required this.showDuplicateError, + required this.onCurrencyDropdownToggle, + required this.dashboardContext, + required this.panelHeight, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Container( + height: panelHeight, + decoration: BoxDecoration( + color: Theme.of(dashboardContext).colorScheme.surface, + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: Theme.of( + dashboardContext, + ).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: 14, vertical: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Account Settings', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: Theme.of( + dashboardContext, + ).colorScheme.onSurface.withOpacity(0.6), + ), + ), + const SizedBox(height: 8), + IntrinsicHeight( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + flex: 3, + child: TextField( + controller: nameController, + buildCounter: + ( + context, { + required currentLength, + required isFocused, + maxLength, + }) => null, + style: const TextStyle(fontSize: 13), + decoration: InputDecoration( + hintText: 'Account name', + hintStyle: TextStyle( + fontSize: 13, + color: Theme.of( + dashboardContext, + ).colorScheme.onSurface.withOpacity(0.4), + ), + filled: true, + fillColor: Theme.of( + dashboardContext, + ).colorScheme.onSurface.withOpacity(0.05), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide( + color: + (showLimitError || + showDuplicateError || + nameController.text.trim().isEmpty) + ? Colors.red + : Theme.of( + dashboardContext, + ).colorScheme.onSurface.withOpacity(0.15), + width: 1.5, + ), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide( + color: + (showLimitError || + showDuplicateError || + nameController.text.trim().isEmpty) + ? Colors.red + : Theme.of( + dashboardContext, + ).colorScheme.onSurface.withOpacity(0.15), + width: 1.5, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide( + color: + (showLimitError || + showDuplicateError || + nameController.text.trim().isEmpty) + ? Colors.red + : const Color(0xFF7C6DED), + width: 1.5, + ), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 12, + ), + ), + ), + ), + const SizedBox(width: 8), + Expanded( + child: GestureDetector( + onTap: onCurrencyDropdownToggle, + child: Container( + height: double.infinity, + decoration: BoxDecoration( + color: Theme.of( + dashboardContext, + ).colorScheme.onSurface.withOpacity(0.05), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: showCurrencyDropdown + ? const Color(0xFF7C6DED) + : Theme.of( + dashboardContext, + ).colorScheme.onSurface.withOpacity(0.15), + width: 1.5, + ), + ), + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + [ + ('USD', '\$'), + ('EUR', '€'), + ('BYN', 'Br'), + ('RUB', '₽'), + ].firstWhere((c) => c.$1 == selectedCurrency).$2, + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(width: 4), + Icon( + showCurrencyDropdown + ? Icons.arrow_drop_up + : Icons.arrow_drop_down, + size: 20, + color: Theme.of( + dashboardContext, + ).colorScheme.onSurface.withOpacity(0.6), + ), + ], + ), + ), + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/dashboard/widgets/account_editor_overlay/panel_tab.dart b/lib/features/dashboard/widgets/account_editor_overlay/panel_tab.dart new file mode 100644 index 0000000..6c35a96 --- /dev/null +++ b/lib/features/dashboard/widgets/account_editor_overlay/panel_tab.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; + +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: 4, 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/account_editor_overlay/utils.dart b/lib/features/dashboard/widgets/account_editor_overlay/utils.dart new file mode 100644 index 0000000..1e6e4f0 --- /dev/null +++ b/lib/features/dashboard/widgets/account_editor_overlay/utils.dart @@ -0,0 +1,6 @@ +import 'package:flutter/material.dart'; + +String colorToHex(Color color) { + final hex = color.value.toRadixString(16).padLeft(8, '0').toUpperCase(); + return hex.substring(2); +}