This commit is contained in:
2026-06-24 00:47:07 +03:00
parent 3961327bdb
commit 83fd8bdbf1
3 changed files with 238 additions and 593 deletions
+151 -507
View File
@@ -1,7 +1,6 @@
import 'package:fl_chart/fl_chart.dart'; import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart';
import '../../core/constants.dart'; import '../../core/constants.dart';
import '../../core/l10n/app_strings.dart'; import '../../core/l10n/app_strings.dart';
import '../../core/l10n/locale_provider.dart'; import '../../core/l10n/locale_provider.dart';
@@ -10,13 +9,9 @@ import '../../shared/providers/amount_format_provider.dart';
import '../../shared/utils/currency_utils.dart'; import '../../shared/utils/currency_utils.dart';
import '../../shared/widgets/byn_sign.dart'; import '../../shared/widgets/byn_sign.dart';
import '../dashboard/provider.dart'; import '../dashboard/provider.dart';
import '../dashboard/widgets/summary_row.dart';
import '../settings/provider.dart'; import '../settings/provider.dart';
import 'provider.dart'; import 'provider.dart';
import 'widgets/account_scope_chips.dart'; import 'widgets/account_scope_chips.dart';
import 'widgets/stats_hero_card.dart';
enum ChartType { pie, bar }
class CategoriesScreen extends ConsumerStatefulWidget { class CategoriesScreen extends ConsumerStatefulWidget {
const CategoriesScreen({super.key}); const CategoriesScreen({super.key});
@@ -27,30 +22,17 @@ class CategoriesScreen extends ConsumerStatefulWidget {
class _CategoriesScreenState extends ConsumerState<CategoriesScreen> { class _CategoriesScreenState extends ConsumerState<CategoriesScreen> {
int _touchedIndex = -1; int _touchedIndex = -1;
ChartType _chartType = ChartType.pie;
bool _showIncome = false; bool _showIncome = false;
String _scopeLabel(AppStrings s) {
final activeAccount = ref.watch(activeAccountProvider);
if (activeAccount == null) return s.allAccounts;
return activeAccount.name;
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final s = ref.watch(stringsProvider); final s = ref.watch(stringsProvider);
final data = _showIncome final data = _showIncome
? ref.watch(categoryIncomeProvider) ? ref.watch(categoryIncomeProvider)
: ref.watch(categoryExpenseProvider); : ref.watch(categoryExpenseProvider);
final monthlyData = _showIncome
? ref.watch(monthlyIncomeBreakdownProvider)
: ref.watch(monthlyBreakdownProvider);
final total = data.values.fold(0.0, (a, b) => a + b); final total = data.values.fold(0.0, (a, b) => a + b);
final currencyInfo = ref.watch(statsCurrencyProvider); final currencyInfo = ref.watch(statsCurrencyProvider);
final income = ref.watch(statsIncomeTotalProvider);
final expense = ref.watch(statsExpenseTotalProvider);
final timeFilter = ref.watch(timeFilterProvider); final timeFilter = ref.watch(timeFilterProvider);
final scopeLabel = _scopeLabel(s);
final sortedEntries = data.entries.toList() final sortedEntries = data.entries.toList()
..sort((a, b) => b.value.compareTo(a.value)); ..sort((a, b) => b.value.compareTo(a.value));
@@ -65,13 +47,6 @@ class _CategoriesScreenState extends ConsumerState<CategoriesScreen> {
color: Theme.of(context).colorScheme.onSurface, color: Theme.of(context).colorScheme.onSurface,
), ),
), ),
actions: [
_ChartToggle(
selected: _chartType,
onChanged: (t) => setState(() => _chartType = t),
),
const SizedBox(width: 8),
],
), ),
body: SafeArea( body: SafeArea(
child: Padding( child: Padding(
@@ -80,81 +55,80 @@ class _CategoriesScreenState extends ConsumerState<CategoriesScreen> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const AccountScopeChips(), const AccountScopeChips(),
const SizedBox(height: 12), const SizedBox(height: 16),
StatsHeroCard(
amount: total,
label: _showIncome ? s.income : s.expenses,
accentColor: _showIncome ? AppColors.income : AppColors.expense,
scopeLabel: scopeLabel,
),
const SizedBox(height: 12),
SummaryRow(
income: income,
expense: expense,
currencyInfo: currencyInfo,
strings: s,
),
const SizedBox(height: 12),
_StatsTimeFilter(
strings: s,
timeFilter: timeFilter,
onAllTime: () =>
ref.read(timeFilterProvider.notifier).set(TimeFilter.allTime),
onMonth: () => ref
.read(timeFilterProvider.notifier)
.set(TimeFilter.lastMonth),
),
const SizedBox(height: 12),
Row( Row(
children: [ children: [
Expanded( Expanded(
child: _TypeToggle( child: _TimeFilterChip(
label: s.expenses, label: s.filterAllTime,
isSelected: !_showIncome, isSelected: timeFilter == TimeFilter.allTime,
color: AppColors.expense, onTap: () {
onTap: () => setState(() { HapticService.selection();
_showIncome = false; ref.read(timeFilterProvider.notifier).set(TimeFilter.allTime);
_touchedIndex = -1; },
}),
), ),
), ),
const SizedBox(width: 12), const SizedBox(width: 8),
Expanded( Expanded(
child: _TypeToggle( child: _TimeFilterChip(
label: s.income, label: s.filterMonth,
isSelected: _showIncome, isSelected: timeFilter == TimeFilter.lastMonth,
color: AppColors.income, onTap: () {
onTap: () => setState(() { HapticService.selection();
_showIncome = true; ref.read(timeFilterProvider.notifier).set(TimeFilter.lastMonth);
_touchedIndex = -1; },
}),
), ),
), ),
], ],
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Row(
children: [
Expanded(
child: _TypeSegment(
label: s.expenses,
isSelected: !_showIncome,
onTap: () {
HapticService.selection();
setState(() {
_showIncome = false;
_touchedIndex = -1;
});
},
),
),
const SizedBox(width: 8),
Expanded(
child: _TypeSegment(
label: s.income,
isSelected: _showIncome,
onTap: () {
HapticService.selection();
setState(() {
_showIncome = true;
_touchedIndex = -1;
});
},
),
),
],
),
const SizedBox(height: 20),
if (data.isEmpty) if (data.isEmpty)
Expanded(child: _EmptyState(isIncome: _showIncome)) Expanded(child: _EmptyState(isIncome: _showIncome))
else else
Expanded( Expanded(
child: ListView( child: ListView(
children: [ children: [
if (_chartType == ChartType.pie) _PieChartSection(
_PieChartCard(
data: data, data: data,
total: total, total: total,
touchedIndex: _touchedIndex, touchedIndex: _touchedIndex,
onTouch: (i) => setState(() => _touchedIndex = i), onTouch: (i) => setState(() => _touchedIndex = i),
currencyInfo: currencyInfo, currencyInfo: currencyInfo,
isIncome: _showIncome, isIncome: _showIncome,
)
else
_BarChartCard(
monthlyData: monthlyData,
currencyInfo: currencyInfo,
isIncome: _showIncome,
), ),
const SizedBox(height: 20), const SizedBox(height: 24),
Text( Text(
s.rankedByAmount, s.rankedByAmount,
style: Theme.of(context).textTheme.titleMedium style: Theme.of(context).textTheme.titleMedium
@@ -163,15 +137,13 @@ class _CategoriesScreenState extends ConsumerState<CategoriesScreen> {
color: Theme.of(context).colorScheme.onSurface, color: Theme.of(context).colorScheme.onSurface,
), ),
), ),
const SizedBox(height: 12), const SizedBox(height: 16),
...sortedEntries.asMap().entries.map((entry) { ...sortedEntries.map((entry) {
final rank = entry.key + 1; final cat = entry.key;
final cat = entry.value.key; final amount = entry.value;
final amount = entry.value.value;
return Padding( return Padding(
padding: const EdgeInsets.only(bottom: 10), padding: const EdgeInsets.only(bottom: 8),
child: _CategoryRow( child: _CategoryItem(
rank: rank,
category: cat, category: cat,
amount: amount, amount: amount,
total: total, total: total,
@@ -192,105 +164,14 @@ class _CategoriesScreenState extends ConsumerState<CategoriesScreen> {
} }
} }
class _StatsTimeFilter extends StatelessWidget { class _TimeFilterChip extends StatelessWidget {
final AppStrings strings;
final TimeFilter timeFilter;
final VoidCallback onAllTime;
final VoidCallback onMonth;
const _StatsTimeFilter({
required this.strings,
required this.timeFilter,
required this.onAllTime,
required this.onMonth,
});
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return Row(
children: [
_TimeChip(
label: strings.filterAllTime,
isSelected: timeFilter == TimeFilter.allTime,
isDark: isDark,
onTap: () {
HapticService.selection();
onAllTime();
},
),
const SizedBox(width: 6),
_TimeChip(
label: strings.filterMonth,
isSelected: timeFilter == TimeFilter.lastMonth,
isDark: isDark,
onTap: () {
HapticService.selection();
onMonth();
},
),
],
);
}
}
class _TimeChip extends StatelessWidget {
final String label; final String label;
final bool isSelected; final bool isSelected;
final bool isDark;
final VoidCallback onTap; final VoidCallback onTap;
const _TimeChip({ const _TimeFilterChip({
required this.label, required this.label,
required this.isSelected, required this.isSelected,
required this.isDark,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: isSelected
? AppColors.accent.withOpacity(0.2)
: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(20),
border: isSelected
? Border.all(color: AppColors.accent, width: 1.5)
: isDark
? null
: Border.all(color: const Color(0xFFDDDDEE), width: 1),
),
child: Text(
label,
style: TextStyle(
fontSize: 12,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
color: isSelected
? AppColors.accent
: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
),
),
),
);
}
}
class _TypeToggle extends StatelessWidget {
final String label;
final bool isSelected;
final Color color;
final VoidCallback onTap;
const _TypeToggle({
required this.label,
required this.isSelected,
required this.color,
required this.onTap, required this.onTap,
}); });
@@ -299,22 +180,18 @@ class _TypeToggle extends StatelessWidget {
return GestureDetector( return GestureDetector(
onTap: onTap, onTap: onTap,
child: Container( child: Container(
padding: const EdgeInsets.symmetric(vertical: 10), padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isSelected ? color : Colors.transparent,
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: isSelected color: isSelected
? color ? AppColors.accent
: Theme.of(context).colorScheme.onSurface.withOpacity(0.2), : Theme.of(context).colorScheme.surface,
width: 1.5, borderRadius: BorderRadius.circular(12),
),
), ),
child: Text( child: Text(
label, label,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
fontSize: 13, fontSize: 14,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: isSelected color: isSelected
? Colors.white ? Colors.white
@@ -326,48 +203,13 @@ class _TypeToggle extends StatelessWidget {
} }
} }
class _ChartToggle extends StatelessWidget { class _TypeSegment extends StatelessWidget {
final ChartType selected; final String label;
final ValueChanged<ChartType> onChanged;
const _ChartToggle({required this.selected, required this.onChanged});
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(10),
border: isDark
? null
: Border.all(color: const Color(0xFFDDDDEE), width: 1),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
_ToggleButton(
icon: Icons.pie_chart_rounded,
isSelected: selected == ChartType.pie,
onTap: () => onChanged(ChartType.pie),
),
_ToggleButton(
icon: Icons.bar_chart_rounded,
isSelected: selected == ChartType.bar,
onTap: () => onChanged(ChartType.bar),
),
],
),
);
}
}
class _ToggleButton extends StatelessWidget {
final IconData icon;
final bool isSelected; final bool isSelected;
final VoidCallback onTap; final VoidCallback onTap;
const _ToggleButton({
required this.icon, const _TypeSegment({
required this.label,
required this.isSelected, required this.isSelected,
required this.onTap, required this.onTap,
}); });
@@ -377,26 +219,35 @@ class _ToggleButton extends StatelessWidget {
return GestureDetector( return GestureDetector(
onTap: onTap, onTap: onTap,
child: Container( child: Container(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isSelected color: isSelected
? AppColors.accent.withOpacity(0.15) ? (_showIncomeColor(label) ? AppColors.income : AppColors.expense)
: Colors.transparent, : Colors.transparent,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(12),
), ),
child: Icon( child: Text(
icon, label,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: isSelected color: isSelected
? AppColors.accent ? Colors.white
: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), : Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
size: 20, ),
), ),
), ),
); );
} }
bool _showIncomeColor(String label) {
final lower = label.toLowerCase();
return lower.contains('income') || lower.contains('доход');
}
} }
class _PieChartCard extends ConsumerWidget { class _PieChartSection extends ConsumerWidget {
final Map<String, double> data; final Map<String, double> data;
final double total; final double total;
final int touchedIndex; final int touchedIndex;
@@ -404,7 +255,7 @@ class _PieChartCard extends ConsumerWidget {
final CurrencyInfo currencyInfo; final CurrencyInfo currencyInfo;
final bool isIncome; final bool isIncome;
const _PieChartCard({ const _PieChartSection({
required this.data, required this.data,
required this.total, required this.total,
required this.touchedIndex, required this.touchedIndex,
@@ -418,25 +269,10 @@ class _PieChartCard extends ConsumerWidget {
final s = ref.watch(stringsProvider); final s = ref.watch(stringsProvider);
final fmt = ref.watch(amountFormatProvider); final fmt = ref.watch(amountFormatProvider);
final entries = data.entries.toList(); final entries = data.entries.toList();
final isDark = Theme.of(context).brightness == Brightness.dark;
final accent = isIncome ? AppColors.income : AppColors.expense; final accent = isIncome ? AppColors.income : AppColors.expense;
return Container( return SizedBox(
padding: const EdgeInsets.all(20), height: 240,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: isDark
? Colors.white.withOpacity(0.08)
: const Color(0xFFDDDDEE),
width: 1,
),
),
child: Column(
children: [
SizedBox(
height: 220,
child: Stack( child: Stack(
alignment: Alignment.center, alignment: Alignment.center,
children: [ children: [
@@ -453,8 +289,8 @@ class _PieChartCard extends ConsumerWidget {
onTouch(response.touchedSection!.touchedSectionIndex); onTouch(response.touchedSection!.touchedSectionIndex);
}, },
), ),
sectionsSpace: 3, sectionsSpace: 2,
centerSpaceRadius: 60, centerSpaceRadius: 70,
sections: List.generate(entries.length, (i) { sections: List.generate(entries.length, (i) {
final isTouched = i == touchedIndex; final isTouched = i == touchedIndex;
final cat = entries[i].key; final cat = entries[i].key;
@@ -465,11 +301,11 @@ class _PieChartCard extends ConsumerWidget {
color: color, color: color,
value: val, value: val,
title: isTouched title: isTouched
? '${(val / total * 100).toStringAsFixed(1)}%' ? '${(val / total * 100).toStringAsFixed(0)}%'
: '', : '',
radius: isTouched ? 60 : 50, radius: isTouched ? 55 : 48,
titleStyle: const TextStyle( titleStyle: const TextStyle(
fontSize: 13, fontSize: 12,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
color: Colors.white, color: Colors.white,
), ),
@@ -483,158 +319,40 @@ class _PieChartCard extends ConsumerWidget {
Text( Text(
s.total, s.total,
style: Theme.of(context).textTheme.bodySmall?.copyWith( style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of( color: Theme.of(context).colorScheme.onSurface.withOpacity(0.5),
context,
).colorScheme.onSurface.withOpacity(0.6),
),
),
Text(
formatAmount(currencyInfo.symbol, total, fmt),
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: accent,
fontWeight: FontWeight.w700,
),
),
],
),
],
),
),
],
),
);
}
}
class _BarChartCard extends ConsumerWidget {
final List<MonthlyData> monthlyData;
final CurrencyInfo currencyInfo;
final bool isIncome;
const _BarChartCard({
required this.monthlyData,
required this.currencyInfo,
required this.isIncome,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final s = ref.watch(stringsProvider);
final fmt = ref.watch(amountFormatProvider);
final isDark = Theme.of(context).brightness == Brightness.dark;
final barColor = isIncome ? AppColors.income : AppColors.expense;
final maxY = monthlyData.isEmpty
? 100.0
: monthlyData.map((e) => e.amount).reduce((a, b) => a > b ? a : b);
final adjustedMaxY = maxY * 1.2;
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: isDark
? Colors.white.withOpacity(0.08)
: const Color(0xFFDDDDEE),
width: 1,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
s.lastSixMonths,
style: Theme.of(context).textTheme.titleSmall?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 16),
SizedBox(
height: 200,
child: BarChart(
BarChartData(
alignment: BarChartAlignment.spaceAround,
maxY: adjustedMaxY > 0 ? adjustedMaxY : 100,
barTouchData: BarTouchData(
touchTooltipData: BarTouchTooltipData(
getTooltipItem: (group, groupIndex, rod, rodIndex) {
return BarTooltipItem(
formatAmount(currencyInfo.symbol, rod.toY, fmt),
const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w600,
fontSize: 12,
),
);
},
),
),
titlesData: FlTitlesData(
show: true,
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
getTitlesWidget: (value, meta) {
if (value.toInt() >= 0 &&
value.toInt() < monthlyData.length) {
final month = monthlyData[value.toInt()].month;
return Padding(
padding: const EdgeInsets.only(top: 8),
child: Text(
DateFormat('MMM').format(month),
style: TextStyle(
color: Theme.of(
context,
).colorScheme.onSurface.withOpacity(0.6),
fontSize: 11, fontSize: 11,
), ),
), ),
); const SizedBox(height: 4),
} currencyInfo.code == 'BYN'
return const Text(''); ? Row(
}, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
BynSign(
fontSize: 24,
color: accent,
), ),
), const SizedBox(width: 2),
leftTitles: const AxisTitles( Text(
sideTitles: SideTitles(showTitles: false), formatAmount('', total, fmt),
), style: Theme.of(context).textTheme.titleLarge?.copyWith(
topTitles: const AxisTitles( color: accent,
sideTitles: SideTitles(showTitles: false), fontWeight: FontWeight.w700,
), fontSize: 24,
rightTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
),
gridData: FlGridData(
show: true,
drawVerticalLine: false,
horizontalInterval: adjustedMaxY > 0 ? adjustedMaxY / 4 : 25,
getDrawingHorizontalLine: (value) => FlLine(
color: Theme.of(context).dividerColor,
strokeWidth: 1,
),
),
borderData: FlBorderData(show: false),
barGroups: List.generate(
monthlyData.length,
(i) => BarChartGroupData(
x: i,
barRods: [
BarChartRodData(
toY: monthlyData[i].amount,
color: barColor,
width: 24,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(6),
), ),
), ),
], ],
)
: Text(
formatAmount(currencyInfo.symbol, total, fmt),
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: accent,
fontWeight: FontWeight.w700,
fontSize: 24,
), ),
), ),
), ],
),
), ),
], ],
), ),
@@ -642,16 +360,14 @@ class _BarChartCard extends ConsumerWidget {
} }
} }
class _CategoryRow extends ConsumerWidget { class _CategoryItem extends ConsumerWidget {
final int rank;
final String category; final String category;
final double amount; final double amount;
final double total; final double total;
final CurrencyInfo currencyInfo; final CurrencyInfo currencyInfo;
final bool isIncome; final bool isIncome;
const _CategoryRow({ const _CategoryItem({
required this.rank,
required this.category, required this.category,
required this.amount, required this.amount,
required this.total, required this.total,
@@ -663,89 +379,57 @@ class _CategoryRow extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final s = ref.watch(stringsProvider); final s = ref.watch(stringsProvider);
final fmt = ref.watch(amountFormatProvider); final fmt = ref.watch(amountFormatProvider);
final isDark = Theme.of(context).brightness == Brightness.dark;
final color = AppCategories.colors[category] ?? AppColors.accent; final color = AppCategories.colors[category] ?? AppColors.accent;
final icon = AppCategories.icons[category] ?? Icons.category_rounded; final icon = AppCategories.icons[category] ?? Icons.category_rounded;
final pct = total > 0 ? amount / total : 0.0; final pct = total > 0 ? amount / total : 0.0;
return Container( return Row(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: isDark
? Colors.white.withOpacity(0.08)
: const Color(0xFFDDDDEE),
width: 1,
),
),
child: Column(
children: [
Row(
children: [ children: [
Container( Container(
width: 28, padding: const EdgeInsets.all(10),
height: 28,
alignment: Alignment.center,
decoration: BoxDecoration( decoration: BoxDecoration(
color: rank <= 3 color: color.withOpacity(0.12),
? color.withOpacity(0.2)
: Theme.of(context).dividerColor,
shape: BoxShape.circle,
),
child: Text(
'$rank',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: rank <= 3
? color
: Theme.of(
context,
).colorScheme.onSurface.withOpacity(0.6),
fontWeight: FontWeight.w700,
),
),
),
const SizedBox(width: 10),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: color.withOpacity(0.15),
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
), ),
child: Icon(icon, color: color, size: 18), child: Icon(icon, color: color, size: 20),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded( Expanded(
child: Text( child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
s.categoryLabel(category), s.categoryLabel(category),
style: Theme.of(context).textTheme.bodyMedium?.copyWith( style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.onSurface, color: Theme.of(context).colorScheme.onSurface,
), ),
), ),
const SizedBox(height: 2),
Text(
'${(pct * 100).toStringAsFixed(1)}%',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.5),
fontSize: 12,
),
),
],
),
), ),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
currencyInfo.code == 'BYN' currencyInfo.code == 'BYN'
? Row( ? Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
BynSign( BynSign(
fontSize: 14, fontSize: 15,
color: isIncome color: isIncome ? AppColors.income : AppColors.expense,
? AppColors.income
: AppColors.expense,
), ),
const SizedBox(width: 2), const SizedBox(width: 2),
Text( Text(
formatAmount('', amount, fmt), formatAmount('', amount, fmt),
style: Theme.of(context).textTheme.bodyMedium style: Theme.of(context).textTheme.bodyMedium?.copyWith(
?.copyWith( color: isIncome ? AppColors.income : AppColors.expense,
color: isIncome
? AppColors.income
: AppColors.expense,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
), ),
), ),
@@ -753,38 +437,12 @@ class _CategoryRow extends ConsumerWidget {
) )
: Text( : Text(
formatAmount(currencyInfo.symbol, amount, fmt), formatAmount(currencyInfo.symbol, amount, fmt),
style: Theme.of(context).textTheme.bodyMedium style: Theme.of(context).textTheme.bodyMedium?.copyWith(
?.copyWith( color: isIncome ? AppColors.income : AppColors.expense,
color: isIncome
? AppColors.income
: AppColors.expense,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
), ),
), ),
Text(
'${(pct * 100).toStringAsFixed(1)}%',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(
context,
).colorScheme.onSurface.withOpacity(0.6),
),
),
], ],
),
],
),
const SizedBox(height: 10),
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator(
value: pct,
backgroundColor: Theme.of(context).dividerColor,
valueColor: AlwaysStoppedAnimation<Color>(color),
minHeight: 6,
),
),
],
),
); );
} }
} }
@@ -796,44 +454,30 @@ class _EmptyState extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final s = ref.watch(stringsProvider); final s = ref.watch(stringsProvider);
final isDark = Theme.of(context).brightness == Brightness.dark;
return Center( return Center(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Container( Icon(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
shape: BoxShape.circle,
border: Border.all(
color: isDark
? Colors.white.withOpacity(0.08)
: const Color(0xFFDDDDEE),
width: 1,
),
),
child: Icon(
Icons.pie_chart_outline_rounded, Icons.pie_chart_outline_rounded,
size: 48, size: 56,
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), color: Theme.of(context).colorScheme.onSurface.withOpacity(0.3),
),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Text( Text(
isIncome ? s.noIncomeData : s.noExpenseData, isIncome ? s.noIncomeData : s.noExpenseData,
style: Theme.of(context).textTheme.titleMedium?.copyWith( style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurface, color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7),
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
), ),
const SizedBox(height: 6), const SizedBox(height: 8),
Text( Text(
isIncome ? s.addIncomeToSeeBreakdown : s.addExpensesToSeeBreakdown, isIncome ? s.addIncomeToSeeBreakdown : s.addExpensesToSeeBreakdown,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium?.copyWith( style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), color: Theme.of(context).colorScheme.onSurface.withOpacity(0.4),
), ),
), ),
], ],
+3 -2
View File
@@ -103,7 +103,8 @@ class TransactionsNotifier extends AsyncNotifier<List<Transaction>> {
} }
final transferPairsProvider = Provider<Map<String, Transaction>>((ref) { final transferPairsProvider = Provider<Map<String, Transaction>>((ref) {
final txs = ref.watch(transactionsProvider).value ?? []; final txsAsync = ref.watch(transactionsProvider);
final txs = txsAsync.value ?? [];
final transfers = txs.where((t) => t.category == 'Transfer').toList(); final transfers = txs.where((t) => t.category == 'Transfer').toList();
final Map<String, Transaction> pairs = {}; final Map<String, Transaction> pairs = {};
@@ -167,9 +168,9 @@ class _TimeFilterNotifier extends Notifier<TimeFilter> {
} }
final accountFilteredTransactionsProvider = Provider<List<Transaction>>((ref) { final accountFilteredTransactionsProvider = Provider<List<Transaction>>((ref) {
final activeAccount = ref.watch(activeAccountProvider);
final txsAsync = ref.watch(transactionsProvider); final txsAsync = ref.watch(transactionsProvider);
final txs = txsAsync.value ?? []; final txs = txsAsync.value ?? [];
final activeAccount = ref.watch(activeAccountProvider);
if (activeAccount == null) { if (activeAccount == null) {
return txs; return txs;
@@ -61,7 +61,7 @@ class _AccountEditorPanelState extends ConsumerState<AccountEditorPanel> {
], ],
), ),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@@ -76,7 +76,7 @@ class _AccountEditorPanelState extends ConsumerState<AccountEditorPanel> {
).colorScheme.onSurface.withOpacity(0.6), ).colorScheme.onSurface.withOpacity(0.6),
), ),
), ),
const SizedBox(height: 8), const SizedBox(height: 6),
IntrinsicHeight( IntrinsicHeight(
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,