better UI

This commit is contained in:
davmar
2026-09-09 14:38:17 +02:00
parent 903a2980de
commit ae464ff304
9 changed files with 771 additions and 471 deletions
+2 -1
View File
@@ -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: () {
+3 -3
View File
@@ -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: () {
+1
View File
@@ -31,6 +31,7 @@ class _MainShellState extends State<MainShell> {
bottomNavigationBar: NavigationBar(
selectedIndex: _index,
onDestinationSelected: (index) => setState(() => _index = index),
labelBehavior: NavigationDestinationLabelBehavior.alwaysShow,
destinations: const [
NavigationDestination(
icon: Icon(Icons.home_outlined),
+88 -87
View File
@@ -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<OverviewScreen> {
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<OverviewScreen> {
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<OverviewScreen> {
),
],
),
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<double>(0, (s, e) => s + e.amount),
currency,
@@ -324,12 +317,14 @@ class _OverviewScreenState extends State<OverviewScreen> {
},
),
],
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<double>(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,
),
),
],
+2 -1
View File
@@ -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<TransactionDetailScreen> {
);
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(
+46 -29
View File
@@ -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<TransactionsScreen> {
),
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<TransactionsScreen> {
},
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<TransactionsScreen> {
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<TransactionsScreen> {
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;