big
This commit is contained in:
2026-06-28 02:56:09 +03:00
parent bc51609f85
commit 1a6ad1fe27
5 changed files with 135 additions and 40 deletions
-15
View File
@@ -24,9 +24,6 @@ class AppStrings {
String get filterMonth => _ru ? 'Месяц' : 'Month'; String get filterMonth => _ru ? 'Месяц' : 'Month';
String get income => _ru ? 'Доход' : 'Income'; String get income => _ru ? 'Доход' : 'Income';
String get expenses => _ru ? 'Расходы' : 'Expenses'; String get expenses => _ru ? 'Расходы' : 'Expenses';
String get monthlyBudget => _ru ? 'Бюджет на месяц' : 'Monthly Budget';
String get spent => _ru ? 'Потрачено' : 'Spent';
String get limit => _ru ? 'Лимит' : 'Limit';
String get noTransactions => String get noTransactions =>
_ru ? 'Транзакции не найдены' : 'No transactions found'; _ru ? 'Транзакции не найдены' : 'No transactions found';
String get addFirstTx => _ru String get addFirstTx => _ru
@@ -80,18 +77,6 @@ class AppStrings {
String get language => _ru ? 'Язык' : 'Language'; String get language => _ru ? 'Язык' : 'Language';
String get langRu => _ru ? 'Русский' : 'Russian'; String get langRu => _ru ? 'Русский' : 'Russian';
String get langEn => _ru ? 'Английский' : 'English'; String get langEn => _ru ? 'Английский' : 'English';
String get budget => _ru ? 'Бюджет' : 'Budget';
String get budgetHint => _ru ? 'Месячный лимит' : 'Monthly limit';
String get budgetNone => _ru ? 'Не установлен' : 'Not set';
String get monthlyBudgetSetting => _ru ? 'Месячный бюджет' : 'Monthly Budget';
String get yourMonthlySpendingLimit =>
_ru ? 'Ваш лимит расходов на месяц' : 'Your monthly spending limit';
String get setMonthlySpendingLimit => _ru
? 'Контролируйте свои расходы за месяц'
: 'Track your monthly spending';
String get leaveEmptyToRemove => _ru
? 'Оставьте пустым для удаления лимита'
: 'Leave empty to remove budget limit';
String get data => _ru ? 'Данные' : 'Data'; String get data => _ru ? 'Данные' : 'Data';
String get exportData => _ru ? 'Экспорт данных' : 'Export data'; String get exportData => _ru ? 'Экспорт данных' : 'Export data';
String get clearData => _ru ? 'Очистить данные' : 'Clear all data'; String get clearData => _ru ? 'Очистить данные' : 'Clear all data';
@@ -64,19 +64,21 @@ class CategoryPicker extends ConsumerWidget {
size: 16, size: 16,
), ),
const SizedBox(width: 6), const SizedBox(width: 6),
Text( Flexible(
child: Text(
cat.label(isRu), cat.label(isRu),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodySmall?.copyWith( style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: isSelected color: isSelected
? color ? color
: Theme.of( : Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
context,
).colorScheme.onSurface.withOpacity(0.6),
fontWeight: isSelected fontWeight: isSelected
? FontWeight.w600 ? FontWeight.w600
: FontWeight.normal, : FontWeight.normal,
), ),
), ),
),
], ],
), ),
), ),
+116 -9
View File
@@ -3,8 +3,11 @@ 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 'package:intl/intl.dart';
import '../../core/constants.dart'; import '../../core/constants.dart';
import '../../core/l10n/app_strings.dart';
import '../../core/l10n/locale_provider.dart'; import '../../core/l10n/locale_provider.dart';
import '../../core/services/haptic_service.dart';
import '../../shared/providers/amount_format_provider.dart'; import '../../shared/providers/amount_format_provider.dart';
import '../../shared/providers/category_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 '../settings/provider.dart'; import '../settings/provider.dart';
@@ -20,11 +23,13 @@ class CategoriesScreen extends ConsumerStatefulWidget {
class _CategoriesScreenState extends ConsumerState<CategoriesScreen> { class _CategoriesScreenState extends ConsumerState<CategoriesScreen> {
int _touchedIndex = -1; int _touchedIndex = -1;
final bool _showIncome = false; bool _showIncome = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final s = ref.watch(stringsProvider); final s = ref.watch(stringsProvider);
final isRu = s.locale == AppLocale.ru;
final catalog = ref.watch(categoryCatalogProvider);
final summary = ref.watch(statsSummaryProvider); final summary = ref.watch(statsSummaryProvider);
final data = _showIncome final data = _showIncome
? ref.watch(categoryIncomeProvider) ? ref.watch(categoryIncomeProvider)
@@ -57,6 +62,17 @@ class _CategoriesScreenState extends ConsumerState<CategoriesScreen> {
children: [ children: [
const AccountScopeChips(), const AccountScopeChips(),
const SizedBox(height: 16), const SizedBox(height: 16),
_IncomeExpenseToggle(
isIncome: _showIncome,
onChanged: (val) {
HapticService.selection();
setState(() {
_showIncome = val;
_touchedIndex = -1;
});
},
),
const SizedBox(height: 16),
_InsightCard( _InsightCard(
title: s.overview, title: s.overview,
subtitle: s.analyticsInsight, subtitle: s.analyticsInsight,
@@ -128,7 +144,7 @@ class _CategoriesScreenState extends ConsumerState<CategoriesScreen> {
title: s.topCategories, title: s.topCategories,
subtitle: topEntry == null subtitle: topEntry == null
? s.rankedByAmount ? s.rankedByAmount
: '${s.topCategory}: ${s.categoryLabel(topEntry.key)}', : '${s.topCategory}: ${catalog.labelFor(topEntry.key, isRu)}',
child: Column( child: Column(
children: [ children: [
_SummaryBadgeRow( _SummaryBadgeRow(
@@ -231,6 +247,8 @@ class _PieChartSection 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 isRu = s.locale == AppLocale.ru;
final catalog = ref.watch(categoryCatalogProvider);
final fmt = ref.watch(amountFormatProvider); final fmt = ref.watch(amountFormatProvider);
final entries = data.entries.toList(); final entries = data.entries.toList();
final accent = isIncome ? AppColors.income : AppColors.expense; final accent = isIncome ? AppColors.income : AppColors.expense;
@@ -264,7 +282,7 @@ class _PieChartSection extends ConsumerWidget {
final isTouched = i == touchedIndex; final isTouched = i == touchedIndex;
final cat = entries[i].key; final cat = entries[i].key;
final val = entries[i].value; final val = entries[i].value;
final color = AppCategories.colors[cat] ?? AppColors.accent; final color = catalog.colorFor(cat, accent);
return PieChartSectionData( return PieChartSectionData(
color: color, color: color,
value: val, value: val,
@@ -319,7 +337,7 @@ class _PieChartSection extends ConsumerWidget {
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.all(14), padding: const EdgeInsets.all(14),
decoration: BoxDecoration( decoration: BoxDecoration(
color: (AppCategories.colors[selectedEntry.key] ?? accent).withOpacity(0.10), color: catalog.colorFor(selectedEntry.key, accent).withOpacity(0.10),
borderRadius: BorderRadius.circular(18), borderRadius: BorderRadius.circular(18),
), ),
child: Row( child: Row(
@@ -328,14 +346,14 @@ class _PieChartSection extends ConsumerWidget {
width: 12, width: 12,
height: 12, height: 12,
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppCategories.colors[selectedEntry.key] ?? accent, color: catalog.colorFor(selectedEntry.key, accent),
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
Expanded( Expanded(
child: Text( child: Text(
s.categoryLabel(selectedEntry.key), catalog.labelFor(selectedEntry.key, isRu),
style: Theme.of(context).textTheme.bodyMedium?.copyWith( style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
), ),
@@ -457,9 +475,11 @@ class _CategoryItem 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 isRu = s.locale == AppLocale.ru;
final catalog = ref.watch(categoryCatalogProvider);
final fmt = ref.watch(amountFormatProvider); final fmt = ref.watch(amountFormatProvider);
final color = AppCategories.colors[category] ?? AppColors.accent; final color = catalog.colorFor(category, AppColors.accent);
final icon = AppCategories.icons[category] ?? Icons.category_rounded; final icon = catalog.iconFor(category);
final pct = total > 0 ? amount / total : 0.0; final pct = total > 0 ? amount / total : 0.0;
return Container( return Container(
@@ -486,7 +506,7 @@ class _CategoryItem extends ConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
s.categoryLabel(category), catalog.labelFor(category, isRu),
style: Theme.of(context).textTheme.bodyMedium?.copyWith( style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
color: Theme.of(context).colorScheme.onSurface, color: Theme.of(context).colorScheme.onSurface,
@@ -846,3 +866,90 @@ class _EmptyState extends ConsumerWidget {
return isIncome; return isIncome;
} }
} }
class _IncomeExpenseToggle extends ConsumerWidget {
final bool isIncome;
final ValueChanged<bool> onChanged;
const _IncomeExpenseToggle({
required this.isIncome,
required this.onChanged,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final s = ref.watch(stringsProvider);
final theme = Theme.of(context);
return Container(
decoration: BoxDecoration(
color: theme.colorScheme.surface,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: theme.colorScheme.onSurface.withOpacity(0.06),
),
),
padding: const EdgeInsets.all(4),
child: Row(
children: [
Expanded(
child: _ToggleChip(
label: s.expenses,
isSelected: !isIncome,
color: AppColors.expense,
onTap: () => onChanged(false),
),
),
Expanded(
child: _ToggleChip(
label: s.income,
isSelected: isIncome,
color: AppColors.income,
onTap: () => onChanged(true),
),
),
],
),
);
}
}
class _ToggleChip extends StatelessWidget {
final String label;
final bool isSelected;
final Color color;
final VoidCallback onTap;
const _ToggleChip({
required this.label,
required this.isSelected,
required this.color,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(vertical: 10),
decoration: BoxDecoration(
color: isSelected ? color.withOpacity(0.15) : Colors.transparent,
borderRadius: BorderRadius.circular(8),
),
child: Text(
label,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
fontWeight: isSelected ? FontWeight.w700 : FontWeight.w500,
color: isSelected
? color
: Theme.of(context).colorScheme.onSurface.withOpacity(0.5),
),
),
),
);
}
}
@@ -133,7 +133,7 @@ class _SectionLabel extends StatelessWidget {
} }
} }
class _CategoryRow extends StatelessWidget { class _CategoryRow extends ConsumerWidget {
final AppCategory category; final AppCategory category;
final bool isRu; final bool isRu;
final VoidCallback? onTap; final VoidCallback? onTap;
@@ -147,7 +147,8 @@ class _CategoryRow extends StatelessWidget {
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, WidgetRef ref) {
final s = ref.watch(stringsProvider);
final theme = Theme.of(context); final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark; final isDark = theme.brightness == Brightness.dark;
final isIncome = category.type == TransactionType.income; final isIncome = category.type == TransactionType.income;
@@ -188,9 +189,7 @@ class _CategoryRow extends StatelessWidget {
), ),
const SizedBox(height: 2), const SizedBox(height: 2),
Text( Text(
isIncome isIncome ? s.income : s.expenses,
? (isRu ? 'Доход' : 'Income')
: (isRu ? 'Расход' : 'Expense'),
style: theme.textTheme.bodySmall?.copyWith( style: theme.textTheme.bodySmall?.copyWith(
color: (isIncome ? AppColors.income : AppColors.expense) color: (isIncome ? AppColors.income : AppColors.expense)
.withOpacity(0.9), .withOpacity(0.9),
+2
View File
@@ -143,6 +143,8 @@ class SettingsScreen extends ConsumerWidget {
const CurrencySection(), const CurrencySection(),
const SizedBox(height: 16), const SizedBox(height: 16),
const AmountFormatSection(), const AmountFormatSection(),
const SizedBox(height: 16),
const CategoriesSection(),
const SizedBox(height: 24), const SizedBox(height: 24),
Text( Text(
s.dangerZone, s.dangerZone,