mirror of
https://github.com/koloideal/Casha.git
synced 2026-06-10 10:25:28 +03:00
update
This commit is contained in:
+2
-1
@@ -21,7 +21,7 @@ migrate_working_dir/
|
|||||||
# The .vscode folder contains launch configuration and tasks you configure in
|
# 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
|
# VS Code which you may wish to be included in version control, so this line
|
||||||
# is commented out by default.
|
# is commented out by default.
|
||||||
#.vscode/
|
.vscode/
|
||||||
|
|
||||||
# Flutter/Dart/Pub related
|
# Flutter/Dart/Pub related
|
||||||
**/doc/api/
|
**/doc/api/
|
||||||
@@ -43,3 +43,4 @@ app.*.map.json
|
|||||||
/android/app/debug
|
/android/app/debug
|
||||||
/android/app/profile
|
/android/app/profile
|
||||||
/android/app/release
|
/android/app/release
|
||||||
|
*.jks
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 310 KiB |
+15
-12
@@ -1,5 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
import '../core/l10n/locale_provider.dart';
|
||||||
import '../features/dashboard/screen.dart';
|
import '../features/dashboard/screen.dart';
|
||||||
import '../features/add_transaction/screen.dart';
|
import '../features/add_transaction/screen.dart';
|
||||||
import '../features/categories/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;
|
final Widget child;
|
||||||
const AppShell({super.key, required this.child});
|
const AppShell({super.key, required this.child});
|
||||||
|
|
||||||
@@ -62,7 +64,8 @@ class AppShell extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final s = ref.watch(stringsProvider);
|
||||||
final idx = _locationToIndex(context);
|
final idx = _locationToIndex(context);
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: child,
|
body: child,
|
||||||
@@ -73,21 +76,21 @@ class AppShell extends StatelessWidget {
|
|||||||
if (i == 1) context.go('/categories');
|
if (i == 1) context.go('/categories');
|
||||||
if (i == 2) context.go('/settings');
|
if (i == 2) context.go('/settings');
|
||||||
},
|
},
|
||||||
destinations: const [
|
destinations: [
|
||||||
NavigationDestination(
|
NavigationDestination(
|
||||||
icon: Icon(Icons.dashboard_outlined),
|
icon: const Icon(Icons.dashboard_outlined),
|
||||||
selectedIcon: Icon(Icons.dashboard_rounded),
|
selectedIcon: const Icon(Icons.dashboard_rounded),
|
||||||
label: 'Dashboard',
|
label: s.navDashboard,
|
||||||
),
|
),
|
||||||
NavigationDestination(
|
NavigationDestination(
|
||||||
icon: Icon(Icons.pie_chart_outline_rounded),
|
icon: const Icon(Icons.pie_chart_outline_rounded),
|
||||||
selectedIcon: Icon(Icons.pie_chart_rounded),
|
selectedIcon: const Icon(Icons.pie_chart_rounded),
|
||||||
label: 'Categories',
|
label: s.navCategories,
|
||||||
),
|
),
|
||||||
NavigationDestination(
|
NavigationDestination(
|
||||||
icon: Icon(Icons.settings_outlined),
|
icon: const Icon(Icons.settings_outlined),
|
||||||
selectedIcon: Icon(Icons.settings_rounded),
|
selectedIcon: const Icon(Icons.settings_rounded),
|
||||||
label: 'Settings',
|
label: s.navSettings,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ class AppTheme {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return base.copyWith(
|
return base.copyWith(
|
||||||
fontFamily: GoogleFonts.poppins().fontFamily, // Explicit font family for Cyrillic support
|
|
||||||
textTheme: textTheme,
|
textTheme: textTheme,
|
||||||
scaffoldBackgroundColor: AppColors.background,
|
scaffoldBackgroundColor: AppColors.background,
|
||||||
colorScheme: const ColorScheme.dark(
|
colorScheme: const ColorScheme.dark(
|
||||||
@@ -112,7 +111,6 @@ class AppTheme {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return base.copyWith(
|
return base.copyWith(
|
||||||
fontFamily: GoogleFonts.poppins().fontFamily, // Explicit font family for Cyrillic support
|
|
||||||
textTheme: textTheme,
|
textTheme: textTheme,
|
||||||
scaffoldBackgroundColor: const Color(0xFFF0F0F7),
|
scaffoldBackgroundColor: const Color(0xFFF0F0F7),
|
||||||
colorScheme: const ColorScheme.light(
|
colorScheme: const ColorScheme.light(
|
||||||
@@ -121,8 +119,6 @@ class AppTheme {
|
|||||||
secondary: AppColors.accent,
|
secondary: AppColors.accent,
|
||||||
onPrimary: Colors.white,
|
onPrimary: Colors.white,
|
||||||
onSurface: Color(0xFF1A1A2E),
|
onSurface: Color(0xFF1A1A2E),
|
||||||
onBackground: Color(0xFF1A1A2E),
|
|
||||||
background: Color(0xFFF0F0F7),
|
|
||||||
),
|
),
|
||||||
cardTheme: CardThemeData(
|
cardTheme: CardThemeData(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ class AppStrings {
|
|||||||
// ── Dashboard ──
|
// ── Dashboard ──
|
||||||
String get appTitle => _ru ? 'Мои финансы' : 'My Finances';
|
String get appTitle => _ru ? 'Мои финансы' : 'My Finances';
|
||||||
String get totalBalance => _ru ? 'ОБЩИЙ БАЛАНС' : 'TOTAL BALANCE';
|
String get totalBalance => _ru ? 'ОБЩИЙ БАЛАНС' : 'TOTAL BALANCE';
|
||||||
|
String get tapAndHoldToEdit => _ru ? 'удерживайте для редактирования' : 'tap and hold to edit';
|
||||||
String get add => _ru ? 'Добавить' : 'Add';
|
String get add => _ru ? 'Добавить' : 'Add';
|
||||||
String get transactions => _ru ? 'Транзакции' : 'Transactions';
|
String get transactions => _ru ? 'Транзакции' : 'Transactions';
|
||||||
String get searchHint => _ru ? 'Поиск транзакций...' : 'Search 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 allTransactionsWillBeDeleted => _ru ? 'Все транзакции будут удалены навсегда. Восстановить их будет невозможно.' : 'All transactions will be deleted forever. There is no way to recover them.';
|
||||||
String get dangerZone => _ru ? 'Опасная зона' : 'Danger Zone';
|
String get dangerZone => _ru ? 'Опасная зона' : 'Danger Zone';
|
||||||
|
|
||||||
|
// ── Navigation ──
|
||||||
|
String get navDashboard => _ru ? 'Главная' : 'Dashboard';
|
||||||
|
String get navCategories => _ru ? 'Категории' : 'Categories';
|
||||||
|
String get navSettings => _ru ? 'Настройки' : 'Settings';
|
||||||
|
|
||||||
// ── Categories ──
|
// ── Categories ──
|
||||||
String get categories => _ru ? 'Категории' : 'Categories';
|
String get categories => _ru ? 'Категории' : 'Categories';
|
||||||
String get rankedByAmount => _ru ? 'По сумме' : 'Ranked by Amount';
|
String get rankedByAmount => _ru ? 'По сумме' : 'Ranked by Amount';
|
||||||
@@ -95,6 +101,10 @@ class AppStrings {
|
|||||||
String get categoryColor => _ru ? 'Цвет' : 'Color';
|
String get categoryColor => _ru ? 'Цвет' : 'Color';
|
||||||
String get deleteCategory => _ru ? 'Удалить категорию' : 'Delete Category';
|
String get deleteCategory => _ru ? 'Удалить категорию' : 'Delete Category';
|
||||||
String get noCategoriesYet => _ru ? 'Нет категорий' : 'No categories yet';
|
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) ──
|
// ── Built-in category names (translated for display only, stored in English) ──
|
||||||
String categoryLabel(String key) {
|
String categoryLabel(String key) {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
|
||||||
import '../../features/dashboard/provider.dart';
|
import '../../features/dashboard/provider.dart';
|
||||||
import 'app_strings.dart';
|
import 'app_strings.dart';
|
||||||
|
|
||||||
|
|||||||
@@ -658,7 +658,7 @@ class _TypeOption extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _CategoryPicker extends StatelessWidget {
|
class _CategoryPicker extends ConsumerWidget {
|
||||||
final List<String> categories;
|
final List<String> categories;
|
||||||
final String selected;
|
final String selected;
|
||||||
final ValueChanged<String> onChanged;
|
final ValueChanged<String> onChanged;
|
||||||
@@ -669,7 +669,8 @@ class _CategoryPicker extends StatelessWidget {
|
|||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final s = ref.watch(stringsProvider);
|
||||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||||
return Wrap(
|
return Wrap(
|
||||||
spacing: 8,
|
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),
|
Icon(icon, color: isSelected ? color : Theme.of(context).colorScheme.onSurface.withOpacity(0.6), size: 16),
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
Text(
|
Text(
|
||||||
cat,
|
s.categoryLabel(cat),
|
||||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
color: isSelected ? color : Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
|
color: isSelected ? color : Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
|
||||||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
|
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
|
||||||
|
|||||||
@@ -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:go_router/go_router.dart';
|
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import '../../core/constants.dart';
|
import '../../core/constants.dart';
|
||||||
import '../../core/l10n/locale_provider.dart';
|
import '../../core/l10n/locale_provider.dart';
|
||||||
@@ -199,6 +198,7 @@ class _PieChartCard extends ConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
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();
|
||||||
|
|
||||||
@@ -255,7 +255,7 @@ class _PieChartCard extends ConsumerWidget {
|
|||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Total',
|
s.total,
|
||||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
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),
|
||||||
),
|
),
|
||||||
@@ -285,6 +285,7 @@ class _BarChartCard extends ConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final s = ref.watch(stringsProvider);
|
||||||
final fmt = ref.watch(amountFormatProvider);
|
final fmt = ref.watch(amountFormatProvider);
|
||||||
final maxY = monthlyData.map((e) => e.amount).reduce((a, b) => a > b ? a : b);
|
final maxY = monthlyData.map((e) => e.amount).reduce((a, b) => a > b ? a : b);
|
||||||
final adjustedMaxY = maxY * 1.2;
|
final adjustedMaxY = maxY * 1.2;
|
||||||
@@ -299,7 +300,7 @@ class _BarChartCard extends ConsumerWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Last 6 Months',
|
s.lastSixMonths,
|
||||||
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||||||
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
|
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
@@ -497,11 +498,12 @@ class _CategoryRow extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _EmptyState extends StatelessWidget {
|
class _EmptyState extends ConsumerWidget {
|
||||||
const _EmptyState();
|
const _EmptyState();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final s = ref.watch(stringsProvider);
|
||||||
return Center(
|
return Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
@@ -520,7 +522,7 @@ class _EmptyState extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
'No expense data',
|
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,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
@@ -528,7 +530,7 @@ class _EmptyState extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
Text(
|
Text(
|
||||||
'Add some expenses to see the breakdown',
|
s.addExpensesToSeeBreakdown,
|
||||||
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.6),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
|||||||
scrolledUnderElevation: 0,
|
scrolledUnderElevation: 0,
|
||||||
titleSpacing: 20,
|
titleSpacing: 20,
|
||||||
title: Text(
|
title: Text(
|
||||||
s.appTitle,
|
'Casha',
|
||||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
||||||
fontWeight: FontWeight.w800,
|
fontWeight: FontWeight.w800,
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
@@ -144,12 +144,18 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(right: 20),
|
padding: const EdgeInsets.only(right: 20),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Text(
|
child: Builder(
|
||||||
DateFormat('MMMM yyyy', s.dateLocale).format(DateTime.now()),
|
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(
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.5),
|
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.5),
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:sensors_plus/sensors_plus.dart';
|
import 'package:sensors_plus/sensors_plus.dart';
|
||||||
import '../../../core/constants.dart';
|
import '../../../core/constants.dart';
|
||||||
|
import '../../../core/l10n/locale_provider.dart';
|
||||||
import '../../../core/services/card_color_service.dart';
|
import '../../../core/services/card_color_service.dart';
|
||||||
import '../../../core/services/haptic_service.dart';
|
import '../../../core/services/haptic_service.dart';
|
||||||
import '../../../shared/providers/amount_format_provider.dart';
|
import '../../../shared/providers/amount_format_provider.dart';
|
||||||
import '../../../shared/utils/currency_utils.dart';
|
|
||||||
import '../../settings/provider.dart';
|
import '../../settings/provider.dart';
|
||||||
import '../provider.dart';
|
import '../provider.dart';
|
||||||
|
|
||||||
@@ -129,6 +129,7 @@ class BalanceCardState extends ConsumerState<BalanceCard>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final s = ref.watch(stringsProvider);
|
||||||
final rates = ref.read(exchangeRateServiceProvider);
|
final rates = ref.read(exchangeRateServiceProvider);
|
||||||
final fmt = ref.watch(amountFormatProvider);
|
final fmt = ref.watch(amountFormatProvider);
|
||||||
final savedColors = ref.watch(cardColorsProvider);
|
final savedColors = ref.watch(cardColorsProvider);
|
||||||
@@ -196,7 +197,7 @@ class BalanceCardState extends ConsumerState<BalanceCard>
|
|||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'TOTAL BALANCE',
|
s.totalBalance,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
letterSpacing: 1.5,
|
letterSpacing: 1.5,
|
||||||
@@ -271,7 +272,7 @@ class BalanceCardState extends ConsumerState<BalanceCard>
|
|||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
child: Text(
|
child: Text(
|
||||||
'tap and hold to edit',
|
s.tapAndHoldToEdit,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 9,
|
fontSize: 9,
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import '../../../core/l10n/app_strings.dart';
|
|||||||
import '../../../shared/providers/amount_format_provider.dart';
|
import '../../../shared/providers/amount_format_provider.dart';
|
||||||
import '../../../shared/utils/currency_utils.dart';
|
import '../../../shared/utils/currency_utils.dart';
|
||||||
import '../../settings/provider.dart';
|
import '../../settings/provider.dart';
|
||||||
import '../provider.dart';
|
|
||||||
|
|
||||||
class BudgetProgress extends ConsumerWidget {
|
class BudgetProgress extends ConsumerWidget {
|
||||||
final double spent;
|
final double spent;
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import 'package:flutter_riverpod/flutter_riverpod.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';
|
||||||
import '../../../core/services/card_color_service.dart';
|
import '../../../core/services/card_color_service.dart';
|
||||||
import '../../../core/services/haptic_service.dart';
|
|
||||||
import '../../settings/provider.dart';
|
import '../../settings/provider.dart';
|
||||||
import '../provider.dart';
|
import '../provider.dart';
|
||||||
import 'balance_card.dart';
|
import 'balance_card.dart';
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import '../../../core/l10n/app_strings.dart';
|
|||||||
import '../../../shared/providers/amount_format_provider.dart';
|
import '../../../shared/providers/amount_format_provider.dart';
|
||||||
import '../../../shared/utils/currency_utils.dart';
|
import '../../../shared/utils/currency_utils.dart';
|
||||||
import '../../settings/provider.dart';
|
import '../../settings/provider.dart';
|
||||||
import '../provider.dart';
|
|
||||||
|
|
||||||
class SummaryRow extends StatelessWidget {
|
class SummaryRow extends StatelessWidget {
|
||||||
final double income;
|
final double income;
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import '../../core/constants.dart';
|
|
||||||
import '../../core/services/haptic_service.dart';
|
import '../../core/services/haptic_service.dart';
|
||||||
import '../../shared/services/exchange_rate_service.dart';
|
import '../../shared/services/exchange_rate_service.dart';
|
||||||
import '../../shared/utils/currency_utils.dart';
|
import '../../shared/utils/currency_utils.dart';
|
||||||
@@ -48,52 +47,57 @@ const Map<String, CurrencyInfo> currencyMap = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
class CurrencyNotifier extends StateNotifier<CurrencyInfo> {
|
class CurrencyNotifier extends StateNotifier<CurrencyInfo> {
|
||||||
CurrencyNotifier() : super(currencyMap['USD']!) {
|
final SharedPreferences _prefs;
|
||||||
|
|
||||||
|
CurrencyNotifier(this._prefs) : super(currencyMap['USD']!) {
|
||||||
_load();
|
_load();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _load() async {
|
void _load() {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final code = _prefs.getString('currency_code') ?? 'USD';
|
||||||
final code = prefs.getString('currency_code') ?? 'USD';
|
|
||||||
state = currencyMap[code] ?? currencyMap['USD']!;
|
state = currencyMap[code] ?? currencyMap['USD']!;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> setCurrency(String code) async {
|
Future<void> setCurrency(String code) async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
|
||||||
state = currencyMap[code] ?? currencyMap['USD']!;
|
state = currencyMap[code] ?? currencyMap['USD']!;
|
||||||
await prefs.setString('currency_code', code);
|
await _prefs.setString('currency_code', code);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final currencyProvider = StateNotifierProvider<CurrencyNotifier, CurrencyInfo>(
|
final currencyProvider = StateNotifierProvider<CurrencyNotifier, CurrencyInfo>(
|
||||||
(ref) => CurrencyNotifier(),
|
(ref) {
|
||||||
|
final prefs = ref.watch(sharedPreferencesProvider);
|
||||||
|
return CurrencyNotifier(prefs);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
class ThemeModeNotifier extends StateNotifier<ThemeMode> {
|
class ThemeModeNotifier extends StateNotifier<ThemeMode> {
|
||||||
ThemeModeNotifier() : super(ThemeMode.dark) {
|
final SharedPreferences _prefs;
|
||||||
|
|
||||||
|
ThemeModeNotifier(this._prefs) : super(ThemeMode.dark) {
|
||||||
_load();
|
_load();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _load() async {
|
void _load() {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
state = (_prefs.getBool('dark_mode') ?? true) ? ThemeMode.dark : ThemeMode.light;
|
||||||
state = (prefs.getBool('dark_mode') ?? true) ? ThemeMode.dark : ThemeMode.light;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> toggle() async {
|
Future<void> toggle() async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
|
||||||
state = state == ThemeMode.dark ? ThemeMode.light : ThemeMode.dark;
|
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<void> setThemeMode(bool isDark) async {
|
Future<void> setThemeMode(bool isDark) async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
|
||||||
state = isDark ? ThemeMode.dark : ThemeMode.light;
|
state = isDark ? ThemeMode.dark : ThemeMode.light;
|
||||||
await prefs.setBool('dark_mode', isDark);
|
await _prefs.setBool('dark_mode', isDark);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final themeProvider = StateNotifierProvider<ThemeModeNotifier, ThemeMode>(
|
final themeProvider = StateNotifierProvider<ThemeModeNotifier, ThemeMode>(
|
||||||
(ref) => ThemeModeNotifier(),
|
(ref) {
|
||||||
|
final prefs = ref.watch(sharedPreferencesProvider);
|
||||||
|
return ThemeModeNotifier(prefs);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
final exchangeRateServiceProvider = Provider<ExchangeRateService>((ref) {
|
final exchangeRateServiceProvider = Provider<ExchangeRateService>((ref) {
|
||||||
|
|||||||
+141
-582
@@ -1,101 +1,84 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.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/constants.dart';
|
||||||
import '../../core/l10n/app_strings.dart';
|
|
||||||
import '../../core/l10n/locale_provider.dart';
|
import '../../core/l10n/locale_provider.dart';
|
||||||
import '../../core/services/biometric_service.dart';
|
import '../../core/services/biometric_service.dart';
|
||||||
import '../../core/services/haptic_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 '../dashboard/provider.dart';
|
||||||
import '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});
|
const SettingsScreen({super.key});
|
||||||
|
|
||||||
@override
|
|
||||||
ConsumerState<SettingsScreen> createState() => _SettingsScreenState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
|
||||||
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<void> _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) {
|
void _confirmClearData(BuildContext context, WidgetRef ref) {
|
||||||
final s = ref.read(stringsProvider);
|
final s = ref.read(stringsProvider);
|
||||||
|
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
|
|
||||||
builder: (ctx) => AlertDialog(
|
builder: (ctx) => AlertDialog(
|
||||||
title: Text(s.clearDataConfirm),
|
title: Text(s.clearDataConfirm),
|
||||||
|
|
||||||
content: Text(s.clearDataWarning),
|
content: Text(s.clearDataWarning),
|
||||||
|
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(ctx),
|
onPressed: () => Navigator.pop(ctx),
|
||||||
|
|
||||||
child: Text(s.cancel),
|
child: Text(s.cancel),
|
||||||
),
|
),
|
||||||
|
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.pop(ctx);
|
Navigator.pop(ctx);
|
||||||
|
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
|
|
||||||
builder: (ctx2) => AlertDialog(
|
builder: (ctx2) => AlertDialog(
|
||||||
title: Text(s.areYouSure),
|
title: Text(s.areYouSure),
|
||||||
|
|
||||||
content: Text(s.allTransactionsWillBeDeleted),
|
content: Text(s.allTransactionsWillBeDeleted),
|
||||||
|
|
||||||
actions: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.pop(ctx2),
|
onPressed: () => Navigator.pop(ctx2),
|
||||||
|
|
||||||
child: Text(s.noKeepThem),
|
child: Text(s.noKeepThem),
|
||||||
),
|
),
|
||||||
|
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
ref.read(transactionsProvider.notifier).clearAll();
|
ref.read(transactionsProvider.notifier).clearAll();
|
||||||
|
|
||||||
Navigator.pop(ctx2);
|
Navigator.pop(ctx2);
|
||||||
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text(s.allTransactionsDeleted)),
|
SnackBar(content: Text(s.allTransactionsDeleted)),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
style: TextButton.styleFrom(foregroundColor: const Color(0xFFE05C6B)),
|
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
foregroundColor: const Color(0xFFE05C6B),
|
||||||
|
),
|
||||||
|
|
||||||
child: Text(s.yesDeleteEverything),
|
child: Text(s.yesDeleteEverything),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
style: TextButton.styleFrom(foregroundColor: const Color(0xFFE05C6B)),
|
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
foregroundColor: const Color(0xFFE05C6B),
|
||||||
|
),
|
||||||
|
|
||||||
child: Text(s.delete),
|
child: Text(s.delete),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -104,79 +87,30 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final s = ref.watch(stringsProvider);
|
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(
|
return Scaffold(
|
||||||
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
|
||||||
|
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Column(
|
title: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
s.settings,
|
s.settings,
|
||||||
|
|
||||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
|
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
s.managePreferences,
|
s.managePreferences,
|
||||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
|
||||||
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
body: SafeArea(
|
|
||||||
child: ListView(
|
|
||||||
padding: const EdgeInsets.fromLTRB(20, 16, 20, 20),
|
|
||||||
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: 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(
|
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),
|
||||||
),
|
),
|
||||||
@@ -184,548 +118,142 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Switch(
|
|
||||||
value: isDarkMode,
|
body: ListView(
|
||||||
onChanged: (value) {
|
physics: const ClampingScrollPhysics(),
|
||||||
ref.read(themeProvider.notifier).setThemeMode(value);
|
|
||||||
},
|
padding: const EdgeInsets.fromLTRB(20, 16, 20, 40),
|
||||||
activeColor: AppColors.accent,
|
|
||||||
),
|
children: [
|
||||||
],
|
const ThemeSection(),
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
Consumer(
|
const HapticSection(),
|
||||||
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 SizedBox(height: 16),
|
||||||
|
|
||||||
const _BiometricSection(),
|
const _BiometricSection(),
|
||||||
|
|
||||||
Container(
|
const LanguageSection(),
|
||||||
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),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
Container(
|
const CurrencySection(),
|
||||||
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),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
Container(
|
const AmountFormatSection(),
|
||||||
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),
|
const SizedBox(height: 16),
|
||||||
if (_isEditing)
|
|
||||||
Column(
|
const BudgetSection(),
|
||||||
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),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
s.dangerZone,
|
s.dangerZone,
|
||||||
|
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
|
|
||||||
letterSpacing: 1.2,
|
letterSpacing: 1.2,
|
||||||
|
|
||||||
color: const Color(0xFFE05C6B).withOpacity(0.8),
|
color: const Color(0xFFE05C6B).withOpacity(0.8),
|
||||||
|
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
|
|
||||||
child: OutlinedButton.icon(
|
child: OutlinedButton.icon(
|
||||||
onPressed: () => _confirmClearData(context, ref),
|
onPressed: () => _confirmClearData(context, ref),
|
||||||
|
|
||||||
icon: const Icon(Icons.delete_forever, color: Color(0xFFE05C6B)),
|
icon: const Icon(Icons.delete_forever, color: Color(0xFFE05C6B)),
|
||||||
|
|
||||||
label: Text(
|
label: Text(
|
||||||
s.clearAllTransactions,
|
s.clearAllTransactions,
|
||||||
|
|
||||||
style: const TextStyle(color: Color(0xFFE05C6B)),
|
style: const TextStyle(color: Color(0xFFE05C6B)),
|
||||||
),
|
),
|
||||||
|
|
||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
side: BorderSide(color: const Color(0xFFE05C6B).withOpacity(0.5)),
|
side: BorderSide(
|
||||||
|
color: const Color(0xFFE05C6B).withOpacity(0.5),
|
||||||
|
),
|
||||||
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 24),
|
const _FooterWidget(),
|
||||||
child: Center(
|
],
|
||||||
child: Builder(
|
),
|
||||||
builder: (context) {
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FooterWidget extends StatelessWidget {
|
||||||
|
const _FooterWidget();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||||
|
|
||||||
final baseColor = isDark
|
final baseColor = isDark
|
||||||
? Colors.white.withOpacity(0.25)
|
? Colors.white.withOpacity(0.25)
|
||||||
: Colors.black.withOpacity(0.25);
|
: Colors.black.withOpacity(0.25);
|
||||||
|
|
||||||
final emphasisColor = isDark
|
final emphasisColor = isDark
|
||||||
? Colors.white.withOpacity(0.35)
|
? Colors.white.withOpacity(0.35)
|
||||||
: Colors.black.withOpacity(0.35);
|
: Colors.black.withOpacity(0.35);
|
||||||
|
|
||||||
return RichText(
|
return Center(
|
||||||
|
child: RichText(
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
|
|
||||||
text: TextSpan(
|
text: TextSpan(
|
||||||
style: TextStyle(fontSize: 13, color: baseColor),
|
style: TextStyle(fontSize: 13, color: baseColor),
|
||||||
|
|
||||||
children: [
|
children: [
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: 'casha',
|
text: 'casha',
|
||||||
|
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
|
|
||||||
color: emphasisColor,
|
color: emphasisColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
TextSpan(
|
|
||||||
|
const TextSpan(
|
||||||
text: ' powered with ❤️ by ',
|
text: ' powered with ❤️ by ',
|
||||||
|
|
||||||
style: TextStyle(fontStyle: FontStyle.italic),
|
style: TextStyle(fontStyle: FontStyle.italic),
|
||||||
),
|
),
|
||||||
|
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: 'kolo',
|
text: 'kolo',
|
||||||
|
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
|
|
||||||
color: emphasisColor,
|
color: emphasisColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -740,22 +268,29 @@ class _BiometricSection extends ConsumerStatefulWidget {
|
|||||||
|
|
||||||
class _BiometricSectionState extends ConsumerState<_BiometricSection> {
|
class _BiometricSectionState extends ConsumerState<_BiometricSection> {
|
||||||
bool _available = false;
|
bool _available = false;
|
||||||
|
|
||||||
bool _enabled = false;
|
bool _enabled = false;
|
||||||
|
|
||||||
bool _loading = true;
|
bool _loading = true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
_load();
|
_load();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _load() async {
|
Future<void> _load() async {
|
||||||
final available = await BiometricService.isAvailable();
|
final available = await BiometricService.isAvailable();
|
||||||
|
|
||||||
final enabled = await BiometricService.isEnabled();
|
final enabled = await BiometricService.isEnabled();
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_available = available;
|
_available = available;
|
||||||
|
|
||||||
_enabled = enabled;
|
_enabled = enabled;
|
||||||
|
|
||||||
_loading = false;
|
_loading = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -764,10 +299,14 @@ class _BiometricSectionState extends ConsumerState<_BiometricSection> {
|
|||||||
Future<void> _onToggle(bool val) async {
|
Future<void> _onToggle(bool val) async {
|
||||||
if (val) {
|
if (val) {
|
||||||
final ok = await BiometricService.authenticate();
|
final ok = await BiometricService.authenticate();
|
||||||
|
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
|
|
||||||
HapticService.light();
|
HapticService.light();
|
||||||
}
|
}
|
||||||
|
|
||||||
await BiometricService.setEnabled(val);
|
await BiometricService.setEnabled(val);
|
||||||
|
|
||||||
if (mounted) setState(() => _enabled = val);
|
if (mounted) setState(() => _enabled = val);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -775,68 +314,88 @@ class _BiometricSectionState extends ConsumerState<_BiometricSection> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (_loading || !_available) return const SizedBox.shrink();
|
if (_loading || !_available) return const SizedBox.shrink();
|
||||||
|
|
||||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
|
||||||
|
|
||||||
return Consumer(
|
|
||||||
builder: (context, ref, _) {
|
|
||||||
final s = ref.watch(stringsProvider);
|
final s = ref.watch(stringsProvider);
|
||||||
|
|
||||||
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Theme.of(context).colorScheme.surface,
|
color: Theme.of(context).colorScheme.surface,
|
||||||
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
border: isDark ? null : Border.all(color: const Color(0xFFDDDDEE), width: 1),
|
|
||||||
|
border: isDark
|
||||||
|
? null
|
||||||
|
: Border.all(color: const Color(0xFFDDDDEE), width: 1),
|
||||||
),
|
),
|
||||||
|
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(10),
|
padding: const EdgeInsets.all(10),
|
||||||
|
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: AppColors.accent.withOpacity(0.15),
|
color: AppColors.accent.withOpacity(0.15),
|
||||||
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
|
|
||||||
child: const Icon(
|
child: const Icon(
|
||||||
Icons.fingerprint,
|
Icons.fingerprint,
|
||||||
|
|
||||||
color: AppColors.accent,
|
color: AppColors.accent,
|
||||||
|
|
||||||
size: 20,
|
size: 20,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
s.biometricLock,
|
s.biometricLock,
|
||||||
|
|
||||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
|
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
s.requireFingerprint,
|
s.requireFingerprint,
|
||||||
|
|
||||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
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),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
Switch(
|
Switch(
|
||||||
value: _enabled,
|
value: _enabled,
|
||||||
|
|
||||||
onChanged: _onToggle,
|
onChanged: _onToggle,
|
||||||
activeColor: const Color(0xFF7C6DED),
|
|
||||||
|
activeThumbColor: const Color(0xFF7C6DED),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<BudgetSection> createState() => _BudgetSectionState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BudgetSectionState extends ConsumerState<BudgetSection> {
|
||||||
|
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<void> _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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user