diff --git a/lib/core/l10n/app_strings.dart b/lib/core/l10n/app_strings.dart index 4b2c436..b8a6a33 100644 --- a/lib/core/l10n/app_strings.dart +++ b/lib/core/l10n/app_strings.dart @@ -24,9 +24,6 @@ class AppStrings { String get filterMonth => _ru ? 'Месяц' : 'Month'; String get income => _ru ? 'Доход' : 'Income'; String get expenses => _ru ? 'Расходы' : 'Expenses'; - String get monthlyBudget => _ru ? 'Бюджет на месяц' : 'Monthly Budget'; - String get spent => _ru ? 'Потрачено' : 'Spent'; - String get limit => _ru ? 'Лимит' : 'Limit'; String get noTransactions => _ru ? 'Транзакции не найдены' : 'No transactions found'; String get addFirstTx => _ru @@ -80,18 +77,6 @@ class AppStrings { String get language => _ru ? 'Язык' : 'Language'; String get langRu => _ru ? 'Русский' : 'Russian'; 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 exportData => _ru ? 'Экспорт данных' : 'Export data'; String get clearData => _ru ? 'Очистить данные' : 'Clear all data'; diff --git a/lib/features/add_transaction/widgets/category_picker.dart b/lib/features/add_transaction/widgets/category_picker.dart index adeb6a3..c96f6fe 100644 --- a/lib/features/add_transaction/widgets/category_picker.dart +++ b/lib/features/add_transaction/widgets/category_picker.dart @@ -64,17 +64,19 @@ class CategoryPicker extends ConsumerWidget { size: 16, ), const SizedBox(width: 6), - Text( - cat.label(isRu), - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: isSelected - ? color - : Theme.of( - context, - ).colorScheme.onSurface.withOpacity(0.6), - fontWeight: isSelected - ? FontWeight.w600 - : FontWeight.normal, + Flexible( + child: Text( + cat.label(isRu), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: isSelected + ? color + : Theme.of(context).colorScheme.onSurface.withOpacity(0.6), + fontWeight: isSelected + ? FontWeight.w600 + : FontWeight.normal, + ), ), ), ], diff --git a/lib/features/categories/screen.dart b/lib/features/categories/screen.dart index d6590fc..7f1909f 100644 --- a/lib/features/categories/screen.dart +++ b/lib/features/categories/screen.dart @@ -3,8 +3,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart'; import '../../core/constants.dart'; +import '../../core/l10n/app_strings.dart'; import '../../core/l10n/locale_provider.dart'; +import '../../core/services/haptic_service.dart'; import '../../shared/providers/amount_format_provider.dart'; +import '../../shared/providers/category_provider.dart'; import '../../shared/utils/currency_utils.dart'; import '../../shared/widgets/byn_sign.dart'; import '../settings/provider.dart'; @@ -20,11 +23,13 @@ class CategoriesScreen extends ConsumerStatefulWidget { class _CategoriesScreenState extends ConsumerState { int _touchedIndex = -1; - final bool _showIncome = false; + bool _showIncome = false; @override Widget build(BuildContext context) { final s = ref.watch(stringsProvider); + final isRu = s.locale == AppLocale.ru; + final catalog = ref.watch(categoryCatalogProvider); final summary = ref.watch(statsSummaryProvider); final data = _showIncome ? ref.watch(categoryIncomeProvider) @@ -57,6 +62,17 @@ class _CategoriesScreenState extends ConsumerState { children: [ const AccountScopeChips(), const SizedBox(height: 16), + _IncomeExpenseToggle( + isIncome: _showIncome, + onChanged: (val) { + HapticService.selection(); + setState(() { + _showIncome = val; + _touchedIndex = -1; + }); + }, + ), + const SizedBox(height: 16), _InsightCard( title: s.overview, subtitle: s.analyticsInsight, @@ -128,7 +144,7 @@ class _CategoriesScreenState extends ConsumerState { title: s.topCategories, subtitle: topEntry == null ? s.rankedByAmount - : '${s.topCategory}: ${s.categoryLabel(topEntry.key)}', + : '${s.topCategory}: ${catalog.labelFor(topEntry.key, isRu)}', child: Column( children: [ _SummaryBadgeRow( @@ -231,6 +247,8 @@ class _PieChartSection extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final s = ref.watch(stringsProvider); + final isRu = s.locale == AppLocale.ru; + final catalog = ref.watch(categoryCatalogProvider); final fmt = ref.watch(amountFormatProvider); final entries = data.entries.toList(); final accent = isIncome ? AppColors.income : AppColors.expense; @@ -264,7 +282,7 @@ class _PieChartSection extends ConsumerWidget { final isTouched = i == touchedIndex; final cat = entries[i].key; final val = entries[i].value; - final color = AppCategories.colors[cat] ?? AppColors.accent; + final color = catalog.colorFor(cat, accent); return PieChartSectionData( color: color, value: val, @@ -319,7 +337,7 @@ class _PieChartSection extends ConsumerWidget { width: double.infinity, padding: const EdgeInsets.all(14), decoration: BoxDecoration( - color: (AppCategories.colors[selectedEntry.key] ?? accent).withOpacity(0.10), + color: catalog.colorFor(selectedEntry.key, accent).withOpacity(0.10), borderRadius: BorderRadius.circular(18), ), child: Row( @@ -328,14 +346,14 @@ class _PieChartSection extends ConsumerWidget { width: 12, height: 12, decoration: BoxDecoration( - color: AppCategories.colors[selectedEntry.key] ?? accent, + color: catalog.colorFor(selectedEntry.key, accent), shape: BoxShape.circle, ), ), const SizedBox(width: 10), Expanded( child: Text( - s.categoryLabel(selectedEntry.key), + catalog.labelFor(selectedEntry.key, isRu), style: Theme.of(context).textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.w700, ), @@ -457,9 +475,11 @@ class _CategoryItem extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final s = ref.watch(stringsProvider); + final isRu = s.locale == AppLocale.ru; + final catalog = ref.watch(categoryCatalogProvider); final fmt = ref.watch(amountFormatProvider); - final color = AppCategories.colors[category] ?? AppColors.accent; - final icon = AppCategories.icons[category] ?? Icons.category_rounded; + final color = catalog.colorFor(category, AppColors.accent); + final icon = catalog.iconFor(category); final pct = total > 0 ? amount / total : 0.0; return Container( @@ -486,7 +506,7 @@ class _CategoryItem extends ConsumerWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - s.categoryLabel(category), + catalog.labelFor(category, isRu), style: Theme.of(context).textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.w700, color: Theme.of(context).colorScheme.onSurface, @@ -846,3 +866,90 @@ class _EmptyState extends ConsumerWidget { return isIncome; } } + +class _IncomeExpenseToggle extends ConsumerWidget { + final bool isIncome; + final ValueChanged 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), + ), + ), + ), + ); + } +} diff --git a/lib/features/settings/categories/category_manager_screen.dart b/lib/features/settings/categories/category_manager_screen.dart index f44d0c6..6995a4b 100644 --- a/lib/features/settings/categories/category_manager_screen.dart +++ b/lib/features/settings/categories/category_manager_screen.dart @@ -133,7 +133,7 @@ class _SectionLabel extends StatelessWidget { } } -class _CategoryRow extends StatelessWidget { +class _CategoryRow extends ConsumerWidget { final AppCategory category; final bool isRu; final VoidCallback? onTap; @@ -147,7 +147,8 @@ class _CategoryRow extends StatelessWidget { }); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final s = ref.watch(stringsProvider); final theme = Theme.of(context); final isDark = theme.brightness == Brightness.dark; final isIncome = category.type == TransactionType.income; @@ -188,9 +189,7 @@ class _CategoryRow extends StatelessWidget { ), const SizedBox(height: 2), Text( - isIncome - ? (isRu ? 'Доход' : 'Income') - : (isRu ? 'Расход' : 'Expense'), + isIncome ? s.income : s.expenses, style: theme.textTheme.bodySmall?.copyWith( color: (isIncome ? AppColors.income : AppColors.expense) .withOpacity(0.9), diff --git a/lib/features/settings/screen.dart b/lib/features/settings/screen.dart index 8632aa5..640dd65 100644 --- a/lib/features/settings/screen.dart +++ b/lib/features/settings/screen.dart @@ -143,6 +143,8 @@ class SettingsScreen extends ConsumerWidget { const CurrencySection(), const SizedBox(height: 16), const AmountFormatSection(), + const SizedBox(height: 16), + const CategoriesSection(), const SizedBox(height: 24), Text( s.dangerZone,