From ae464ff30483ab6448d51d3783c0fca09be4170b Mon Sep 17 00:00:00 2001 From: davmar Date: Wed, 9 Sep 2026 14:38:17 +0200 Subject: [PATCH] better UI --- lib/screens/home_screen.dart | 3 +- lib/screens/label_transactions_screen.dart | 6 +- lib/screens/main_shell.dart | 1 + lib/screens/overview_screen.dart | 175 +++--- lib/screens/transaction_detail_screen.dart | 3 +- lib/screens/transactions_screen.dart | 75 ++- lib/utils/app_theme.dart | 332 +++++++++-- lib/widgets/label_filter_dialog.dart | 642 +++++++++++---------- lib/widgets/similar_match_dialog.dart | 5 +- 9 files changed, 771 insertions(+), 471 deletions(-) diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index f32c311..6727220 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -6,6 +6,7 @@ import 'package:provider/provider.dart'; import '../providers/bank_provider.dart'; import '../providers/tx_meta_provider.dart'; +import '../utils/app_theme.dart'; import '../utils/connect_bank.dart'; import '../utils/helpers.dart'; import '../utils/navigation.dart'; @@ -232,7 +233,7 @@ class _HomeBody extends StatelessWidget { trailing: Text( formatMoney(tx.amount, tx.currency), style: theme.textTheme.titleSmall?.copyWith( - color: tx.amount >= 0 ? colors.primary : colors.error, + color: tx.amount >= 0 ? AppTheme.accent : AppTheme.expense, ), ), onTap: () { diff --git a/lib/screens/label_transactions_screen.dart b/lib/screens/label_transactions_screen.dart index 039b0cf..262d86f 100644 --- a/lib/screens/label_transactions_screen.dart +++ b/lib/screens/label_transactions_screen.dart @@ -7,6 +7,7 @@ import 'package:provider/provider.dart'; import '../models/transaction_meta.dart'; import '../providers/bank_provider.dart'; import '../providers/tx_meta_provider.dart'; +import '../utils/app_theme.dart'; import '../utils/helpers.dart'; import '../widgets/responsive_body.dart'; import '../widgets/skip_build_when_covered.dart'; @@ -33,7 +34,6 @@ class LabelTransactionsScreen extends StatelessWidget { if (meta.labelIdsFor(tx).contains(label.id)) tx, ]; final theme = Theme.of(context); - final colors = theme.colorScheme; final currency = bank.balance?.currency ?? (txs.isNotEmpty ? txs.first.currency : 'EUR'); var totalOut = 0.0; @@ -75,8 +75,8 @@ class LabelTransactionsScreen extends StatelessWidget { formatMoney(tx.amount, tx.currency), style: theme.textTheme.titleSmall?.copyWith( color: tx.amount >= 0 - ? colors.primary - : colors.error, + ? AppTheme.accent + : AppTheme.expense, ), ), onTap: () { diff --git a/lib/screens/main_shell.dart b/lib/screens/main_shell.dart index 44ada4a..1e50b59 100644 --- a/lib/screens/main_shell.dart +++ b/lib/screens/main_shell.dart @@ -31,6 +31,7 @@ class _MainShellState extends State { bottomNavigationBar: NavigationBar( selectedIndex: _index, onDestinationSelected: (index) => setState(() => _index = index), + labelBehavior: NavigationDestinationLabelBehavior.alwaysShow, destinations: const [ NavigationDestination( icon: Icon(Icons.home_outlined), diff --git a/lib/screens/overview_screen.dart b/lib/screens/overview_screen.dart index 9ad5d3b..3c4d7a2 100644 --- a/lib/screens/overview_screen.dart +++ b/lib/screens/overview_screen.dart @@ -9,6 +9,7 @@ import '../models/bank.dart'; import '../models/transaction_meta.dart'; import '../providers/bank_provider.dart'; import '../providers/tx_meta_provider.dart'; +import '../utils/app_theme.dart'; import '../utils/helpers.dart'; import '../utils/navigation.dart'; import '../utils/tx_calendar.dart'; @@ -24,9 +25,8 @@ enum _PeriodMode { monthly, yearly } enum _MetricMode { flow, labels } -/// Distinct, intuitive colors (avoid two greens). -const _kIncomeColor = Color(0xFF2563EB); // blue -const _kExpenseColor = Color(0xFFDC2626); // red +const _kIncomeColor = AppTheme.accent; +const _kExpenseColor = AppTheme.expense; class OverviewScreen extends StatefulWidget { const OverviewScreen({super.key}); @@ -282,7 +282,7 @@ class _OverviewScreenState extends State { Expanded( child: _SummaryCard( icon: Icons.south_west, - iconBackground: _kIncomeColor.withValues(alpha: 0.12), + iconBackground: _kIncomeColor.withValues(alpha: 0.18), iconColor: _kIncomeColor, amount: formatMoney(summaryFlow.income, currency), label: 'Income', @@ -292,7 +292,7 @@ class _OverviewScreenState extends State { Expanded( child: _SummaryCard( icon: Icons.north_east, - iconBackground: _kExpenseColor.withValues(alpha: 0.12), + iconBackground: _kExpenseColor.withValues(alpha: 0.18), iconColor: _kExpenseColor, amount: formatMoney(summaryFlow.expense, currency), label: 'Expenses', @@ -300,19 +300,12 @@ class _OverviewScreenState extends State { ), ], ), - const SizedBox(height: 8), - Text( - _period == _PeriodMode.monthly - ? 'Totals for ${_cursor.year}' - : 'Totals across all years', - style: theme.textTheme.labelMedium, - ), ] else ...[ _SummaryCard( icon: Icons.label_outline, iconBackground: - theme.colorScheme.secondaryContainer.withValues(alpha: 0.6), - iconColor: theme.colorScheme.onSecondaryContainer, + theme.colorScheme.primary.withValues(alpha: 0.16), + iconColor: theme.colorScheme.primary, amount: formatMoney( labelSpend.fold(0, (s, e) => s + e.amount), currency, @@ -324,12 +317,14 @@ class _OverviewScreenState extends State { }, ), ], - const SizedBox(height: 20), + const SizedBox(height: 22), Text( _metric == _MetricMode.flow ? 'History · Income & expenses' : 'History · Labels', - style: theme.textTheme.titleLarge, + style: theme.textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.w700, + ), ), const SizedBox(height: 4), Text( @@ -672,35 +667,29 @@ class _ChartCard extends StatelessWidget { final colors = theme.colorScheme; return Card( - color: colors.surface, + color: colors.surfaceContainer, elevation: 0, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(24), - side: BorderSide(color: colors.outlineVariant.withValues(alpha: 0.5)), + borderRadius: BorderRadius.circular(20), ), child: Padding( - padding: const EdgeInsets.fromLTRB(12, 12, 12, 16), + padding: const EdgeInsets.fromLTRB(14, 14, 14, 16), child: Column( children: [ Row( children: [ SegmentedButton<_PeriodMode>( showSelectedIcon: false, - style: ButtonStyle( - visualDensity: const VisualDensity( + style: const ButtonStyle( + visualDensity: VisualDensity( horizontal: -4, vertical: -3, ), tapTargetSize: MaterialTapTargetSize.shrinkWrap, - padding: const WidgetStatePropertyAll( - EdgeInsets.symmetric(horizontal: 8), - ), - minimumSize: const WidgetStatePropertyAll(Size(0, 28)), - textStyle: WidgetStatePropertyAll( - theme.textTheme.labelSmall?.copyWith( - fontWeight: FontWeight.w600, - ), + padding: WidgetStatePropertyAll( + EdgeInsets.symmetric(horizontal: 10), ), + minimumSize: WidgetStatePropertyAll(Size(0, 32)), ), segments: const [ ButtonSegment( @@ -718,7 +707,7 @@ class _ChartCard extends StatelessWidget { }, ), if (metric == _MetricMode.labels) ...[ - const SizedBox(width: 2), + const SizedBox(width: 4), IconButton( tooltip: includedLabelCount == null ? 'Filter labels ($availableLabelCount)' @@ -729,46 +718,41 @@ class _ChartCard extends StatelessWidget { vertical: -4, ), constraints: const BoxConstraints( - minWidth: 28, - minHeight: 28, + minWidth: 32, + minHeight: 32, ), padding: EdgeInsets.zero, style: IconButton.styleFrom( backgroundColor: includedLabelCount == null - ? null - : colors.secondaryContainer, + ? colors.surfaceContainerHigh + : colors.primary.withValues(alpha: 0.22), + foregroundColor: includedLabelCount == null + ? colors.onSurfaceVariant + : colors.primary, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), ), icon: Badge( isLabelVisible: includedLabelCount != null, smallSize: 6, - child: Icon( - Icons.filter_list, - size: 18, - color: includedLabelCount == null - ? colors.onSurfaceVariant - : colors.onSecondaryContainer, - ), + child: const Icon(Icons.filter_list, size: 18), ), ), ], const Spacer(), SegmentedButton<_MetricMode>( showSelectedIcon: false, - style: ButtonStyle( - visualDensity: const VisualDensity( + style: const ButtonStyle( + visualDensity: VisualDensity( horizontal: -4, vertical: -3, ), tapTargetSize: MaterialTapTargetSize.shrinkWrap, - padding: const WidgetStatePropertyAll( - EdgeInsets.symmetric(horizontal: 8), - ), - minimumSize: const WidgetStatePropertyAll(Size(0, 28)), - textStyle: WidgetStatePropertyAll( - theme.textTheme.labelSmall?.copyWith( - fontWeight: FontWeight.w600, - ), + padding: WidgetStatePropertyAll( + EdgeInsets.symmetric(horizontal: 10), ), + minimumSize: WidgetStatePropertyAll(Size(0, 32)), ), segments: const [ ButtonSegment( @@ -787,7 +771,7 @@ class _ChartCard extends StatelessWidget { ), ], ), - const SizedBox(height: 6), + const SizedBox(height: 8), if (period == _PeriodMode.monthly) Row( children: [ @@ -809,8 +793,8 @@ class _ChartCard extends StatelessWidget { child: Text( periodTitle, textAlign: TextAlign.center, - style: theme.textTheme.labelLarge?.copyWith( - fontWeight: FontWeight.w600, + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w700, ), ), ), @@ -836,8 +820,8 @@ class _ChartCard extends StatelessWidget { child: Text( periodTitle, textAlign: TextAlign.center, - style: theme.textTheme.labelLarge?.copyWith( - fontWeight: FontWeight.w600, + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w700, ), ), ), @@ -1016,8 +1000,9 @@ class _FlowBars extends StatelessWidget { drawVerticalLine: false, horizontalInterval: safeMax / 3, getDrawingHorizontalLine: (value) => FlLine( - color: colors.outlineVariant.withValues(alpha: 0.45), + color: colors.outlineVariant.withValues(alpha: 0.35), strokeWidth: 1, + dashArray: const [4, 4], ), ), borderData: FlBorderData(show: false), @@ -1209,8 +1194,9 @@ class _LabelMonthBars extends StatelessWidget { drawVerticalLine: false, horizontalInterval: safeMax / 3, getDrawingHorizontalLine: (value) => FlLine( - color: colors.outlineVariant.withValues(alpha: 0.45), + color: colors.outlineVariant.withValues(alpha: 0.35), strokeWidth: 1, + dashArray: const [4, 4], ), ), borderData: FlBorderData(show: false), @@ -1291,26 +1277,25 @@ class _SummaryCard extends StatelessWidget { final colors = theme.colorScheme; return Card( - color: colors.surface, + color: colors.surfaceContainer, elevation: 0, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - side: BorderSide(color: colors.outlineVariant.withValues(alpha: 0.5)), + borderRadius: BorderRadius.circular(18), ), child: Padding( - padding: const EdgeInsets.fromLTRB(12, 14, 12, 14), + padding: const EdgeInsets.fromLTRB(14, 14, 14, 14), child: Row( children: [ Container( - width: 40, - height: 40, + width: 42, + height: 42, decoration: BoxDecoration( color: iconBackground, borderRadius: BorderRadius.circular(12), ), child: Icon(icon, color: iconColor, size: 22), ), - const SizedBox(width: 10), + const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -1321,9 +1306,18 @@ class _SummaryCard extends StatelessWidget { overflow: TextOverflow.ellipsis, style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w700, + color: colors.onSurface, + ), + ), + const SizedBox(height: 2), + Text( + label.toUpperCase(), + style: theme.textTheme.labelSmall?.copyWith( + letterSpacing: 0.6, + fontWeight: FontWeight.w600, + color: colors.onSurfaceVariant, ), ), - Text(label, style: theme.textTheme.labelMedium), ], ), ), @@ -1355,16 +1349,16 @@ class _HistoryCard extends StatelessWidget { Widget build(BuildContext context) { final theme = Theme.of(context); final colors = theme.colorScheme; + final isYear = title.length == 4 && int.tryParse(title) != null; return Card( - color: colors.surface, + color: colors.surfaceContainer, elevation: 0, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(18), - side: BorderSide(color: colors.outlineVariant.withValues(alpha: 0.5)), + borderRadius: BorderRadius.circular(16), ), child: InkWell( - borderRadius: BorderRadius.circular(18), + borderRadius: BorderRadius.circular(16), onTap: onTap, child: Padding( padding: const EdgeInsets.fromLTRB(16, 14, 16, 14), @@ -1375,16 +1369,19 @@ class _HistoryCard extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - month.month == 1 && title.length == 4 ? 'Year' : 'Month', - style: theme.textTheme.labelMedium, + isYear ? 'YEAR' : 'MONTH', + style: theme.textTheme.labelSmall?.copyWith( + letterSpacing: 0.8, + fontWeight: FontWeight.w600, + color: colors.onSurfaceVariant, + ), ), - const SizedBox(height: 2), + const SizedBox(height: 4), Text( - title.length == 4 && int.tryParse(title) != null - ? title - : formatMonthYear(month), + isYear ? title : formatMonthYear(month), style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w700, + color: colors.onSurface, ), ), ], @@ -1397,15 +1394,15 @@ class _HistoryCard extends StatelessWidget { formatMoney(income, currency), style: theme.textTheme.titleSmall?.copyWith( color: _kIncomeColor, - fontWeight: FontWeight.w600, + fontWeight: FontWeight.w700, ), ), - const SizedBox(height: 2), + const SizedBox(height: 4), Text( formatMoney(-expense, currency), style: theme.textTheme.titleSmall?.copyWith( color: _kExpenseColor, - fontWeight: FontWeight.w600, + fontWeight: FontWeight.w700, ), ), ], @@ -1444,11 +1441,10 @@ class _LabelHistoryCard extends StatelessWidget { final total = rows.fold(0, (sum, row) => sum + row.amount); return Card( - color: colors.surface, + color: colors.surfaceContainer, elevation: 0, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(18), - side: BorderSide(color: colors.outlineVariant.withValues(alpha: 0.5)), + borderRadius: BorderRadius.circular(16), ), clipBehavior: Clip.antiAlias, child: Column( @@ -1465,14 +1461,19 @@ class _LabelHistoryCard extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - isYear ? 'Year' : 'Month', - style: theme.textTheme.labelMedium, + isYear ? 'YEAR' : 'MONTH', + style: theme.textTheme.labelSmall?.copyWith( + letterSpacing: 0.8, + fontWeight: FontWeight.w600, + color: colors.onSurfaceVariant, + ), ), - const SizedBox(height: 2), + const SizedBox(height: 4), Text( periodLabel, style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w700, + color: colors.onSurface, ), ), ], @@ -1482,7 +1483,7 @@ class _LabelHistoryCard extends StatelessWidget { formatMoney(-total, currency), style: theme.textTheme.titleSmall?.copyWith( color: _kExpenseColor, - fontWeight: FontWeight.w600, + fontWeight: FontWeight.w700, ), ), ], diff --git a/lib/screens/transaction_detail_screen.dart b/lib/screens/transaction_detail_screen.dart index 0c8ea30..7874082 100644 --- a/lib/screens/transaction_detail_screen.dart +++ b/lib/screens/transaction_detail_screen.dart @@ -8,6 +8,7 @@ import '../models/bank.dart'; import '../models/transaction_meta.dart'; import '../providers/bank_provider.dart'; import '../providers/tx_meta_provider.dart'; +import '../utils/app_theme.dart'; import '../utils/helpers.dart'; import '../widgets/label_parent_picker.dart'; import '../widgets/responsive_body.dart'; @@ -290,7 +291,7 @@ class _TransactionDetailScreenState extends State { ); final theme = Theme.of(context); final colors = theme.colorScheme; - final amountColor = tx.amount >= 0 ? colors.primary : colors.error; + final amountColor = tx.amount >= 0 ? AppTheme.accent : AppTheme.expense; final labelEntries = metaController.labelsInTreeOrder(); return Scaffold( diff --git a/lib/screens/transactions_screen.dart b/lib/screens/transactions_screen.dart index 5ceab37..3185c3a 100644 --- a/lib/screens/transactions_screen.dart +++ b/lib/screens/transactions_screen.dart @@ -10,6 +10,7 @@ import '../models/bank.dart'; import '../models/transaction_meta.dart'; import '../providers/bank_provider.dart'; import '../providers/tx_meta_provider.dart'; +import '../utils/app_theme.dart'; import '../utils/connect_bank.dart'; import '../utils/helpers.dart'; import '../utils/navigation.dart'; @@ -451,8 +452,14 @@ class _TransactionsScreenState extends State { ), style: IconButton.styleFrom( backgroundColor: _hasListFilters - ? colors.secondaryContainer - : null, + ? colors.primary.withValues(alpha: 0.22) + : colors.surfaceContainer, + foregroundColor: _hasListFilters + ? colors.primary + : colors.onSurfaceVariant, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), ), icon: Badge( isLabelVisible: _hasListFilters, @@ -476,8 +483,14 @@ class _TransactionsScreenState extends State { }, style: IconButton.styleFrom( backgroundColor: _showCalendar && _searchInMonthOnly - ? colors.secondaryContainer - : null, + ? colors.primary.withValues(alpha: 0.22) + : colors.surfaceContainer, + foregroundColor: _showCalendar && _searchInMonthOnly + ? colors.primary + : colors.onSurfaceVariant, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), ), icon: Icon( _showCalendar && _searchInMonthOnly @@ -580,7 +593,7 @@ class _TransactionsScreenState extends State { child: _TotalsCard( label: 'Money out', value: formatMoney(moneyOut, currency), - color: colors.error, + color: AppTheme.expense, icon: Icons.arrow_upward, ), ), @@ -589,7 +602,7 @@ class _TransactionsScreenState extends State { child: _TotalsCard( label: 'Money in', value: formatMoney(moneyIn, currency), - color: colors.primary, + color: AppTheme.accent, icon: Icons.arrow_downward, ), ), @@ -842,28 +855,31 @@ class _TotalsCard extends StatelessWidget { final theme = Theme.of(context); final colors = theme.colorScheme; - return Container( - padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), - decoration: BoxDecoration( - color: colors.surfaceContainerLow, - borderRadius: BorderRadius.circular(14), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon(icon, size: 16, color: color), - const SizedBox(width: 6), - Text(label, style: theme.textTheme.labelMedium), - ], - ), - const SizedBox(height: 6), - Text( - value, - style: theme.textTheme.titleMedium?.copyWith(color: color), - ), - ], + return Material( + color: colors.surfaceContainer, + borderRadius: BorderRadius.circular(16), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(icon, size: 16, color: color), + const SizedBox(width: 6), + Text(label, style: theme.textTheme.labelMedium), + ], + ), + const SizedBox(height: 6), + Text( + value, + style: theme.textTheme.titleMedium?.copyWith( + color: color, + fontWeight: FontWeight.w700, + ), + ), + ], + ), ), ); } @@ -884,7 +900,8 @@ class _CompactTransactionTile extends StatelessWidget { Widget build(BuildContext context) { final theme = Theme.of(context); final colors = theme.colorScheme; - final amountColor = tx.amount >= 0 ? colors.primary : colors.error; + final amountColor = + tx.amount >= 0 ? AppTheme.accent : AppTheme.expense; final primary = labels.isEmpty ? null : labels.first; final iconColor = primary?.color ?? colors.outline; final iconData = primary?.icon ?? Icons.receipt_long_outlined; diff --git a/lib/utils/app_theme.dart b/lib/utils/app_theme.dart index 2b8d926..829a6c0 100644 --- a/lib/utils/app_theme.dart +++ b/lib/utils/app_theme.dart @@ -1,46 +1,124 @@ -/// Material 3 light and dark themes, including typography and component styles. +/// Material 3 light and dark themes — charcoal surfaces, mint accent. library; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; /// Builds the [ThemeData] used by [MaterialApp] for light and dark mode. -/// -/// Colors are generated from a single seed so both schemes stay consistent -/// with Material 3 tonal palettes. abstract final class AppTheme { - static const Color seed = Color(0xFF0F766E); + /// Mint / emerald accent from the product look. + static const Color accent = Color(0xFF4FA894); + + /// Soft rose for expenses / money out. + static const Color expense = Color(0xFFE07A7A); + + /// Near-black charcoal scaffold (dark). + static const Color darkCanvas = Color(0xFF0B0D0C); + + static const Color darkSurface = Color(0xFF141716); + + static const Color darkContainer = Color(0xFF1E2321); + + static const Color darkContainerHigh = Color(0xFF2A302D); static ThemeData light() => _build(Brightness.light); static ThemeData dark() => _build(Brightness.dark); - static ThemeData _build(Brightness brightness) { - final colorScheme = ColorScheme.fromSeed( - seedColor: seed, - brightness: brightness, - ); + static ColorScheme _scheme(Brightness brightness) { + if (brightness == Brightness.dark) { + return const ColorScheme.dark( + primary: accent, + onPrimary: Color(0xFF06201A), + primaryContainer: Color(0xFF1F4F44), + onPrimaryContainer: Color(0xFFB8E6D8), + secondary: Color(0xFF8FB5AB), + onSecondary: Color(0xFF0B1F1A), + secondaryContainer: Color(0xFF2A403A), + onSecondaryContainer: Color(0xFFD0E8E0), + tertiary: expense, + onTertiary: Color(0xFF2A0F0F), + tertiaryContainer: Color(0xFF5C2E2E), + onTertiaryContainer: Color(0xFFFFDAD6), + error: expense, + onError: Color(0xFF2A0F0F), + errorContainer: Color(0xFF5C2E2E), + onErrorContainer: Color(0xFFFFDAD6), + surface: darkSurface, + onSurface: Color(0xFFE8EEEC), + onSurfaceVariant: Color(0xFFA3AFA9), + surfaceContainerLowest: darkCanvas, + surfaceContainerLow: Color(0xFF171B19), + surfaceContainer: darkContainer, + surfaceContainerHigh: darkContainerHigh, + surfaceContainerHighest: Color(0xFF343B38), + outline: Color(0xFF6B7671), + outlineVariant: Color(0xFF3A423E), + shadow: Colors.black, + scrim: Colors.black, + inverseSurface: Color(0xFFE8EEEC), + onInverseSurface: Color(0xFF1A1F1D), + inversePrimary: Color(0xFF0F766E), + ); + } + return ColorScheme.fromSeed( + seedColor: accent, + brightness: Brightness.light, + primary: const Color(0xFF1F6B5C), + error: const Color(0xFFC45C5C), + ).copyWith( + surface: const Color(0xFFF4F7F5), + surfaceContainerLowest: const Color(0xFFFFFFFF), + surfaceContainerLow: const Color(0xFFEEF3F0), + surfaceContainer: const Color(0xFFE6EDE9), + surfaceContainerHigh: const Color(0xFFDCE6E1), + surfaceContainerHighest: const Color(0xFFD0DCD6), + onSurface: const Color(0xFF15201C), + onSurfaceVariant: const Color(0xFF4A5A54), + outline: const Color(0xFF7A8A83), + outlineVariant: const Color(0xFFC5D0CB), + tertiary: expense, + error: expense, + ); + } + + static ThemeData _build(Brightness brightness) { + final colorScheme = _scheme(brightness); final isDark = brightness == Brightness.dark; final textTheme = _textTheme(colorScheme); + const radius = 14.0; + const radiusLg = 18.0; return ThemeData( useMaterial3: true, + brightness: brightness, colorScheme: colorScheme, textTheme: textTheme, - scaffoldBackgroundColor: colorScheme.surface, + scaffoldBackgroundColor: + isDark ? darkCanvas : colorScheme.surfaceContainerLowest, + canvasColor: isDark ? darkCanvas : colorScheme.surface, appBarTheme: AppBarTheme( centerTitle: false, elevation: 0, - scrolledUnderElevation: 1, - backgroundColor: colorScheme.surface, + scrolledUnderElevation: 0, + backgroundColor: isDark ? darkCanvas : colorScheme.surfaceContainerLowest, foregroundColor: colorScheme.onSurface, titleTextStyle: textTheme.titleLarge, + systemOverlayStyle: isDark + ? SystemUiOverlayStyle.light.copyWith( + statusBarColor: Colors.transparent, + systemNavigationBarColor: darkCanvas, + ) + : SystemUiOverlayStyle.dark.copyWith( + statusBarColor: Colors.transparent, + ), ), cardTheme: CardThemeData( elevation: 0, - color: colorScheme.surfaceContainerLow, + color: colorScheme.surfaceContainer, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), + borderRadius: BorderRadius.circular(radiusLg), ), clipBehavior: Clip.antiAlias, margin: EdgeInsets.zero, @@ -48,124 +126,264 @@ abstract final class AppTheme { navigationBarTheme: NavigationBarThemeData( elevation: 0, height: 72, - backgroundColor: colorScheme.surfaceContainer, - indicatorColor: colorScheme.secondaryContainer, + backgroundColor: isDark ? darkSurface : colorScheme.surfaceContainer, + indicatorColor: colorScheme.primary.withValues(alpha: 0.22), labelTextStyle: WidgetStateProperty.resolveWith((states) { final selected = states.contains(WidgetState.selected); return textTheme.labelMedium?.copyWith( fontWeight: selected ? FontWeight.w600 : FontWeight.w500, + color: selected ? colorScheme.primary : colorScheme.onSurfaceVariant, + ); + }), + iconTheme: WidgetStateProperty.resolveWith((states) { + final selected = states.contains(WidgetState.selected); + return IconThemeData( + color: selected ? colorScheme.primary : colorScheme.onSurfaceVariant, ); }), ), inputDecorationTheme: InputDecorationTheme( filled: true, - fillColor: colorScheme.surfaceContainerHighest.withValues(alpha: 0.6), + fillColor: colorScheme.surfaceContainerHigh, border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(radius), + borderSide: BorderSide.none, ), enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide(color: colorScheme.outlineVariant), + borderRadius: BorderRadius.circular(radius), + borderSide: BorderSide( + color: colorScheme.outlineVariant.withValues(alpha: 0.6), + ), ), focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - borderSide: BorderSide(color: colorScheme.primary, width: 2), + borderRadius: BorderRadius.circular(radius), + borderSide: BorderSide(color: colorScheme.primary, width: 1.5), ), contentPadding: const EdgeInsets.symmetric( horizontal: 16, vertical: 14, ), + hintStyle: textTheme.bodyMedium?.copyWith( + color: colorScheme.onSurfaceVariant.withValues(alpha: 0.75), + ), ), filledButtonTheme: FilledButtonThemeData( style: FilledButton.styleFrom( minimumSize: const Size.fromHeight(48), + backgroundColor: colorScheme.primary, + foregroundColor: colorScheme.onPrimary, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(radius), + ), + textStyle: textTheme.labelLarge?.copyWith(fontWeight: FontWeight.w700), + ), + ), + textButtonTheme: TextButtonThemeData( + style: TextButton.styleFrom( + foregroundColor: colorScheme.primary, + textStyle: textTheme.labelLarge, + ), + ), + outlinedButtonTheme: OutlinedButtonThemeData( + style: OutlinedButton.styleFrom( + foregroundColor: colorScheme.onSurface, + side: BorderSide(color: colorScheme.outlineVariant), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(radius), + ), + minimumSize: const Size(48, 44), + ), + ), + iconButtonTheme: IconButtonThemeData( + style: IconButton.styleFrom( + foregroundColor: colorScheme.onSurfaceVariant, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), - textStyle: textTheme.labelLarge, ), ), floatingActionButtonTheme: FloatingActionButtonThemeData( backgroundColor: colorScheme.primary, foregroundColor: colorScheme.onPrimary, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), + borderRadius: BorderRadius.circular(radiusLg), ), ), dividerTheme: DividerThemeData( - color: colorScheme.outlineVariant, + color: colorScheme.outlineVariant.withValues(alpha: 0.55), space: 1, + thickness: 1, ), snackBarTheme: SnackBarThemeData( behavior: SnackBarBehavior.floating, + backgroundColor: colorScheme.inverseSurface, + contentTextStyle: textTheme.bodyMedium?.copyWith( + color: colorScheme.onInverseSurface, + ), shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(radius), ), ), listTileTheme: ListTileThemeData( shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), + borderRadius: BorderRadius.circular(radius), ), iconColor: colorScheme.primary, + textColor: colorScheme.onSurface, + ), + chipTheme: ChipThemeData( + backgroundColor: colorScheme.surfaceContainerHigh, + selectedColor: colorScheme.primary.withValues(alpha: 0.28), + deleteIconColor: colorScheme.onSurfaceVariant, + labelStyle: textTheme.labelLarge?.copyWith(fontWeight: FontWeight.w600), + secondaryLabelStyle: textTheme.labelMedium, + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + side: BorderSide( + color: colorScheme.outlineVariant.withValues(alpha: 0.5), + ), + ), + side: BorderSide( + color: colorScheme.outlineVariant.withValues(alpha: 0.5), + ), + ), + dialogTheme: DialogThemeData( + backgroundColor: colorScheme.surfaceContainer, + surfaceTintColor: Colors.transparent, + elevation: 8, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(24), + ), + titleTextStyle: textTheme.titleLarge, + contentTextStyle: textTheme.bodyMedium, + ), + bottomSheetTheme: BottomSheetThemeData( + backgroundColor: colorScheme.surfaceContainer, + surfaceTintColor: Colors.transparent, + elevation: 8, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(24)), + ), + dragHandleColor: colorScheme.outlineVariant, + showDragHandle: true, + ), + // Avoid textStyle here — SegmentedButton lerps selected/unselected styles + // and crashes if inherit flags disagree. + segmentedButtonTheme: SegmentedButtonThemeData( + style: ButtonStyle( + backgroundColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.selected)) { + return colorScheme.primary; + } + return colorScheme.surfaceContainerHigh; + }), + foregroundColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.selected)) { + return colorScheme.onPrimary; + } + return colorScheme.onSurfaceVariant; + }), + side: WidgetStatePropertyAll( + BorderSide(color: colorScheme.outlineVariant.withValues(alpha: 0.7)), + ), + shape: WidgetStatePropertyAll( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + visualDensity: VisualDensity.compact, + ), + ), + sliderTheme: SliderThemeData( + activeTrackColor: colorScheme.primary, + inactiveTrackColor: colorScheme.outlineVariant.withValues(alpha: 0.5), + thumbColor: colorScheme.primary, + overlayColor: colorScheme.primary.withValues(alpha: 0.16), + rangeThumbShape: const RoundRangeSliderThumbShape(enabledThumbRadius: 8), + trackHeight: 4, + ), + checkboxTheme: CheckboxThemeData( + fillColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.selected)) { + return colorScheme.primary; + } + return Colors.transparent; + }), + checkColor: WidgetStatePropertyAll(colorScheme.onPrimary), + side: BorderSide(color: colorScheme.outline, width: 1.5), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)), + ), + progressIndicatorTheme: ProgressIndicatorThemeData( + color: colorScheme.primary, + linearTrackColor: colorScheme.outlineVariant.withValues(alpha: 0.35), ), shadowColor: isDark ? Colors.black : const Color(0x1A0F172A), ); } - /// Slightly tighter, more editorial type scale than the Material defaults. + /// Built from Material 2021 typography so every style shares `inherit: false` + /// and theme animations / SegmentedButton can lerp safely. static TextTheme _textTheme(ColorScheme colors) { - const family = 'Roboto'; - return TextTheme( - displaySmall: TextStyle( - fontFamily: family, + final base = Typography.material2021().englishLike.apply( + bodyColor: colors.onSurface, + displayColor: colors.onSurface, + ); + + return base.copyWith( + displaySmall: base.displaySmall?.copyWith( fontSize: 36, fontWeight: FontWeight.w700, - letterSpacing: -0.5, - color: colors.onSurface, + letterSpacing: -0.6, ), - headlineMedium: TextStyle( - fontFamily: family, + headlineMedium: base.headlineMedium?.copyWith( fontSize: 28, fontWeight: FontWeight.w600, - letterSpacing: -0.3, - color: colors.onSurface, + letterSpacing: -0.4, ), - titleLarge: TextStyle( - fontFamily: family, + titleLarge: base.titleLarge?.copyWith( fontSize: 22, fontWeight: FontWeight.w600, - color: colors.onSurface, + letterSpacing: -0.2, ), - titleMedium: TextStyle( - fontFamily: family, + titleMedium: base.titleMedium?.copyWith( fontSize: 16, fontWeight: FontWeight.w600, - color: colors.onSurface, ), - bodyLarge: TextStyle( - fontFamily: family, + titleSmall: base.titleSmall?.copyWith( + fontSize: 14, + fontWeight: FontWeight.w600, + ), + bodyLarge: base.bodyLarge?.copyWith( fontSize: 16, - height: 1.45, - color: colors.onSurface, + height: 1.4, + fontWeight: FontWeight.w500, ), - bodyMedium: TextStyle( - fontFamily: family, + bodyMedium: base.bodyMedium?.copyWith( fontSize: 14, height: 1.4, color: colors.onSurfaceVariant, ), - labelLarge: TextStyle( - fontFamily: family, + bodySmall: base.bodySmall?.copyWith( + fontSize: 12, + height: 1.35, + color: colors.onSurfaceVariant, + ), + labelLarge: base.labelLarge?.copyWith( fontSize: 14, fontWeight: FontWeight.w600, letterSpacing: 0.1, - color: colors.onPrimary, ), - labelMedium: TextStyle( - fontFamily: family, + labelMedium: base.labelMedium?.copyWith( fontSize: 12, fontWeight: FontWeight.w500, - letterSpacing: 0.4, + letterSpacing: 0.2, + color: colors.onSurfaceVariant, + ), + labelSmall: base.labelSmall?.copyWith( + fontSize: 11, + fontWeight: FontWeight.w500, + letterSpacing: 0.3, color: colors.onSurfaceVariant, ), ); diff --git a/lib/widgets/label_filter_dialog.dart b/lib/widgets/label_filter_dialog.dart index daac06f..cf8d5a9 100644 --- a/lib/widgets/label_filter_dialog.dart +++ b/lib/widgets/label_filter_dialog.dart @@ -85,14 +85,22 @@ Future showTransactionFilterDialog( required TransactionFilterSelection initial, DateTime? currentMonth, }) { - return showDialog( + return showModalBottomSheet( context: context, + isScrollControlled: true, + useSafeArea: true, + showDragHandle: true, builder: (dialogContext) { - return _TransactionFilterDialog( - available: available, - transactions: transactions, - initial: initial, - currentMonth: currentMonth ?? monthStart(DateTime.now()), + return Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.viewInsetsOf(dialogContext).bottom, + ), + child: _TransactionFilterDialog( + available: available, + transactions: transactions, + initial: initial, + currentMonth: currentMonth ?? monthStart(DateTime.now()), + ), ); }, ); @@ -476,8 +484,6 @@ class _TransactionFilterDialogState extends State<_TransactionFilterDialog> { final colors = theme.colorScheme; final meta = context.watch(); final size = MediaQuery.sizeOf(context); - final dialogWidth = - (size.width - 48).clamp(280.0, 560.0).toDouble(); final needle = _labelSearchController.text.trim().toLowerCase(); final entries = _filterLabelEntries( meta: meta, @@ -488,246 +494,285 @@ class _TransactionFilterDialogState extends State<_TransactionFilterDialog> { kUncategorizedFilterLabel.name.toLowerCase().contains(needle); final sectionStyle = _sectionStyle(theme, colors); - return AlertDialog( - title: const Text('Filter transactions'), - contentPadding: const EdgeInsets.fromLTRB(24, 8, 24, 0), - content: SizedBox( - width: dialogWidth, - child: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text( - 'Search, period, flow, amount, and labels.', - style: theme.textTheme.bodyMedium?.copyWith( - color: colors.onSurfaceVariant, - ), - ), - const SizedBox(height: 14), - Text('Search text', style: sectionStyle), - const SizedBox(height: 8), - TextField( - controller: _queryController, - textInputAction: TextInputAction.search, - decoration: const InputDecoration( - hintText: 'Description, notes, amount…', - prefixIcon: Icon(Icons.search), - ), - ), - const SizedBox(height: 14), - Text('Period', style: sectionStyle), - const SizedBox(height: 8), - _RectToggleBar( - value: _monthOnly, - onChanged: (value) => setState(() => _monthOnly = value), - options: const [ - _RectToggleOption( - value: false, - label: 'All', - ), - _RectToggleOption( - value: true, - label: 'Month', - icon: Icons.calendar_month, - ), - ], - ), - if (_monthOnly) ...[ - const SizedBox(height: 8), + // Fixed sheet height so Expanded labels get a real viewport (not a tiny 220px box). + final sheetHeight = (size.height * 0.92).clamp(420.0, size.height); + + return SizedBox( + height: sheetHeight, + width: double.infinity, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(24, 4, 24, 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ Text( - 'Uses ${formatMonthYear(widget.currentMonth)}. ' - 'Change month on the transactions list.', - style: theme.textTheme.bodySmall?.copyWith( + 'Filter transactions', + style: theme.textTheme.titleLarge, + ), + const SizedBox(height: 4), + Text( + 'Search, period, flow, amount, and labels.', + style: theme.textTheme.bodyMedium?.copyWith( color: colors.onSurfaceVariant, ), ), ], - const SizedBox(height: 14), - Text('Income / expense', style: sectionStyle), - const SizedBox(height: 8), - _RectToggleBar( - value: _flow, - onChanged: (value) => setState(() => _flow = value), - options: const [ - _RectToggleOption( - value: TxFlowFilter.all, - label: 'All', - ), - _RectToggleOption( - value: TxFlowFilter.income, - label: 'In', - icon: Icons.south_west, - ), - _RectToggleOption( - value: TxFlowFilter.expense, - label: 'Out', - icon: Icons.north_east, - ), - ], - ), - const SizedBox(height: 14), - Text('Amount', style: sectionStyle), - const SizedBox(height: 8), - Text( - 'Exact', - style: theme.textTheme.bodySmall?.copyWith( - color: colors.onSurfaceVariant, - fontWeight: FontWeight.w600, - ), - ), - const SizedBox(height: 6), - TextField( - controller: _exactAmountController, - keyboardType: const TextInputType.numberWithOptions( - decimal: true, - ), - decoration: InputDecoration( - isDense: true, - hintText: 'e.g. 12.50', - prefixIcon: const Icon(Icons.pin, size: 20), - suffixIcon: _exactAmountController.text.trim().isEmpty - ? null - : IconButton( - tooltip: 'Clear exact', - onPressed: () { - _exactAmountController.clear(); - setState(() {}); - }, - icon: const Icon(Icons.clear, size: 18), - ), - ), - onChanged: (_) { - setState(() { - // Exact takes precedence; clear range filter intent. - _rangeTouched = false; - _amountRange = RangeValues(0, _amountCeil); - }); - }, - ), - const SizedBox(height: 12), - Text( - 'Range', - style: theme.textTheme.bodySmall?.copyWith( - color: colors.onSurfaceVariant, - fontWeight: FontWeight.w600, - ), - ), - const SizedBox(height: 4), - Text( - _rangeTouched - ? '${_amountRange.start.toStringAsFixed(0)} – ${_amountRange.end.toStringAsFixed(0)}' - : 'Any amount', - style: theme.textTheme.bodySmall?.copyWith( - color: colors.onSurfaceVariant, - ), - ), - RangeSlider( - values: _amountRange, - min: 0, - max: _amountCeil, - labels: RangeLabels( - _amountRange.start.toStringAsFixed(0), - _amountRange.end.toStringAsFixed(0), - ), - onChanged: (values) { - setState(() { - _exactAmountController.clear(); - _rangeTouched = true; - _amountRange = values; - }); - }, - ), - Align( - alignment: Alignment.centerRight, - child: TextButton( - onPressed: !_rangeTouched - ? null - : () { - setState(() { - _rangeTouched = false; - _amountRange = RangeValues(0, _amountCeil); - }); - }, - child: const Text('Reset range'), - ), - ), - const SizedBox(height: 8), - Text('Labels', style: sectionStyle), - const SizedBox(height: 8), - TextField( - controller: _labelSearchController, - decoration: InputDecoration( - isDense: true, - hintText: 'Filter labels', - prefixIcon: const Icon(Icons.search, size: 20), - suffixIcon: needle.isEmpty - ? null - : IconButton( - onPressed: () { - _labelSearchController.clear(); - setState(() {}); - }, - icon: const Icon(Icons.clear, size: 18), - ), - ), - onChanged: (_) => setState(() {}), - ), - const SizedBox(height: 8), - SizedBox( - height: 220, - width: dialogWidth, - child: _LabelChecklist( - entries: entries, - showUncategorized: showUncategorized, - selected: _selected, - onToggle: _toggle, - allowMove: false, - ), - ), - ], + ), ), - ), + const SizedBox(height: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Flexible( + flex: 2, + child: SingleChildScrollView( + padding: const EdgeInsets.fromLTRB(24, 4, 24, 8), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text('Search text', style: sectionStyle), + const SizedBox(height: 8), + TextField( + controller: _queryController, + textInputAction: TextInputAction.search, + decoration: const InputDecoration( + hintText: 'Description, notes, amount…', + prefixIcon: Icon(Icons.search), + ), + ), + const SizedBox(height: 14), + Text('Period', style: sectionStyle), + const SizedBox(height: 8), + _RectToggleBar( + value: _monthOnly, + onChanged: (value) => + setState(() => _monthOnly = value), + options: const [ + _RectToggleOption( + value: false, + label: 'All', + ), + _RectToggleOption( + value: true, + label: 'Month', + icon: Icons.calendar_month, + ), + ], + ), + if (_monthOnly) ...[ + const SizedBox(height: 8), + Text( + 'Uses ${formatMonthYear(widget.currentMonth)}. ' + 'Change month on the transactions list.', + style: theme.textTheme.bodySmall?.copyWith( + color: colors.onSurfaceVariant, + ), + ), + ], + const SizedBox(height: 14), + Text('Income / expense', style: sectionStyle), + const SizedBox(height: 8), + _RectToggleBar( + value: _flow, + onChanged: (value) => setState(() => _flow = value), + options: const [ + _RectToggleOption( + value: TxFlowFilter.all, + label: 'All', + ), + _RectToggleOption( + value: TxFlowFilter.income, + label: 'In', + icon: Icons.south_west, + ), + _RectToggleOption( + value: TxFlowFilter.expense, + label: 'Out', + icon: Icons.north_east, + ), + ], + ), + const SizedBox(height: 14), + Text('Amount', style: sectionStyle), + const SizedBox(height: 8), + Text( + 'Exact', + style: theme.textTheme.bodySmall?.copyWith( + color: colors.onSurfaceVariant, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 6), + TextField( + controller: _exactAmountController, + keyboardType: const TextInputType.numberWithOptions( + decimal: true, + ), + decoration: InputDecoration( + isDense: true, + hintText: 'e.g. 12.50', + prefixIcon: const Icon(Icons.pin, size: 20), + suffixIcon: + _exactAmountController.text.trim().isEmpty + ? null + : IconButton( + tooltip: 'Clear exact', + onPressed: () { + _exactAmountController.clear(); + setState(() {}); + }, + icon: const Icon(Icons.clear, size: 18), + ), + ), + onChanged: (_) { + setState(() { + _rangeTouched = false; + _amountRange = RangeValues(0, _amountCeil); + }); + }, + ), + const SizedBox(height: 12), + Text( + 'Range', + style: theme.textTheme.bodySmall?.copyWith( + color: colors.onSurfaceVariant, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 4), + Text( + _rangeTouched + ? '${_amountRange.start.toStringAsFixed(0)} – ${_amountRange.end.toStringAsFixed(0)}' + : 'Any amount', + style: theme.textTheme.bodySmall?.copyWith( + color: colors.onSurfaceVariant, + ), + ), + RangeSlider( + values: _amountRange, + min: 0, + max: _amountCeil, + labels: RangeLabels( + _amountRange.start.toStringAsFixed(0), + _amountRange.end.toStringAsFixed(0), + ), + onChanged: (values) { + setState(() { + _exactAmountController.clear(); + _rangeTouched = true; + _amountRange = values; + }); + }, + ), + Align( + alignment: Alignment.centerRight, + child: TextButton( + onPressed: !_rangeTouched + ? null + : () { + setState(() { + _rangeTouched = false; + _amountRange = + RangeValues(0, _amountCeil); + }); + }, + child: const Text('Reset range'), + ), + ), + ], + ), + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(24, 4, 24, 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text('Labels', style: sectionStyle), + const SizedBox(height: 8), + TextField( + controller: _labelSearchController, + decoration: InputDecoration( + isDense: true, + hintText: 'Filter labels', + prefixIcon: const Icon(Icons.search, size: 20), + suffixIcon: needle.isEmpty + ? null + : IconButton( + onPressed: () { + _labelSearchController.clear(); + setState(() {}); + }, + icon: const Icon(Icons.clear, size: 18), + ), + ), + onChanged: (_) => setState(() {}), + ), + Row( + children: [ + TextButton( + onPressed: () { + setState(() { + _selected + ..clear() + ..addAll(entries.map((e) => e.label.id)); + if (showUncategorized) { + _selected.add(kUncategorizedFilterId); + } + }); + }, + child: const Text('All labels'), + ), + TextButton( + onPressed: () => + setState(() => _selected.clear()), + child: const Text('No labels'), + ), + ], + ), + ], + ), + ), + Expanded( + flex: 3, + child: Padding( + padding: const EdgeInsets.fromLTRB(24, 0, 24, 8), + child: _LabelChecklist( + entries: entries, + showUncategorized: showUncategorized, + selected: _selected, + onToggle: _toggle, + allowMove: false, + ), + ), + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(24, 4, 24, 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + FilledButton( + onPressed: _apply, + child: const Text('Apply'), + ), + const SizedBox(height: 4), + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Cancel'), + ), + ], + ), + ), + ], ), - actionsAlignment: MainAxisAlignment.spaceBetween, - actionsPadding: const EdgeInsets.fromLTRB(16, 8, 16, 16), - actions: [ - Wrap( - spacing: 4, - children: [ - TextButton( - onPressed: () { - setState(() { - _selected - ..clear() - ..addAll(entries.map((e) => e.label.id)); - if (showUncategorized) { - _selected.add(kUncategorizedFilterId); - } - }); - }, - child: const Text('All labels'), - ), - TextButton( - onPressed: () => setState(() => _selected.clear()), - child: const Text('No labels'), - ), - ], - ), - Wrap( - spacing: 8, - children: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text('Cancel'), - ), - FilledButton( - onPressed: _apply, - child: const Text('Apply'), - ), - ], - ), - ], ); } } @@ -879,65 +924,80 @@ class _RectToggleBar extends StatelessWidget { return SizedBox( width: double.infinity, child: DecoratedBox( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - border: Border.all(color: colors.outlineVariant), - ), - child: Row( - children: [ - for (var i = 0; i < options.length; i++) ...[ - if (i > 0) - Container( - width: 1, - height: 36, - color: colors.outlineVariant, - ), - Expanded( - child: Material( - color: options[i].value == value - ? colors.secondaryContainer - : colors.surface, - child: InkWell( - onTap: () => onChanged(options[i].value), - child: SizedBox( - height: 36, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (options[i].icon != null) ...[ - Icon( - options[i].icon, - size: 15, - color: options[i].value == value - ? colors.onSecondaryContainer - : colors.onSurfaceVariant, - ), - const SizedBox(width: 4), - ], - Flexible( - child: Text( - options[i].label, - overflow: TextOverflow.ellipsis, - style: theme.textTheme.labelLarge?.copyWith( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: colors.surfaceContainerHigh, + border: Border.all( + color: colors.outlineVariant.withValues(alpha: 0.7), + ), + ), + child: Row( + children: [ + for (var i = 0; i < options.length; i++) ...[ + if (i > 0) + Container( + width: 1, + height: 36, + color: colors.outlineVariant.withValues(alpha: 0.7), + ), + Expanded( + child: Material( + color: options[i].value == value + ? colors.primary + : Colors.transparent, + borderRadius: BorderRadius.horizontal( + left: i == 0 ? const Radius.circular(11) : Radius.zero, + right: i == options.length - 1 + ? const Radius.circular(11) + : Radius.zero, + ), + child: InkWell( + onTap: () => onChanged(options[i].value), + borderRadius: BorderRadius.horizontal( + left: i == 0 ? const Radius.circular(11) : Radius.zero, + right: i == options.length - 1 + ? const Radius.circular(11) + : Radius.zero, + ), + child: SizedBox( + height: 36, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (options[i].icon != null) ...[ + Icon( + options[i].icon, + size: 15, color: options[i].value == value - ? colors.onSecondaryContainer - : colors.onSurface, - fontWeight: options[i].value == value - ? FontWeight.w600 - : FontWeight.w500, + ? colors.onPrimary + : colors.onSurfaceVariant, + ), + const SizedBox(width: 4), + ], + Flexible( + child: Text( + options[i].label, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.labelLarge?.copyWith( + color: options[i].value == value + ? colors.onPrimary + : colors.onSurface, + fontWeight: options[i].value == value + ? FontWeight.w700 + : FontWeight.w500, + ), ), ), - ), - ], + ], + ), ), ), ), ), - ), + ], ], - ], + ), ), - ), ); } } diff --git a/lib/widgets/similar_match_dialog.dart b/lib/widgets/similar_match_dialog.dart index 4277903..ec9edee 100644 --- a/lib/widgets/similar_match_dialog.dart +++ b/lib/widgets/similar_match_dialog.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import '../models/bank.dart'; import '../models/transaction_meta.dart'; +import '../utils/app_theme.dart'; import '../utils/helpers.dart'; class _RegexExample { @@ -256,8 +257,8 @@ class _SimilarMatchDialogState extends State<_SimilarMatchDialog> { formatMoney(tx.amount, tx.currency), style: theme.textTheme.bodyMedium?.copyWith( color: tx.amount >= 0 - ? colors.primary - : colors.error, + ? AppTheme.accent + : AppTheme.expense, fontWeight: isPrototype ? FontWeight.w700 : null, ),