diff --git a/.gitignore b/.gitignore index 3820a95..3179de2 100644 --- a/.gitignore +++ b/.gitignore @@ -21,7 +21,7 @@ migrate_working_dir/ # The .vscode folder contains launch configuration and tasks you configure in # VS Code which you may wish to be included in version control, so this line # is commented out by default. -#.vscode/ +.vscode/ # Flutter/Dart/Pub related **/doc/api/ @@ -43,3 +43,4 @@ app.*.map.json /android/app/debug /android/app/profile /android/app/release +*.jks diff --git a/flutter_01.png b/flutter_01.png deleted file mode 100644 index 56fe064..0000000 Binary files a/flutter_01.png and /dev/null differ diff --git a/lib/app/router.dart b/lib/app/router.dart index d9eaa07..4dfe0fb 100644 --- a/lib/app/router.dart +++ b/lib/app/router.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; +import '../core/l10n/locale_provider.dart'; import '../features/dashboard/screen.dart'; import '../features/add_transaction/screen.dart'; import '../features/categories/screen.dart'; @@ -50,7 +52,7 @@ final appRouter = GoRouter( ], ); -class AppShell extends StatelessWidget { +class AppShell extends ConsumerWidget { final Widget child; const AppShell({super.key, required this.child}); @@ -62,7 +64,8 @@ class AppShell extends StatelessWidget { } @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final s = ref.watch(stringsProvider); final idx = _locationToIndex(context); return Scaffold( body: child, @@ -73,21 +76,21 @@ class AppShell extends StatelessWidget { if (i == 1) context.go('/categories'); if (i == 2) context.go('/settings'); }, - destinations: const [ + destinations: [ NavigationDestination( - icon: Icon(Icons.dashboard_outlined), - selectedIcon: Icon(Icons.dashboard_rounded), - label: 'Dashboard', + icon: const Icon(Icons.dashboard_outlined), + selectedIcon: const Icon(Icons.dashboard_rounded), + label: s.navDashboard, ), NavigationDestination( - icon: Icon(Icons.pie_chart_outline_rounded), - selectedIcon: Icon(Icons.pie_chart_rounded), - label: 'Categories', + icon: const Icon(Icons.pie_chart_outline_rounded), + selectedIcon: const Icon(Icons.pie_chart_rounded), + label: s.navCategories, ), NavigationDestination( - icon: Icon(Icons.settings_outlined), - selectedIcon: Icon(Icons.settings_rounded), - label: 'Settings', + icon: const Icon(Icons.settings_outlined), + selectedIcon: const Icon(Icons.settings_rounded), + label: s.navSettings, ), ], ), diff --git a/lib/app/theme.dart b/lib/app/theme.dart index 294489b..effabb3 100644 --- a/lib/app/theme.dart +++ b/lib/app/theme.dart @@ -12,7 +12,6 @@ class AppTheme { ); return base.copyWith( - fontFamily: GoogleFonts.poppins().fontFamily, // Explicit font family for Cyrillic support textTheme: textTheme, scaffoldBackgroundColor: AppColors.background, colorScheme: const ColorScheme.dark( @@ -112,7 +111,6 @@ class AppTheme { ); return base.copyWith( - fontFamily: GoogleFonts.poppins().fontFamily, // Explicit font family for Cyrillic support textTheme: textTheme, scaffoldBackgroundColor: const Color(0xFFF0F0F7), colorScheme: const ColorScheme.light( @@ -121,8 +119,6 @@ class AppTheme { secondary: AppColors.accent, onPrimary: Colors.white, onSurface: Color(0xFF1A1A2E), - onBackground: Color(0xFF1A1A2E), - background: Color(0xFFF0F0F7), ), cardTheme: CardThemeData( color: Colors.white, diff --git a/lib/core/l10n/app_strings.dart b/lib/core/l10n/app_strings.dart index 1f2b176..24a81c8 100644 --- a/lib/core/l10n/app_strings.dart +++ b/lib/core/l10n/app_strings.dart @@ -10,6 +10,7 @@ class AppStrings { // ── Dashboard ── String get appTitle => _ru ? 'Мои финансы' : 'My Finances'; String get totalBalance => _ru ? 'ОБЩИЙ БАЛАНС' : 'TOTAL BALANCE'; + String get tapAndHoldToEdit => _ru ? 'удерживайте для редактирования' : 'tap and hold to edit'; String get add => _ru ? 'Добавить' : 'Add'; String get transactions => _ru ? 'Транзакции' : 'Transactions'; String get searchHint => _ru ? 'Поиск транзакций...' : 'Search transactions...'; @@ -85,6 +86,11 @@ class AppStrings { String get allTransactionsWillBeDeleted => _ru ? 'Все транзакции будут удалены навсегда. Восстановить их будет невозможно.' : 'All transactions will be deleted forever. There is no way to recover them.'; String get dangerZone => _ru ? 'Опасная зона' : 'Danger Zone'; + // ── Navigation ── + String get navDashboard => _ru ? 'Главная' : 'Dashboard'; + String get navCategories => _ru ? 'Категории' : 'Categories'; + String get navSettings => _ru ? 'Настройки' : 'Settings'; + // ── Categories ── String get categories => _ru ? 'Категории' : 'Categories'; String get rankedByAmount => _ru ? 'По сумме' : 'Ranked by Amount'; @@ -95,6 +101,10 @@ class AppStrings { String get categoryColor => _ru ? 'Цвет' : 'Color'; String get deleteCategory => _ru ? 'Удалить категорию' : 'Delete Category'; String get noCategoriesYet => _ru ? 'Нет категорий' : 'No categories yet'; + String get noExpenseData => _ru ? 'Нет данных о расходах' : 'No expense data'; + String get addExpensesToSeeBreakdown => _ru ? 'Добавьте расходы, чтобы увидеть разбивку' : 'Add some expenses to see the breakdown'; + String get total => _ru ? 'Всего' : 'Total'; + String get lastSixMonths => _ru ? 'Последние 6 месяцев' : 'Last 6 Months'; // ── Built-in category names (translated for display only, stored in English) ── String categoryLabel(String key) { diff --git a/lib/core/l10n/locale_provider.dart b/lib/core/l10n/locale_provider.dart index 7137616..4092b19 100644 --- a/lib/core/l10n/locale_provider.dart +++ b/lib/core/l10n/locale_provider.dart @@ -1,5 +1,4 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import '../../features/dashboard/provider.dart'; import 'app_strings.dart'; diff --git a/lib/features/add_transaction/screen.dart b/lib/features/add_transaction/screen.dart index 2e2def4..020fa8c 100644 --- a/lib/features/add_transaction/screen.dart +++ b/lib/features/add_transaction/screen.dart @@ -658,7 +658,7 @@ class _TypeOption extends StatelessWidget { } } -class _CategoryPicker extends StatelessWidget { +class _CategoryPicker extends ConsumerWidget { final List categories; final String selected; final ValueChanged onChanged; @@ -669,7 +669,8 @@ class _CategoryPicker extends StatelessWidget { }); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final s = ref.watch(stringsProvider); final isDark = Theme.of(context).brightness == Brightness.dark; return Wrap( spacing: 8, @@ -696,7 +697,7 @@ class _CategoryPicker extends StatelessWidget { Icon(icon, color: isSelected ? color : Theme.of(context).colorScheme.onSurface.withOpacity(0.6), size: 16), const SizedBox(width: 6), Text( - cat, + s.categoryLabel(cat), 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 3fd9f48..0214ab5 100644 --- a/lib/features/categories/screen.dart +++ b/lib/features/categories/screen.dart @@ -1,7 +1,6 @@ import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; import '../../core/constants.dart'; import '../../core/l10n/locale_provider.dart'; @@ -199,6 +198,7 @@ class _PieChartCard extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final s = ref.watch(stringsProvider); final fmt = ref.watch(amountFormatProvider); final entries = data.entries.toList(); @@ -255,7 +255,7 @@ class _PieChartCard extends ConsumerWidget { mainAxisSize: MainAxisSize.min, children: [ Text( - 'Total', + s.total, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), ), @@ -285,6 +285,7 @@ class _BarChartCard extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final s = ref.watch(stringsProvider); final fmt = ref.watch(amountFormatProvider); final maxY = monthlyData.map((e) => e.amount).reduce((a, b) => a > b ? a : b); final adjustedMaxY = maxY * 1.2; @@ -299,7 +300,7 @@ class _BarChartCard extends ConsumerWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Last 6 Months', + s.lastSixMonths, style: Theme.of(context).textTheme.titleSmall?.copyWith( color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), fontWeight: FontWeight.w600, @@ -497,11 +498,12 @@ class _CategoryRow extends ConsumerWidget { } } -class _EmptyState extends StatelessWidget { +class _EmptyState extends ConsumerWidget { const _EmptyState(); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final s = ref.watch(stringsProvider); return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, @@ -520,7 +522,7 @@ class _EmptyState extends StatelessWidget { ), const SizedBox(height: 16), Text( - 'No expense data', + s.noExpenseData, style: Theme.of(context).textTheme.titleMedium?.copyWith( color: Theme.of(context).colorScheme.onSurface, fontWeight: FontWeight.w600, @@ -528,7 +530,7 @@ class _EmptyState extends StatelessWidget { ), const SizedBox(height: 6), Text( - 'Add some expenses to see the breakdown', + s.addExpensesToSeeBreakdown, style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), ), diff --git a/lib/features/dashboard/screen.dart b/lib/features/dashboard/screen.dart index 03a2d7b..8e496db 100644 --- a/lib/features/dashboard/screen.dart +++ b/lib/features/dashboard/screen.dart @@ -133,7 +133,7 @@ class _DashboardScreenState extends ConsumerState { scrolledUnderElevation: 0, titleSpacing: 20, title: Text( - s.appTitle, + 'Casha', style: Theme.of(context).textTheme.headlineMedium?.copyWith( fontWeight: FontWeight.w800, color: Theme.of(context).colorScheme.onSurface, @@ -144,12 +144,18 @@ class _DashboardScreenState extends ConsumerState { Padding( padding: const EdgeInsets.only(right: 20), child: Center( - child: Text( - DateFormat('MMMM yyyy', s.dateLocale).format(DateTime.now()), - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.5), - fontWeight: FontWeight.w500, - ), + child: Builder( + builder: (context) { + final raw = DateFormat('LLLL, yyyy', s.dateLocale).format(DateTime.now()); + final capitalized = raw.isNotEmpty ? '${raw[0].toUpperCase()}${raw.substring(1)}' : raw; + return Text( + capitalized, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.5), + fontWeight: FontWeight.w500, + ), + ); + }, ), ), ), diff --git a/lib/features/dashboard/widgets/balance_card.dart b/lib/features/dashboard/widgets/balance_card.dart index a89eb0d..96edb22 100644 --- a/lib/features/dashboard/widgets/balance_card.dart +++ b/lib/features/dashboard/widgets/balance_card.dart @@ -3,10 +3,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:sensors_plus/sensors_plus.dart'; import '../../../core/constants.dart'; +import '../../../core/l10n/locale_provider.dart'; import '../../../core/services/card_color_service.dart'; import '../../../core/services/haptic_service.dart'; import '../../../shared/providers/amount_format_provider.dart'; -import '../../../shared/utils/currency_utils.dart'; import '../../settings/provider.dart'; import '../provider.dart'; @@ -129,6 +129,7 @@ class BalanceCardState extends ConsumerState @override Widget build(BuildContext context) { + final s = ref.watch(stringsProvider); final rates = ref.read(exchangeRateServiceProvider); final fmt = ref.watch(amountFormatProvider); final savedColors = ref.watch(cardColorsProvider); @@ -196,7 +197,7 @@ class BalanceCardState extends ConsumerState crossAxisAlignment: CrossAxisAlignment.center, children: [ Text( - 'TOTAL BALANCE', + s.totalBalance, style: TextStyle( fontSize: 11, letterSpacing: 1.5, @@ -271,7 +272,7 @@ class BalanceCardState extends ConsumerState left: 0, right: 0, child: Text( - 'tap and hold to edit', + s.tapAndHoldToEdit, textAlign: TextAlign.center, style: TextStyle( fontSize: 9, diff --git a/lib/features/dashboard/widgets/budget_progress.dart b/lib/features/dashboard/widgets/budget_progress.dart index e73abed..c9e38f2 100644 --- a/lib/features/dashboard/widgets/budget_progress.dart +++ b/lib/features/dashboard/widgets/budget_progress.dart @@ -4,7 +4,6 @@ import '../../../core/l10n/app_strings.dart'; import '../../../shared/providers/amount_format_provider.dart'; import '../../../shared/utils/currency_utils.dart'; import '../../settings/provider.dart'; -import '../provider.dart'; class BudgetProgress extends ConsumerWidget { final double spent; diff --git a/lib/features/dashboard/widgets/color_editor_overlay.dart b/lib/features/dashboard/widgets/color_editor_overlay.dart index 8bf5943..c0c87cf 100644 --- a/lib/features/dashboard/widgets/color_editor_overlay.dart +++ b/lib/features/dashboard/widgets/color_editor_overlay.dart @@ -5,7 +5,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../core/l10n/app_strings.dart'; import '../../../core/l10n/locale_provider.dart'; import '../../../core/services/card_color_service.dart'; -import '../../../core/services/haptic_service.dart'; import '../../settings/provider.dart'; import '../provider.dart'; import 'balance_card.dart'; diff --git a/lib/features/dashboard/widgets/summary_row.dart b/lib/features/dashboard/widgets/summary_row.dart index c4d66b7..4d479ef 100644 --- a/lib/features/dashboard/widgets/summary_row.dart +++ b/lib/features/dashboard/widgets/summary_row.dart @@ -5,7 +5,6 @@ import '../../../core/l10n/app_strings.dart'; import '../../../shared/providers/amount_format_provider.dart'; import '../../../shared/utils/currency_utils.dart'; import '../../settings/provider.dart'; -import '../provider.dart'; class SummaryRow extends StatelessWidget { final double income; diff --git a/lib/features/settings/provider.dart b/lib/features/settings/provider.dart index f3fdcec..bd49d38 100644 --- a/lib/features/settings/provider.dart +++ b/lib/features/settings/provider.dart @@ -5,7 +5,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:path_provider/path_provider.dart'; import 'package:intl/intl.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import '../../core/constants.dart'; import '../../core/services/haptic_service.dart'; import '../../shared/services/exchange_rate_service.dart'; import '../../shared/utils/currency_utils.dart'; @@ -48,52 +47,57 @@ const Map currencyMap = { }; class CurrencyNotifier extends StateNotifier { - CurrencyNotifier() : super(currencyMap['USD']!) { + final SharedPreferences _prefs; + + CurrencyNotifier(this._prefs) : super(currencyMap['USD']!) { _load(); } - void _load() async { - final prefs = await SharedPreferences.getInstance(); - final code = prefs.getString('currency_code') ?? 'USD'; + void _load() { + final code = _prefs.getString('currency_code') ?? 'USD'; state = currencyMap[code] ?? currencyMap['USD']!; } Future setCurrency(String code) async { - final prefs = await SharedPreferences.getInstance(); state = currencyMap[code] ?? currencyMap['USD']!; - await prefs.setString('currency_code', code); + await _prefs.setString('currency_code', code); } } final currencyProvider = StateNotifierProvider( - (ref) => CurrencyNotifier(), + (ref) { + final prefs = ref.watch(sharedPreferencesProvider); + return CurrencyNotifier(prefs); + }, ); class ThemeModeNotifier extends StateNotifier { - ThemeModeNotifier() : super(ThemeMode.dark) { + final SharedPreferences _prefs; + + ThemeModeNotifier(this._prefs) : super(ThemeMode.dark) { _load(); } - void _load() async { - final prefs = await SharedPreferences.getInstance(); - state = (prefs.getBool('dark_mode') ?? true) ? ThemeMode.dark : ThemeMode.light; + void _load() { + state = (_prefs.getBool('dark_mode') ?? true) ? ThemeMode.dark : ThemeMode.light; } Future toggle() async { - final prefs = await SharedPreferences.getInstance(); state = state == ThemeMode.dark ? ThemeMode.light : ThemeMode.dark; - await prefs.setBool('dark_mode', state == ThemeMode.dark); + await _prefs.setBool('dark_mode', state == ThemeMode.dark); } Future setThemeMode(bool isDark) async { - final prefs = await SharedPreferences.getInstance(); state = isDark ? ThemeMode.dark : ThemeMode.light; - await prefs.setBool('dark_mode', isDark); + await _prefs.setBool('dark_mode', isDark); } } final themeProvider = StateNotifierProvider( - (ref) => ThemeModeNotifier(), + (ref) { + final prefs = ref.watch(sharedPreferencesProvider); + return ThemeModeNotifier(prefs); + }, ); final exchangeRateServiceProvider = Provider((ref) { diff --git a/lib/features/settings/screen.dart b/lib/features/settings/screen.dart index c2ead26..97743b0 100644 --- a/lib/features/settings/screen.dart +++ b/lib/features/settings/screen.dart @@ -1,101 +1,84 @@ import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:go_router/go_router.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/biometric_service.dart'; import '../../core/services/haptic_service.dart'; -import '../../shared/utils/currency_utils.dart'; -import '../../shared/providers/amount_format_provider.dart'; import '../dashboard/provider.dart'; import 'provider.dart'; +import 'widgets/theme_section.dart'; +import 'widgets/haptic_section.dart'; +import 'widgets/language_section.dart'; +import 'widgets/currency_section.dart'; +import 'widgets/amount_format_section.dart'; +import 'widgets/budget_section.dart'; -class SettingsScreen extends ConsumerStatefulWidget { +class SettingsScreen extends ConsumerWidget { const SettingsScreen({super.key}); - @override - ConsumerState createState() => _SettingsScreenState(); -} - -class _SettingsScreenState extends ConsumerState { - final _budgetController = TextEditingController(); - bool _isEditing = false; - late NumberFormat _currencyFmt; - - @override - void initState() { - super.initState(); - final currencyInfo = ref.read(currencyProvider); - _currencyFmt = NumberFormat.currency(symbol: currencyInfo.symbol, decimalDigits: 2); - final budget = ref.read(budgetProvider); - if (budget != null) { - _budgetController.text = budget.toStringAsFixed(2); - } - } - - @override - void dispose() { - _budgetController.dispose(); - super.dispose(); - } - - Future _saveBudget() async { - final text = _budgetController.text.trim(); - if (text.isEmpty) { - await ref.read(budgetProvider.notifier).setBudget(null); - } else { - final value = double.tryParse(text); - if (value != null && value > 0) { - await ref.read(budgetProvider.notifier).setBudget(value); - } - } - setState(() => _isEditing = false); - } - void _confirmClearData(BuildContext context, WidgetRef ref) { final s = ref.read(stringsProvider); + showDialog( context: context, + builder: (ctx) => AlertDialog( title: Text(s.clearDataConfirm), + content: Text(s.clearDataWarning), + actions: [ TextButton( onPressed: () => Navigator.pop(ctx), + child: Text(s.cancel), ), + TextButton( onPressed: () { Navigator.pop(ctx); + showDialog( context: context, + builder: (ctx2) => AlertDialog( title: Text(s.areYouSure), + content: Text(s.allTransactionsWillBeDeleted), + actions: [ TextButton( onPressed: () => Navigator.pop(ctx2), + child: Text(s.noKeepThem), ), + TextButton( onPressed: () { ref.read(transactionsProvider.notifier).clearAll(); + Navigator.pop(ctx2); + ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(s.allTransactionsDeleted)), ); }, - style: TextButton.styleFrom(foregroundColor: const Color(0xFFE05C6B)), + + style: TextButton.styleFrom( + foregroundColor: const Color(0xFFE05C6B), + ), + child: Text(s.yesDeleteEverything), ), ], ), ); }, - style: TextButton.styleFrom(foregroundColor: const Color(0xFFE05C6B)), + + style: TextButton.styleFrom( + foregroundColor: const Color(0xFFE05C6B), + ), + child: Text(s.delete), ), ], @@ -104,624 +87,169 @@ class _SettingsScreenState extends ConsumerState { } @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { final s = ref.watch(stringsProvider); - final budget = ref.watch(budgetProvider); - final themeMode = ref.watch(themeProvider); - final isDarkMode = themeMode == ThemeMode.dark; - final currencyInfo = ref.watch(currencyProvider); - final fmt = ref.watch(amountFormatProvider); - final isDark = Theme.of(context).brightness == Brightness.dark; - - _currencyFmt = NumberFormat.currency(symbol: currencyInfo.symbol, decimalDigits: 2); return Scaffold( backgroundColor: Theme.of(context).scaffoldBackgroundColor, + appBar: AppBar( title: Column( crossAxisAlignment: CrossAxisAlignment.start, + children: [ Text( s.settings, + style: Theme.of(context).textTheme.titleLarge?.copyWith( - fontWeight: FontWeight.w700, - color: Theme.of(context).colorScheme.onSurface, - ), + fontWeight: FontWeight.w700, + + color: Theme.of(context).colorScheme.onSurface, + ), ), + Text( s.managePreferences, + style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), - ), + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), + ), ), ], ), ), - body: SafeArea( - child: ListView( - padding: const EdgeInsets.fromLTRB(20, 16, 20, 20), + + body: ListView( + physics: const ClampingScrollPhysics(), + + padding: const EdgeInsets.fromLTRB(20, 16, 20, 40), + + children: [ + const ThemeSection(), + + const SizedBox(height: 16), + + const HapticSection(), + + const SizedBox(height: 16), + + const _BiometricSection(), + + const LanguageSection(), + + const SizedBox(height: 16), + + const CurrencySection(), + + const SizedBox(height: 16), + + const AmountFormatSection(), + + const SizedBox(height: 16), + + const BudgetSection(), + + const SizedBox(height: 24), + + Text( + s.dangerZone, + + style: TextStyle( + fontSize: 12, + + letterSpacing: 1.2, + + color: const Color(0xFFE05C6B).withOpacity(0.8), + + fontWeight: FontWeight.w600, + ), + ), + + const SizedBox(height: 8), + + SizedBox( + width: double.infinity, + + child: OutlinedButton.icon( + onPressed: () => _confirmClearData(context, ref), + + icon: const Icon(Icons.delete_forever, color: Color(0xFFE05C6B)), + + label: Text( + s.clearAllTransactions, + + style: const TextStyle(color: Color(0xFFE05C6B)), + ), + + style: OutlinedButton.styleFrom( + side: BorderSide( + color: const Color(0xFFE05C6B).withOpacity(0.5), + ), + + padding: const EdgeInsets.symmetric(vertical: 14), + + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + ), + ), + + const SizedBox(height: 32), + + const _FooterWidget(), + ], + ), + ); + } +} + +class _FooterWidget extends StatelessWidget { + const _FooterWidget(); + + @override + Widget build(BuildContext context) { + final isDark = Theme.of(context).brightness == Brightness.dark; + + final baseColor = isDark + ? Colors.white.withOpacity(0.25) + : Colors.black.withOpacity(0.25); + + final emphasisColor = isDark + ? Colors.white.withOpacity(0.35) + : Colors.black.withOpacity(0.35); + + return Center( + child: RichText( + textAlign: TextAlign.center, + + text: TextSpan( + style: TextStyle(fontSize: 13, color: baseColor), + children: [ + TextSpan( + text: 'casha', - Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: BorderRadius.circular(16), - border: isDark ? null : Border.all(color: const Color(0xFFDDDDEE), width: 1), - ), - child: Row( - children: [ - Container( - padding: const EdgeInsets.all(10), - decoration: BoxDecoration( - color: AppColors.accent.withOpacity(0.15), - borderRadius: BorderRadius.circular(12), - ), - child: Icon( - isDarkMode ? Icons.dark_mode_rounded : Icons.light_mode_rounded, - color: AppColors.accent, - size: 20, - ), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - s.darkMode, - style: Theme.of(context).textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w600, - color: Theme.of(context).colorScheme.onSurface, - ), - ), - Text( - isDarkMode ? s.enabled : s.disabled, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), - ), - ), - ], - ), - ), - Switch( - value: isDarkMode, - onChanged: (value) { - ref.read(themeProvider.notifier).setThemeMode(value); - }, - activeColor: AppColors.accent, - ), - ], - ), - ), - const SizedBox(height: 16), - - Consumer( - builder: (context, ref, _) { - final enabled = ref.watch(hapticEnabledProvider); - final isDark = Theme.of(context).brightness == Brightness.dark; - return Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: BorderRadius.circular(16), - border: isDark ? null : Border.all(color: const Color(0xFFDDDDEE), width: 1), - ), - child: Row( - children: [ - Container( - padding: const EdgeInsets.all(10), - decoration: BoxDecoration( - color: AppColors.accent.withOpacity(0.15), - borderRadius: BorderRadius.circular(12), - ), - child: const Icon( - Icons.vibration_rounded, - color: AppColors.accent, - size: 20, - ), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - s.hapticFeedback, - style: Theme.of(context).textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w600, - color: Theme.of(context).colorScheme.onSurface, - ), - ), - Text( - s.vibrationOnInteractions, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), - ), - ), - ], - ), - ), - Switch( - value: enabled, - onChanged: (val) => ref.read(hapticEnabledProvider.notifier).toggle(val), - activeColor: const Color(0xFF7C6DED), - ), - ], - ), - ); - }, - ), - const SizedBox(height: 16), - - const _BiometricSection(), - - Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: BorderRadius.circular(16), - border: isDark ? null : Border.all(color: const Color(0xFFDDDDEE), width: 1), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Container( - padding: const EdgeInsets.all(10), - decoration: BoxDecoration( - color: AppColors.accent.withOpacity(0.15), - borderRadius: BorderRadius.circular(12), - ), - child: const Icon( - Icons.language_rounded, - color: AppColors.accent, - size: 20, - ), - ), - const SizedBox(width: 12), - Expanded( - child: Text( - s.language, - style: Theme.of(context).textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w600, - color: Theme.of(context).colorScheme.onSurface, - ), - ), - ), - ], - ), - const SizedBox(height: 16), - Consumer( - builder: (context, ref, _) { - final currentLocale = ref.watch(localeProvider); - return Row( - children: [ - Expanded( - child: GestureDetector( - onTap: () => ref.read(localeProvider.notifier).setLocale(AppLocale.en), - child: Container( - padding: const EdgeInsets.symmetric(vertical: 12), - decoration: BoxDecoration( - color: currentLocale == AppLocale.en - ? AppColors.accent.withOpacity(0.2) - : Theme.of(context).scaffoldBackgroundColor, - borderRadius: BorderRadius.circular(12), - border: currentLocale == AppLocale.en - ? Border.all(color: AppColors.accent, width: 1.5) - : (isDark ? null : Border.all(color: const Color(0xFFDDDDEE), width: 1)), - ), - child: Text( - s.langEn, - textAlign: TextAlign.center, - style: TextStyle( - color: currentLocale == AppLocale.en - ? AppColors.accent - : Theme.of(context).colorScheme.onSurface.withOpacity(0.6), - fontWeight: currentLocale == AppLocale.en ? FontWeight.w600 : FontWeight.normal, - ), - ), - ), - ), - ), - const SizedBox(width: 8), - Expanded( - child: GestureDetector( - onTap: () => ref.read(localeProvider.notifier).setLocale(AppLocale.ru), - child: Container( - padding: const EdgeInsets.symmetric(vertical: 12), - decoration: BoxDecoration( - color: currentLocale == AppLocale.ru - ? AppColors.accent.withOpacity(0.2) - : Theme.of(context).scaffoldBackgroundColor, - borderRadius: BorderRadius.circular(12), - border: currentLocale == AppLocale.ru - ? Border.all(color: AppColors.accent, width: 1.5) - : (isDark ? null : Border.all(color: const Color(0xFFDDDDEE), width: 1)), - ), - child: Text( - s.langRu, - textAlign: TextAlign.center, - style: TextStyle( - color: currentLocale == AppLocale.ru - ? AppColors.accent - : Theme.of(context).colorScheme.onSurface.withOpacity(0.6), - fontWeight: currentLocale == AppLocale.ru ? FontWeight.w600 : FontWeight.normal, - ), - ), - ), - ), - ), - ], - ); - }, - ), - ], - ), - ), - const SizedBox(height: 16), - - Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: BorderRadius.circular(16), - border: isDark ? null : Border.all(color: const Color(0xFFDDDDEE), width: 1), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Container( - padding: const EdgeInsets.all(10), - decoration: BoxDecoration( - color: AppColors.accent.withOpacity(0.15), - borderRadius: BorderRadius.circular(12), - ), - child: const Icon( - Icons.attach_money_rounded, - color: AppColors.accent, - size: 20, - ), - ), - const SizedBox(width: 12), - Expanded( - child: Text( - s.currency, - style: Theme.of(context).textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w600, - color: Theme.of(context).colorScheme.onSurface, - ), - ), - ), - ], - ), - const SizedBox(height: 16), - Row( - children: ['USD', 'EUR', 'BYN', 'RUB'].map((code) { - final info = currencyMap[code]!; - final isSelected = currencyInfo.code == code; - return Expanded( - child: Padding( - padding: const EdgeInsets.only(right: 8), - child: GestureDetector( - onTap: () { - final oldCode = ref.read(currencyProvider).code; - final rates = ref.read(exchangeRateServiceProvider); - ref.read(budgetProvider.notifier).onCurrencyChanged(oldCode, code, rates); - ref.read(currencyProvider.notifier).setCurrency(code); - }, - child: Container( - padding: const EdgeInsets.symmetric(vertical: 12), - decoration: BoxDecoration( - color: isSelected - ? AppColors.accent.withOpacity(0.2) - : Theme.of(context).scaffoldBackgroundColor, - borderRadius: BorderRadius.circular(12), - border: isSelected - ? Border.all(color: AppColors.accent, width: 1.5) - : (isDark ? null : Border.all(color: const Color(0xFFDDDDEE), width: 1)), - ), - child: Column( - children: [ - Text( - info.symbol, - style: Theme.of(context).textTheme.titleLarge?.copyWith( - color: isSelected ? AppColors.accent : Theme.of(context).colorScheme.onSurface.withOpacity(0.6), - fontWeight: isSelected ? FontWeight.w700 : FontWeight.normal, - ), - ), - const SizedBox(height: 2), - Text( - code, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: isSelected ? AppColors.accent : Theme.of(context).colorScheme.onSurface.withOpacity(0.6), - fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, - ), - ), - ], - ), - ), - ), - ), - ); - }).toList(), - ), - ], - ), - ), - - const SizedBox(height: 16), - - Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: BorderRadius.circular(16), - border: isDark ? null : Border.all(color: const Color(0xFFDDDDEE), width: 1), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Container( - padding: const EdgeInsets.all(10), - decoration: BoxDecoration( - color: AppColors.accent.withOpacity(0.15), - borderRadius: BorderRadius.circular(12), - ), - child: const Icon( - Icons.format_list_numbered_rounded, - color: AppColors.accent, - size: 20, - ), - ), - const SizedBox(width: 12), - Expanded( - child: Text( - s.amountFormat, - style: Theme.of(context).textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w600, - color: Theme.of(context).colorScheme.onSurface, - ), - ), - ), - ], - ), - const SizedBox(height: 16), - ...AmountFormat.values.map((format) { - final isSelected = fmt == format; - return Padding( - padding: const EdgeInsets.only(bottom: 8), - child: GestureDetector( - onTap: () => ref.read(amountFormatProvider.notifier).set(format), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - decoration: BoxDecoration( - color: isSelected - ? AppColors.accent.withOpacity(0.2) - : Theme.of(context).scaffoldBackgroundColor, - borderRadius: BorderRadius.circular(12), - border: isSelected - ? Border.all(color: AppColors.accent, width: 1.5) - : (isDark ? null : Border.all(color: const Color(0xFFDDDDEE), width: 1)), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - format.label, - style: TextStyle( - color: isSelected ? AppColors.accent : Theme.of(context).colorScheme.onSurface, - fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500, - ), - ), - Text( - format.example.replaceFirst('SYM', currencyInfo.symbol), - style: TextStyle( - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), - fontSize: 12, - ), - ), - ], - ), - ), - ), - ); - }).toList(), - ], - ), - ), - - const SizedBox(height: 16), - - Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: BorderRadius.circular(16), - border: isDark ? null : Border.all(color: const Color(0xFFDDDDEE), width: 1), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Container( - padding: const EdgeInsets.all(10), - decoration: BoxDecoration( - color: AppColors.accent.withOpacity(0.15), - borderRadius: BorderRadius.circular(12), - ), - child: const Icon( - Icons.account_balance_wallet_rounded, - color: AppColors.accent, - size: 20, - ), - ), - const SizedBox(width: 12), - Expanded( - child: Text( - s.monthlyBudgetSetting, - style: Theme.of(context).textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w600, - color: Theme.of(context).colorScheme.onSurface, - ), - ), - ), - if (!_isEditing) - IconButton( - icon: const Icon(Icons.edit_rounded, size: 20), - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), - onPressed: () => setState(() => _isEditing = true), - ), - ], - ), - const SizedBox(height: 16), - if (_isEditing) - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - TextFormField( - controller: _budgetController, - keyboardType: const TextInputType.numberWithOptions(decimal: true), - inputFormatters: [ - FilteringTextInputFormatter.allow(RegExp(r'^\d+\.?\d{0,2}')), - ], - decoration: InputDecoration( - prefixText: currencyInfo.symbol == 'Br' || currencyInfo.symbol == '₽' - ? '${currencyInfo.symbol} ' - : currencyInfo.symbol, - hintText: '0.00', - helperText: s.leaveEmptyToRemove, - ), - autofocus: true, - ), - const SizedBox(height: 12), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton( - onPressed: () { - final budget = ref.read(budgetProvider); - _budgetController.text = budget?.toStringAsFixed(2) ?? ''; - setState(() => _isEditing = false); - }, - child: Text(s.cancel), - ), - const SizedBox(width: 8), - ElevatedButton( - onPressed: _saveBudget, - style: ElevatedButton.styleFrom( - minimumSize: const Size(80, 40), - ), - child: Text(s.save), - ), - ], - ), - ], - ) - else - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - budget != null - ? formatAmount(currencyInfo.symbol, budget, fmt) - : s.budgetNone, - style: Theme.of(context).textTheme.headlineSmall?.copyWith( - color: budget != null ? AppColors.accent : Theme.of(context).colorScheme.onSurface.withOpacity(0.6), - fontWeight: FontWeight.w700, - ), - ), - const SizedBox(height: 8), - Text( - budget != null - ? s.yourMonthlySpendingLimit - : s.setMonthlySpendingLimit, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), - ), - ), - ], - ), - ], - ), - ), - - const SizedBox(height: 24), - - Text( - s.dangerZone, style: TextStyle( - fontSize: 12, - letterSpacing: 1.2, - color: const Color(0xFFE05C6B).withOpacity(0.8), - fontWeight: FontWeight.w600, + fontWeight: FontWeight.bold, + + color: emphasisColor, ), ), - const SizedBox(height: 8), - SizedBox( - width: double.infinity, - child: OutlinedButton.icon( - onPressed: () => _confirmClearData(context, ref), - icon: const Icon(Icons.delete_forever, color: Color(0xFFE05C6B)), - label: Text( - s.clearAllTransactions, - style: const TextStyle(color: Color(0xFFE05C6B)), - ), - style: OutlinedButton.styleFrom( - side: BorderSide(color: const Color(0xFFE05C6B).withOpacity(0.5)), - padding: const EdgeInsets.symmetric(vertical: 14), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - ), - ), + + const TextSpan( + text: ' powered with ❤️ by ', + + style: TextStyle(fontStyle: FontStyle.italic), ), - const SizedBox(height: 32), - Padding( - padding: const EdgeInsets.only(bottom: 24), - child: Center( - child: Builder( - builder: (context) { - final isDark = Theme.of(context).brightness == Brightness.dark; - final baseColor = isDark - ? Colors.white.withOpacity(0.25) - : Colors.black.withOpacity(0.25); - final emphasisColor = isDark - ? Colors.white.withOpacity(0.35) - : Colors.black.withOpacity(0.35); - - return RichText( - textAlign: TextAlign.center, - text: TextSpan( - style: TextStyle(fontSize: 13, color: baseColor), - children: [ - TextSpan( - text: 'casha', - style: TextStyle( - fontWeight: FontWeight.bold, - color: emphasisColor, - ), - ), - TextSpan( - text: ' powered with ❤️ by ', - style: TextStyle(fontStyle: FontStyle.italic), - ), - TextSpan( - text: 'kolo', - style: TextStyle( - fontWeight: FontWeight.bold, - color: emphasisColor, - ), - ), - ], - ), - ); - }, - ), + + TextSpan( + text: 'kolo', + + style: TextStyle( + fontWeight: FontWeight.bold, + + color: emphasisColor, ), ), ], @@ -740,22 +268,29 @@ class _BiometricSection extends ConsumerStatefulWidget { class _BiometricSectionState extends ConsumerState<_BiometricSection> { bool _available = false; + bool _enabled = false; + bool _loading = true; @override void initState() { super.initState(); + _load(); } Future _load() async { final available = await BiometricService.isAvailable(); + final enabled = await BiometricService.isEnabled(); + if (mounted) { setState(() { _available = available; + _enabled = enabled; + _loading = false; }); } @@ -764,79 +299,103 @@ class _BiometricSectionState extends ConsumerState<_BiometricSection> { Future _onToggle(bool val) async { if (val) { final ok = await BiometricService.authenticate(); + if (!ok) return; + HapticService.light(); } + await BiometricService.setEnabled(val); + if (mounted) setState(() => _enabled = val); } @override Widget build(BuildContext context) { if (_loading || !_available) return const SizedBox.shrink(); - + + final s = ref.watch(stringsProvider); + final isDark = Theme.of(context).brightness == Brightness.dark; - - return Consumer( - builder: (context, ref, _) { - final s = ref.watch(stringsProvider); - - return Column( - children: [ - Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: BorderRadius.circular(16), - border: isDark ? null : Border.all(color: const Color(0xFFDDDDEE), width: 1), + + return Column( + children: [ + Container( + padding: const EdgeInsets.all(20), + + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + + borderRadius: BorderRadius.circular(16), + + border: isDark + ? null + : Border.all(color: const Color(0xFFDDDDEE), width: 1), + ), + + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(10), + + decoration: BoxDecoration( + color: AppColors.accent.withOpacity(0.15), + + borderRadius: BorderRadius.circular(12), + ), + + child: const Icon( + Icons.fingerprint, + + color: AppColors.accent, + + size: 20, + ), ), - child: Row( - children: [ - Container( - padding: const EdgeInsets.all(10), - decoration: BoxDecoration( - color: AppColors.accent.withOpacity(0.15), - borderRadius: BorderRadius.circular(12), + + const SizedBox(width: 12), + + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + + children: [ + Text( + s.biometricLock, + + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + + color: Theme.of(context).colorScheme.onSurface, + ), ), - child: const Icon( - Icons.fingerprint, - color: AppColors.accent, - size: 20, + + Text( + s.requireFingerprint, + + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of( + context, + ).colorScheme.onSurface.withOpacity(0.6), + ), ), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - s.biometricLock, - style: Theme.of(context).textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w600, - color: Theme.of(context).colorScheme.onSurface, - ), - ), - Text( - s.requireFingerprint, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), - ), - ), - ], - ), - ), - Switch( - value: _enabled, - onChanged: _onToggle, - activeColor: const Color(0xFF7C6DED), - ), - ], + ], + ), ), - ), - const SizedBox(height: 16), - ], - ); - }, + + Switch( + value: _enabled, + + onChanged: _onToggle, + + activeThumbColor: const Color(0xFF7C6DED), + ), + ], + ), + ), + + const SizedBox(height: 16), + ], ); } } diff --git a/lib/features/settings/widgets/amount_format_section.dart b/lib/features/settings/widgets/amount_format_section.dart new file mode 100644 index 0000000..c844226 --- /dev/null +++ b/lib/features/settings/widgets/amount_format_section.dart @@ -0,0 +1,99 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../../core/constants.dart'; +import '../../../core/l10n/locale_provider.dart'; +import '../../../shared/providers/amount_format_provider.dart'; +import '../provider.dart'; + +class AmountFormatSection extends ConsumerWidget { + const AmountFormatSection({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final s = ref.watch(stringsProvider); + final fmt = ref.watch(amountFormatProvider); + final currencyInfo = ref.watch(currencyProvider); + final isDark = Theme.of(context).brightness == Brightness.dark; + + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(16), + border: isDark ? null : Border.all(color: const Color(0xFFDDDDEE), width: 1), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: AppColors.accent.withOpacity(0.15), + borderRadius: BorderRadius.circular(12), + ), + child: const Icon( + Icons.format_list_numbered_rounded, + color: AppColors.accent, + size: 20, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + s.amountFormat, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ), + ], + ), + const SizedBox(height: 16), + ...AmountFormat.values.map((format) { + final isSelected = fmt == format; + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: GestureDetector( + onTap: () => ref.read(amountFormatProvider.notifier).set(format), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: isSelected + ? AppColors.accent.withOpacity(0.2) + : Theme.of(context).scaffoldBackgroundColor, + borderRadius: BorderRadius.circular(12), + border: isSelected + ? Border.all(color: AppColors.accent, width: 1.5) + : (isDark ? null : Border.all(color: const Color(0xFFDDDDEE), width: 1)), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + format.label, + style: TextStyle( + color: isSelected ? AppColors.accent : Theme.of(context).colorScheme.onSurface, + fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500, + ), + ), + Text( + format.example.replaceFirst('SYM', currencyInfo.symbol), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), + fontSize: 12, + ), + ), + ], + ), + ), + ), + ); + }), + ], + ), + ); + } +} diff --git a/lib/features/settings/widgets/budget_section.dart b/lib/features/settings/widgets/budget_section.dart new file mode 100644 index 0000000..089b509 --- /dev/null +++ b/lib/features/settings/widgets/budget_section.dart @@ -0,0 +1,171 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../../core/constants.dart'; +import '../../../core/l10n/locale_provider.dart'; +import '../../../shared/providers/amount_format_provider.dart'; +import '../../../shared/utils/currency_utils.dart'; +import '../provider.dart'; + +class BudgetSection extends ConsumerStatefulWidget { + const BudgetSection({super.key}); + + @override + ConsumerState createState() => _BudgetSectionState(); +} + +class _BudgetSectionState extends ConsumerState { + final _budgetController = TextEditingController(); + bool _isEditing = false; + + @override + void initState() { + super.initState(); + final budget = ref.read(budgetProvider); + if (budget != null) { + _budgetController.text = budget.toStringAsFixed(2); + } + } + + @override + void dispose() { + _budgetController.dispose(); + super.dispose(); + } + + Future _saveBudget() async { + final text = _budgetController.text.trim(); + if (text.isEmpty) { + await ref.read(budgetProvider.notifier).setBudget(null); + } else { + final value = double.tryParse(text); + if (value != null && value > 0) { + await ref.read(budgetProvider.notifier).setBudget(value); + } + } + setState(() => _isEditing = false); + } + + @override + Widget build(BuildContext context) { + final s = ref.watch(stringsProvider); + final budget = ref.watch(budgetProvider); + final currencyInfo = ref.watch(currencyProvider); + final fmt = ref.watch(amountFormatProvider); + final isDark = Theme.of(context).brightness == Brightness.dark; + + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(16), + border: isDark ? null : Border.all(color: const Color(0xFFDDDDEE), width: 1), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: AppColors.accent.withOpacity(0.15), + borderRadius: BorderRadius.circular(12), + ), + child: const Icon( + Icons.account_balance_wallet_rounded, + color: AppColors.accent, + size: 20, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + s.monthlyBudgetSetting, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ), + if (!_isEditing) + IconButton( + icon: const Icon(Icons.edit_rounded, size: 20), + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), + onPressed: () => setState(() => _isEditing = true), + ), + ], + ), + const SizedBox(height: 16), + if (_isEditing) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextFormField( + controller: _budgetController, + keyboardType: const TextInputType.numberWithOptions(decimal: true), + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp(r'^\d+\.?\d{0,2}')), + ], + decoration: InputDecoration( + prefixText: currencyInfo.symbol == 'Br' || currencyInfo.symbol == '₽' + ? '${currencyInfo.symbol} ' + : currencyInfo.symbol, + hintText: '0.00', + helperText: s.leaveEmptyToRemove, + ), + autofocus: true, + ), + const SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () { + final budget = ref.read(budgetProvider); + _budgetController.text = budget?.toStringAsFixed(2) ?? ''; + setState(() => _isEditing = false); + }, + child: Text(s.cancel), + ), + const SizedBox(width: 8), + ElevatedButton( + onPressed: _saveBudget, + style: ElevatedButton.styleFrom( + minimumSize: const Size(80, 40), + ), + child: Text(s.save), + ), + ], + ), + ], + ) + else + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + budget != null + ? formatAmount(currencyInfo.symbol, budget, fmt) + : s.budgetNone, + style: Theme.of(context).textTheme.headlineSmall?.copyWith( + color: budget != null ? AppColors.accent : Theme.of(context).colorScheme.onSurface.withOpacity(0.6), + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(height: 8), + Text( + budget != null + ? s.yourMonthlySpendingLimit + : s.setMonthlySpendingLimit, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/features/settings/widgets/currency_section.dart b/lib/features/settings/widgets/currency_section.dart new file mode 100644 index 0000000..ae6ee1b --- /dev/null +++ b/lib/features/settings/widgets/currency_section.dart @@ -0,0 +1,107 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../../core/constants.dart'; +import '../../../core/l10n/locale_provider.dart'; +import '../provider.dart'; + +class CurrencySection extends ConsumerWidget { + const CurrencySection({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final s = ref.watch(stringsProvider); + final currencyInfo = ref.watch(currencyProvider); + final isDark = Theme.of(context).brightness == Brightness.dark; + + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(16), + border: isDark ? null : Border.all(color: const Color(0xFFDDDDEE), width: 1), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: AppColors.accent.withOpacity(0.15), + borderRadius: BorderRadius.circular(12), + ), + child: const Icon( + Icons.attach_money_rounded, + color: AppColors.accent, + size: 20, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + s.currency, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ), + ], + ), + const SizedBox(height: 16), + Row( + children: ['USD', 'EUR', 'BYN', 'RUB'].map((code) { + final info = currencyMap[code]!; + final isSelected = currencyInfo.code == code; + return Expanded( + child: Padding( + padding: const EdgeInsets.only(right: 8), + child: GestureDetector( + onTap: () { + final oldCode = ref.read(currencyProvider).code; + final rates = ref.read(exchangeRateServiceProvider); + ref.read(budgetProvider.notifier).onCurrencyChanged(oldCode, code, rates); + ref.read(currencyProvider.notifier).setCurrency(code); + }, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 12), + decoration: BoxDecoration( + color: isSelected + ? AppColors.accent.withOpacity(0.2) + : Theme.of(context).scaffoldBackgroundColor, + borderRadius: BorderRadius.circular(12), + border: isSelected + ? Border.all(color: AppColors.accent, width: 1.5) + : (isDark ? null : Border.all(color: const Color(0xFFDDDDEE), width: 1)), + ), + child: Column( + children: [ + Text( + info.symbol, + style: Theme.of(context).textTheme.titleLarge?.copyWith( + color: isSelected ? AppColors.accent : Theme.of(context).colorScheme.onSurface.withOpacity(0.6), + fontWeight: isSelected ? FontWeight.w700 : FontWeight.normal, + ), + ), + const SizedBox(height: 2), + Text( + code, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: isSelected ? AppColors.accent : Theme.of(context).colorScheme.onSurface.withOpacity(0.6), + fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, + ), + ), + ], + ), + ), + ), + ), + ); + }).toList(), + ), + ], + ), + ); + } +} diff --git a/lib/features/settings/widgets/haptic_section.dart b/lib/features/settings/widgets/haptic_section.dart new file mode 100644 index 0000000..b57fa32 --- /dev/null +++ b/lib/features/settings/widgets/haptic_section.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../../core/constants.dart'; +import '../../../core/l10n/locale_provider.dart'; +import '../provider.dart'; + +class HapticSection extends ConsumerWidget { + const HapticSection({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final s = ref.watch(stringsProvider); + final enabled = ref.watch(hapticEnabledProvider); + final isDark = Theme.of(context).brightness == Brightness.dark; + + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(16), + border: isDark ? null : Border.all(color: const Color(0xFFDDDDEE), width: 1), + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: AppColors.accent.withOpacity(0.15), + borderRadius: BorderRadius.circular(12), + ), + child: const Icon( + Icons.vibration_rounded, + color: AppColors.accent, + size: 20, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + s.hapticFeedback, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + Text( + s.vibrationOnInteractions, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), + ), + ), + ], + ), + ), + Switch( + value: enabled, + onChanged: (val) => ref.read(hapticEnabledProvider.notifier).toggle(val), + activeThumbColor: const Color(0xFF7C6DED), + ), + ], + ), + ); + } +} diff --git a/lib/features/settings/widgets/language_section.dart b/lib/features/settings/widgets/language_section.dart new file mode 100644 index 0000000..1db8433 --- /dev/null +++ b/lib/features/settings/widgets/language_section.dart @@ -0,0 +1,116 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../../core/constants.dart'; +import '../../../core/l10n/app_strings.dart'; +import '../../../core/l10n/locale_provider.dart'; + +class LanguageSection extends ConsumerWidget { + const LanguageSection({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final s = ref.watch(stringsProvider); + final currentLocale = ref.watch(localeProvider); + final isDark = Theme.of(context).brightness == Brightness.dark; + + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(16), + border: isDark ? null : Border.all(color: const Color(0xFFDDDDEE), width: 1), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: AppColors.accent.withOpacity(0.15), + borderRadius: BorderRadius.circular(12), + ), + child: const Icon( + Icons.language_rounded, + color: AppColors.accent, + size: 20, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + s.language, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ), + ], + ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: GestureDetector( + onTap: () => ref.read(localeProvider.notifier).setLocale(AppLocale.en), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 12), + decoration: BoxDecoration( + color: currentLocale == AppLocale.en + ? AppColors.accent.withOpacity(0.2) + : Theme.of(context).scaffoldBackgroundColor, + borderRadius: BorderRadius.circular(12), + border: currentLocale == AppLocale.en + ? Border.all(color: AppColors.accent, width: 1.5) + : (isDark ? null : Border.all(color: const Color(0xFFDDDDEE), width: 1)), + ), + child: Text( + s.langEn, + textAlign: TextAlign.center, + style: TextStyle( + color: currentLocale == AppLocale.en + ? AppColors.accent + : Theme.of(context).colorScheme.onSurface.withOpacity(0.6), + fontWeight: currentLocale == AppLocale.en ? FontWeight.w600 : FontWeight.normal, + ), + ), + ), + ), + ), + const SizedBox(width: 8), + Expanded( + child: GestureDetector( + onTap: () => ref.read(localeProvider.notifier).setLocale(AppLocale.ru), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 12), + decoration: BoxDecoration( + color: currentLocale == AppLocale.ru + ? AppColors.accent.withOpacity(0.2) + : Theme.of(context).scaffoldBackgroundColor, + borderRadius: BorderRadius.circular(12), + border: currentLocale == AppLocale.ru + ? Border.all(color: AppColors.accent, width: 1.5) + : (isDark ? null : Border.all(color: const Color(0xFFDDDDEE), width: 1)), + ), + child: Text( + s.langRu, + textAlign: TextAlign.center, + style: TextStyle( + color: currentLocale == AppLocale.ru + ? AppColors.accent + : Theme.of(context).colorScheme.onSurface.withOpacity(0.6), + fontWeight: currentLocale == AppLocale.ru ? FontWeight.w600 : FontWeight.normal, + ), + ), + ), + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/features/settings/widgets/theme_section.dart b/lib/features/settings/widgets/theme_section.dart new file mode 100644 index 0000000..5f8d3ab --- /dev/null +++ b/lib/features/settings/widgets/theme_section.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../../core/constants.dart'; +import '../../../core/l10n/locale_provider.dart'; +import '../provider.dart'; + +class ThemeSection extends ConsumerWidget { + const ThemeSection({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final s = ref.watch(stringsProvider); + final themeMode = ref.watch(themeProvider); + final isDarkMode = themeMode == ThemeMode.dark; + final isDark = Theme.of(context).brightness == Brightness.dark; + + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(16), + border: isDark ? null : Border.all(color: const Color(0xFFDDDDEE), width: 1), + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: AppColors.accent.withOpacity(0.15), + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + isDarkMode ? Icons.dark_mode_rounded : Icons.light_mode_rounded, + color: AppColors.accent, + size: 20, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + s.darkMode, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + Text( + isDarkMode ? s.enabled : s.disabled, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), + ), + ), + ], + ), + ), + Switch( + value: isDarkMode, + onChanged: (value) { + ref.read(themeProvider.notifier).setThemeMode(value); + }, + activeThumbColor: AppColors.accent, + ), + ], + ), + ); + } +}