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) {
|
||||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
final raw = DateFormat('LLLL, yyyy', s.dateLocale).format(DateTime.now());
|
||||||
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.5),
|
final capitalized = raw.isNotEmpty ? '${raw[0].toUpperCase()}${raw.substring(1)}' : raw;
|
||||||
fontWeight: FontWeight.w500,
|
return Text(
|
||||||
),
|
capitalized,
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
|
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.5),
|
||||||
|
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) {
|
||||||
|
|||||||
+257
-698
File diff suppressed because it is too large
Load Diff
@@ -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