This commit is contained in:
2026-03-21 12:06:39 +03:00
parent b37f55843d
commit a0f800cfe4
16 changed files with 474 additions and 138 deletions
+1 -1
View File
@@ -19,7 +19,7 @@ Casha is the **"Simplest"**, **"Most Elegant"**, and **"Most Focused"** way to t
## 📱 Screenshots ## 📱 Screenshots
> _Add your screenshots here_ ![screenshot](https://i.ibb.co/Fq06nxKM/screenshot.png)
## 🚀 Getting Started ## 🚀 Getting Started
Binary file not shown.

After

Width:  |  Height:  |  Size: 429 KiB

+4
View File
@@ -8,9 +8,11 @@ class AppTheme {
final textTheme = GoogleFonts.poppinsTextTheme(base.textTheme).apply( final textTheme = GoogleFonts.poppinsTextTheme(base.textTheme).apply(
bodyColor: AppColors.textPrimary, bodyColor: AppColors.textPrimary,
displayColor: AppColors.textPrimary, displayColor: AppColors.textPrimary,
fontFamilyFallback: ['Roboto'], // Ensures Cyrillic renders with same visual style
); );
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(
@@ -106,9 +108,11 @@ class AppTheme {
final textTheme = GoogleFonts.poppinsTextTheme(base.textTheme).apply( final textTheme = GoogleFonts.poppinsTextTheme(base.textTheme).apply(
bodyColor: const Color(0xFF1A1A2E), bodyColor: const Color(0xFF1A1A2E),
displayColor: const Color(0xFF1A1A2E), displayColor: const Color(0xFF1A1A2E),
fontFamilyFallback: ['Roboto'], // Ensures Cyrillic renders with same visual style
); );
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(
+140
View File
@@ -0,0 +1,140 @@
enum AppLocale { en, ru }
class AppStrings {
final AppLocale locale;
const AppStrings(this.locale);
bool get _ru => locale == AppLocale.ru;
// ── Dashboard ──
String get appTitle => _ru ? 'Мои финансы' : 'My Finances';
String get totalBalance => _ru ? 'ОБЩИЙ БАЛАНС' : 'TOTAL BALANCE';
String get add => _ru ? 'Добавить' : 'Add';
String get transactions => _ru ? 'Транзакции' : 'Transactions';
String get searchHint => _ru ? 'Поиск транзакций...' : 'Search transactions...';
String get filterAll => _ru ? 'Все' : 'All';
String get filterIncome => _ru ? 'Доход' : 'Income';
String get filterExpense => _ru ? 'Расход' : 'Expense';
String get filterAllTime => _ru ? 'Всё время' : 'All Time';
String get filterMonth => _ru ? 'Месяц' : 'Month';
String get income => _ru ? 'Доход' : 'Income';
String get expenses => _ru ? 'Расходы' : 'Expenses';
String get monthlyBudget => _ru ? 'Бюджет на месяц' : 'Monthly Budget';
String get spent => _ru ? 'Потрачено' : 'Spent';
String get limit => _ru ? 'Лимит' : 'Limit';
String get noTransactions => _ru ? 'Транзакции не найдены' : 'No transactions found';
String get addFirstTx => _ru ? 'Нажмите + чтобы добавить первую транзакцию' : 'Tap + to add your first transaction';
// ── Add/Edit transaction screen ──
String get addTransaction => _ru ? 'Новая транзакция' : 'Add Transaction';
String get editTransaction => _ru ? 'Редактировать' : 'Edit Transaction';
String get amount => _ru ? 'Сумма' : 'Amount';
String get category => _ru ? 'Категория' : 'Category';
String get note => _ru ? 'Заметка' : 'Note';
String get date => _ru ? 'Дата' : 'Date';
String get time => _ru ? 'Время' : 'Time';
String get save => _ru ? 'Сохранить' : 'Save';
String get delete => _ru ? 'Удалить' : 'Delete';
String get cancel => _ru ? 'Отмена' : 'Cancel';
String get typeIncome => _ru ? 'Доход' : 'Income';
String get typeExpense => _ru ? 'Расход' : 'Expense';
String get confirmDelete => _ru ? 'Удалить транзакцию?' : 'Delete transaction?';
String get confirmDeleteBody => _ru ? 'Это действие нельзя отменить.' : 'This action cannot be undone.';
String get saveChanges => _ru ? 'Сохранить изменения' : 'Save Changes';
String get noteOptional => _ru ? 'Заметка (необязательно)' : 'Note (optional)';
String get addNote => _ru ? 'Добавить заметку...' : 'Add a note...';
// ── Settings ──
String get settings => _ru ? 'Настройки' : 'Settings';
String get managePreferences => _ru ? 'Управление настройками' : 'Manage your preferences';
String get appearance => _ru ? 'Внешний вид' : 'Appearance';
String get theme => _ru ? 'Тема' : 'Theme';
String get themeDark => _ru ? 'Тёмная' : 'Dark';
String get themeLight => _ru ? 'Светлая' : 'Light';
String get themeSystem => _ru ? 'Системная' : 'System';
String get darkMode => _ru ? 'Тёмная тема' : 'Dark Mode';
String get enabled => _ru ? 'Включено' : 'Enabled';
String get disabled => _ru ? 'Выключено' : 'Disabled';
String get hapticFeedback => _ru ? 'Тактильная отдача' : 'Haptic Feedback';
String get vibrationOnInteractions => _ru ? 'Вибрация при взаимодействии' : 'Vibration on interactions';
String get biometricLock => _ru ? 'Биометрическая блокировка' : 'Biometric Lock';
String get requireFingerprint => _ru ? 'Требовать отпечаток при запуске' : 'Require fingerprint on app launch';
String get currency => _ru ? 'Валюта' : 'Currency';
String get amountFormat => _ru ? 'Формат суммы' : 'Amount Format';
String get language => _ru ? 'Язык' : 'Language';
String get langRu => _ru ? 'Русский' : 'Russian';
String get langEn => _ru ? 'Английский' : 'English';
String get budget => _ru ? 'Бюджет' : 'Budget';
String get budgetHint => _ru ? 'Месячный лимит' : 'Monthly limit';
String get budgetNone => _ru ? 'Не установлен' : 'Not set';
String get monthlyBudgetSetting => _ru ? 'Месячный бюджет' : 'Monthly Budget';
String get yourMonthlySpendingLimit => _ru ? 'Ваш месячный лимит расходов' : 'Your monthly spending limit';
String get setMonthlySpendingLimit => _ru ? 'Установите месячный лимит расходов для отслеживания бюджета' : 'Set a monthly spending limit to track your budget';
String get leaveEmptyToRemove => _ru ? 'Оставьте пустым для удаления лимита' : 'Leave empty to remove budget limit';
String get data => _ru ? 'Данные' : 'Data';
String get exportData => _ru ? 'Экспорт данных' : 'Export data';
String get clearData => _ru ? 'Очистить данные' : 'Clear all data';
String get clearAllTransactions => _ru ? 'Очистить все транзакции' : 'Clear All Transactions';
String get clearDataConfirm => _ru ? 'Удалить все транзакции?' : 'Clear all transactions?';
String get clearDataWarning => _ru ? 'Это навсегда удалит всю историю транзакций. Это действие нельзя отменить.' : 'This will permanently delete all your transaction history. This cannot be undone.';
String get areYouSure => _ru ? 'Вы абсолютно уверены?' : 'Are you absolutely sure?';
String get allTransactionsDeleted => _ru ? 'Все транзакции удалены' : 'All transactions deleted';
String get noKeepThem => _ru ? 'Нет, оставить' : 'No, keep them';
String get yesDeleteEverything => _ru ? 'Да, удалить всё' : 'Yes, delete everything';
String get allTransactionsWillBeDeleted => _ru ? 'Все транзакции будут удалены навсегда. Восстановить их будет невозможно.' : 'All transactions will be deleted forever. There is no way to recover them.';
String get dangerZone => _ru ? 'Опасная зона' : 'Danger Zone';
// ── Categories ──
String get categories => _ru ? 'Категории' : 'Categories';
String get rankedByAmount => _ru ? 'По сумме' : 'Ranked by Amount';
String get addCategory => _ru ? 'Добавить категорию' : 'Add Category';
String get editCategory => _ru ? 'Редактировать' : 'Edit Category';
String get categoryName => _ru ? 'Название' : 'Name';
String get categoryIcon => _ru ? 'Иконка' : 'Icon';
String get categoryColor => _ru ? 'Цвет' : 'Color';
String get deleteCategory => _ru ? 'Удалить категорию' : 'Delete Category';
String get noCategoriesYet => _ru ? 'Нет категорий' : 'No categories yet';
// ── Built-in category names (translated for display only, stored in English) ──
String categoryLabel(String key) {
if (!_ru) return key; // English: return key as-is
const map = {
'Food': 'Еда',
'Transport': 'Транспорт',
'Shopping': 'Покупки',
'Entertainment': 'Развлечения',
'Health': 'Здоровье',
'Housing': 'Жильё',
'Education': 'Образование',
'Travel': 'Путешествия',
'Salary': 'Зарплата',
'Freelance': 'Фриланс',
'Investment': 'Инвестиции',
'Gift': 'Подарок',
'Other': 'Другое',
'Utilities': 'Коммунальные',
'Clothing': 'Одежда',
'Sports': 'Спорт',
'Beauty': 'Красота',
'Pets': 'Питомцы',
'Business': 'Бизнес',
'Savings': 'Накопления',
};
return map[key] ?? key; // fallback to English key if not in map
}
// ── Color editor overlay ──
String get colorPrimary => _ru ? 'Основной' : 'Primary';
String get colorSecondary => _ru ? 'Вторичный' : 'Secondary';
String get colorSolid => _ru ? 'Однотон' : 'Solid';
String get gradientLinear => _ru ? 'Линейный' : 'Linear';
String get gradientReverse => _ru ? 'Обратный' : 'Reverse';
String get gradientRadial => _ru ? 'Радиус' : 'Radial';
String get gradientSweep => _ru ? 'Круговой' : 'Sweep';
String get reset => _ru ? 'Сброс' : 'Reset';
String get apply => _ru ? 'Применить' : 'Apply';
// ── Date locale code for intl DateFormat ──
String get dateLocale => _ru ? 'ru_RU' : 'en_US';
}
+30
View File
@@ -0,0 +1,30 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../features/dashboard/provider.dart';
import 'app_strings.dart';
class LocaleNotifier extends Notifier<AppLocale> {
static const _key = 'app_locale';
@override
AppLocale build() {
// Load persisted locale synchronously via ref
final prefs = ref.read(sharedPreferencesProvider);
final saved = prefs.getString(_key);
return saved == 'ru' ? AppLocale.ru : AppLocale.en;
}
void setLocale(AppLocale locale) {
state = locale;
ref.read(sharedPreferencesProvider).setString(_key, locale.name);
}
}
final localeProvider = NotifierProvider<LocaleNotifier, AppLocale>(
LocaleNotifier.new,
);
final stringsProvider = Provider<AppStrings>((ref) {
final locale = ref.watch(localeProvider);
return AppStrings(locale);
});
+26 -17
View File
@@ -5,6 +5,8 @@ import 'package:go_router/go_router.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
import '../../core/constants.dart'; import '../../core/constants.dart';
import '../../core/l10n/app_strings.dart';
import '../../core/l10n/locale_provider.dart';
import '../../core/services/haptic_service.dart'; import '../../core/services/haptic_service.dart';
import '../../shared/models/transaction.dart'; import '../../shared/models/transaction.dart';
import '../dashboard/provider.dart'; import '../dashboard/provider.dart';
@@ -216,6 +218,7 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final s = ref.watch(stringsProvider);
final state = ref.watch(addTransactionProvider(widget.initial)); final state = ref.watch(addTransactionProvider(widget.initial));
final categories = ref.watch(availableCategoriesProvider(widget.initial)); final categories = ref.watch(availableCategoriesProvider(widget.initial));
final overrideCurrency = state.overrideCurrency; final overrideCurrency = state.overrideCurrency;
@@ -224,7 +227,7 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen>
return Scaffold( return Scaffold(
backgroundColor: Theme.of(context).scaffoldBackgroundColor, backgroundColor: Theme.of(context).scaffoldBackgroundColor,
appBar: AppBar( appBar: AppBar(
title: Text(state.isEditing ? 'Edit Transaction' : 'Add Transaction'), title: Text(state.isEditing ? s.editTransaction : s.addTransaction),
leading: IconButton( leading: IconButton(
icon: const Icon(Icons.close_rounded), icon: const Icon(Icons.close_rounded),
onPressed: () => context.pop(), onPressed: () => context.pop(),
@@ -238,12 +241,12 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen>
showDialog( showDialog(
context: context, context: context,
builder: (ctx) => AlertDialog( builder: (ctx) => AlertDialog(
title: const Text('Delete transaction?'), title: Text(s.confirmDelete),
content: const Text('This action cannot be undone.'), content: Text(s.confirmDeleteBody),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(ctx), onPressed: () => Navigator.pop(ctx),
child: const Text('Cancel'), child: Text(s.cancel),
), ),
TextButton( TextButton(
onPressed: () { onPressed: () {
@@ -254,7 +257,7 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen>
style: TextButton.styleFrom( style: TextButton.styleFrom(
foregroundColor: const Color(0xFFE05C6B), foregroundColor: const Color(0xFFE05C6B),
), ),
child: const Text('Delete'), child: Text(s.delete),
), ),
], ],
), ),
@@ -271,12 +274,13 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen>
children: [ children: [
_TypeToggle( _TypeToggle(
selected: state.type, selected: state.type,
strings: s,
onChanged: (t) => onChanged: (t) =>
ref.read(addTransactionProvider(widget.initial).notifier).setType(t), ref.read(addTransactionProvider(widget.initial).notifier).setType(t),
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
_SectionLabel('Amount'), _SectionLabel(s.amount),
const SizedBox(height: 8), const SizedBox(height: 8),
AnimatedBuilder( AnimatedBuilder(
animation: _borderColorAnimation, animation: _borderColorAnimation,
@@ -339,7 +343,7 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen>
const SizedBox(height: 20), const SizedBox(height: 20),
Text( Text(
'Currency', s.currency,
style: TextStyle( style: TextStyle(
fontSize: 13, fontSize: 13,
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
@@ -353,7 +357,7 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen>
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
_SectionLabel('Category'), _SectionLabel(s.category),
const SizedBox(height: 8), const SizedBox(height: 8),
_CategoryPicker( _CategoryPicker(
categories: categories, categories: categories,
@@ -372,7 +376,7 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen>
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
'Date', s.date,
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),
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
@@ -400,7 +404,7 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen>
const SizedBox(width: 8), const SizedBox(width: 8),
Expanded( Expanded(
child: Text( child: Text(
DateFormat('MMM d, yyyy').format(_selectedDate), DateFormat('MMM d, yyyy', s.dateLocale).format(_selectedDate),
style: Theme.of(context).textTheme.bodyMedium?.copyWith( style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurface, color: Theme.of(context).colorScheme.onSurface,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
@@ -422,7 +426,7 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen>
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
'Time', s.time,
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),
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
@@ -466,7 +470,7 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen>
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
_SectionLabel('Note (optional)'), _SectionLabel(s.noteOptional),
const SizedBox(height: 8), const SizedBox(height: 8),
TextFormField( TextFormField(
controller: _noteController, controller: _noteController,
@@ -482,7 +486,7 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen>
), ),
), ),
decoration: InputDecoration( decoration: InputDecoration(
hintText: 'Add a note...', hintText: s.addNote,
enabledBorder: OutlineInputBorder( enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
borderSide: isDark borderSide: isDark
@@ -530,7 +534,7 @@ class _AddTransactionScreenState extends ConsumerState<AddTransactionScreen>
), ),
) )
: Text( : Text(
state.isEditing ? 'Save Changes' : 'Add Transaction', state.isEditing ? s.saveChanges : s.addTransaction,
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@@ -570,7 +574,12 @@ class _SectionLabel extends StatelessWidget {
class _TypeToggle extends StatelessWidget { class _TypeToggle extends StatelessWidget {
final TransactionType selected; final TransactionType selected;
final ValueChanged<TransactionType> onChanged; final ValueChanged<TransactionType> onChanged;
const _TypeToggle({required this.selected, required this.onChanged}); final AppStrings strings;
const _TypeToggle({
required this.selected,
required this.onChanged,
required this.strings,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -584,14 +593,14 @@ class _TypeToggle extends StatelessWidget {
child: Row( child: Row(
children: [ children: [
_TypeOption( _TypeOption(
label: 'Income', label: strings.typeIncome,
icon: Icons.arrow_downward_rounded, icon: Icons.arrow_downward_rounded,
color: AppColors.income, color: AppColors.income,
isSelected: selected == TransactionType.income, isSelected: selected == TransactionType.income,
onTap: () => onChanged(TransactionType.income), onTap: () => onChanged(TransactionType.income),
), ),
_TypeOption( _TypeOption(
label: 'Expense', label: strings.typeExpense,
icon: Icons.arrow_upward_rounded, icon: Icons.arrow_upward_rounded,
color: AppColors.expense, color: AppColors.expense,
isSelected: selected == TransactionType.expense, isSelected: selected == TransactionType.expense,
+7 -4
View File
@@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.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 '../../shared/utils/currency_utils.dart'; import '../../shared/utils/currency_utils.dart';
import '../../shared/providers/amount_format_provider.dart'; import '../../shared/providers/amount_format_provider.dart';
import '../settings/provider.dart'; import '../settings/provider.dart';
@@ -24,6 +25,7 @@ class _CategoriesScreenState extends ConsumerState<CategoriesScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final s = ref.watch(stringsProvider);
final data = ref.watch(categoryExpenseProvider); final data = ref.watch(categoryExpenseProvider);
final monthlyData = ref.watch(monthlyBreakdownProvider); final monthlyData = ref.watch(monthlyBreakdownProvider);
final total = data.values.fold(0.0, (a, b) => a + b); final total = data.values.fold(0.0, (a, b) => a + b);
@@ -39,14 +41,14 @@ class _CategoriesScreenState extends ConsumerState<CategoriesScreen> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
'Categories', s.categories,
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(
'Expense breakdown', s.expenses,
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),
), ),
@@ -85,7 +87,7 @@ class _CategoriesScreenState extends ConsumerState<CategoriesScreen> {
_BarChartCard(monthlyData: monthlyData, currency: currencyInfo.symbol), _BarChartCard(monthlyData: monthlyData, currency: currencyInfo.symbol),
const SizedBox(height: 20), const SizedBox(height: 20),
Text( Text(
'Ranked by Amount', s.rankedByAmount,
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,
@@ -408,6 +410,7 @@ class _CategoryRow 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 color = AppCategories.colors[category] ?? AppColors.accent; final color = AppCategories.colors[category] ?? AppColors.accent;
final icon = AppCategories.icons[category] ?? Icons.category_rounded; final icon = AppCategories.icons[category] ?? Icons.category_rounded;
@@ -451,7 +454,7 @@ class _CategoryRow extends ConsumerWidget {
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded( Expanded(
child: Text( child: Text(
category, s.categoryLabel(category),
style: Theme.of(context).textTheme.bodyMedium?.copyWith( style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.onSurface, color: Theme.of(context).colorScheme.onSurface,
+12 -7
View File
@@ -2,6 +2,7 @@ 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:go_router/go_router.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.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 '../settings/provider.dart'; import '../settings/provider.dart';
@@ -115,6 +116,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final s = ref.watch(stringsProvider);
final balance = ref.watch(totalBalanceProvider); final balance = ref.watch(totalBalanceProvider);
final income = ref.watch(totalIncomeProvider); final income = ref.watch(totalIncomeProvider);
final expense = ref.watch(totalExpenseProvider); final expense = ref.watch(totalExpenseProvider);
@@ -131,7 +133,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
scrolledUnderElevation: 0, scrolledUnderElevation: 0,
titleSpacing: 20, titleSpacing: 20,
title: Text( title: Text(
'Casha', s.appTitle,
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,
@@ -143,7 +145,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
padding: const EdgeInsets.only(right: 20), padding: const EdgeInsets.only(right: 20),
child: Center( child: Center(
child: Text( child: Text(
DateFormat('MMMM yyyy').format(DateTime.now()), DateFormat('MMMM yyyy', s.dateLocale).format(DateTime.now()),
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,
@@ -161,7 +163,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
backgroundColor: const Color(0xFF7C6DED), backgroundColor: const Color(0xFF7C6DED),
foregroundColor: Colors.white, foregroundColor: Colors.white,
icon: const Icon(Icons.add), icon: const Icon(Icons.add),
label: const Text('Add', style: TextStyle(fontWeight: FontWeight.w600)), label: Text(s.add, style: const TextStyle(fontWeight: FontWeight.w600)),
), ),
floatingActionButtonLocation: FloatingActionButtonLocation.endFloat, floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,
body: SafeArea( body: SafeArea(
@@ -188,6 +190,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
income: income, income: income,
expense: expense, expense: expense,
currencyInfo: currencyInfo, currencyInfo: currencyInfo,
strings: s,
), ),
if (budget != null) ...[ if (budget != null) ...[
const SizedBox(height: 16), const SizedBox(height: 16),
@@ -195,6 +198,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
spent: monthExpense, spent: monthExpense,
budget: budget, budget: budget,
currencyInfo: currencyInfo, currencyInfo: currencyInfo,
strings: s,
), ),
], ],
const SizedBox(height: 24), const SizedBox(height: 24),
@@ -203,12 +207,13 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
focusNode: _searchFocusNode, focusNode: _searchFocusNode,
onTap: _scrollToSearch, onTap: _scrollToSearch,
ref: ref, ref: ref,
strings: s,
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
const FilterChips(), FilterChips(strings: s),
const SizedBox(height: 20), const SizedBox(height: 20),
Text( Text(
'Transactions', s.transactions,
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,
@@ -220,9 +225,9 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
), ),
), ),
if (recent.isEmpty) if (recent.isEmpty)
const SliverFillRemaining( SliverFillRemaining(
hasScrollBody: false, hasScrollBody: false,
child: EmptyState(), child: EmptyState(strings: s),
) )
else else
SliverPadding( SliverPadding(
@@ -1,5 +1,6 @@
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 '../../../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';
@@ -9,11 +10,13 @@ class BudgetProgress extends ConsumerWidget {
final double spent; final double spent;
final double budget; final double budget;
final CurrencyInfo currencyInfo; final CurrencyInfo currencyInfo;
final AppStrings strings;
const BudgetProgress({ const BudgetProgress({
super.key, super.key,
required this.spent, required this.spent,
required this.budget, required this.budget,
required this.currencyInfo, required this.currencyInfo,
required this.strings,
}); });
Border? _themeBorder(BuildContext context) { Border? _themeBorder(BuildContext context) {
@@ -51,7 +54,7 @@ class BudgetProgress extends ConsumerWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text( Text(
'Monthly Budget', strings.monthlyBudget,
style: Theme.of(context).textTheme.bodyMedium?.copyWith( style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of( color: Theme.of(
context, context,
@@ -95,7 +98,7 @@ class BudgetProgress extends ConsumerWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text( Text(
'Spent: ${formatAmount(currencyInfo.symbol, spent, fmt)}', '${strings.spent}: ${formatAmount(currencyInfo.symbol, spent, fmt)}',
style: Theme.of(context).textTheme.bodySmall?.copyWith( style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of( color: Theme.of(
context, context,
@@ -103,7 +106,7 @@ class BudgetProgress extends ConsumerWidget {
), ),
), ),
Text( Text(
'Limit: ${formatAmount(currencyInfo.symbol, budget, fmt)}', '${strings.limit}: ${formatAmount(currencyInfo.symbol, budget, fmt)}',
style: Theme.of(context).textTheme.bodySmall?.copyWith( style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of( color: Theme.of(
context, context,
@@ -2,6 +2,8 @@ import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_colorpicker/flutter_colorpicker.dart'; import 'package:flutter_colorpicker/flutter_colorpicker.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/l10n/app_strings.dart';
import '../../../core/l10n/locale_provider.dart';
import '../../../core/services/card_color_service.dart'; import '../../../core/services/card_color_service.dart';
import '../../../core/services/haptic_service.dart'; import '../../../core/services/haptic_service.dart';
import '../../settings/provider.dart'; import '../../settings/provider.dart';
@@ -96,6 +98,10 @@ class _FullScreenBlurOverlayState extends State<FullScreenBlurOverlay> {
), ),
child: StatefulBuilder( child: StatefulBuilder(
builder: (ctx, setPanelState) { builder: (ctx, setPanelState) {
final s = AppStrings(
ProviderScope.containerOf(widget.context).read(localeProvider),
);
void onHSVChanged(HSVColor hsv) { void onHSVChanged(HSVColor hsv) {
setPanelState(() {}); setPanelState(() {});
dash.setState(() { dash.setState(() {
@@ -125,7 +131,7 @@ class _FullScreenBlurOverlayState extends State<FullScreenBlurOverlay> {
children: [ children: [
Expanded( Expanded(
child: PanelTab( child: PanelTab(
label: 'Primary', label: s.colorPrimary,
isSelected: dash.editingPrimary, isSelected: dash.editingPrimary,
color: dash.tempPrimary, color: dash.tempPrimary,
isDimmed: isSolid, isDimmed: isSolid,
@@ -142,7 +148,7 @@ class _FullScreenBlurOverlayState extends State<FullScreenBlurOverlay> {
const SizedBox(width: 6), const SizedBox(width: 6),
Expanded( Expanded(
child: PanelTab( child: PanelTab(
label: 'Secondary', label: s.colorSecondary,
isSelected: !dash.editingPrimary, isSelected: !dash.editingPrimary,
color: dash.tempSecondary, color: dash.tempSecondary,
isDimmed: isSolid, isDimmed: isSolid,
@@ -196,7 +202,7 @@ class _FullScreenBlurOverlayState extends State<FullScreenBlurOverlay> {
), ),
), ),
child: Text( child: Text(
'Solid', s.colorSolid,
style: TextStyle( style: TextStyle(
fontSize: 11, fontSize: 11,
fontWeight: isSolid fontWeight: isSolid
@@ -391,10 +397,10 @@ class _FullScreenBlurOverlayState extends State<FullScreenBlurOverlay> {
.map((type) { .map((type) {
final isSelected = dash.tempGradientType == type; final isSelected = dash.tempGradientType == type;
final label = switch (type) { final label = switch (type) {
GradientType.linear => 'Linear', GradientType.linear => s.gradientLinear,
GradientType.linearReverse => 'Reverse', GradientType.linearReverse => s.gradientReverse,
GradientType.radial => 'Radial', GradientType.radial => s.gradientRadial,
GradientType.sweep => 'Sweep', GradientType.sweep => s.gradientSweep,
GradientType.solid => '', GradientType.solid => '',
}; };
final icon = switch (type) { final icon = switch (type) {
@@ -498,8 +504,8 @@ class _FullScreenBlurOverlayState extends State<FullScreenBlurOverlay> {
dash.overlayEntry?.markNeedsBuild(); dash.overlayEntry?.markNeedsBuild();
}, },
icon: const Icon(Icons.restart_alt_rounded, size: 15), icon: const Icon(Icons.restart_alt_rounded, size: 15),
label: const Text('Reset', label: Text(s.reset,
style: TextStyle(fontSize: 13)), style: const TextStyle(fontSize: 13)),
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
foregroundColor: Theme.of(widget.context) foregroundColor: Theme.of(widget.context)
.colorScheme .colorScheme
@@ -529,8 +535,8 @@ class _FullScreenBlurOverlayState extends State<FullScreenBlurOverlay> {
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)), borderRadius: BorderRadius.circular(12)),
), ),
child: const Text('Apply', child: Text(s.apply,
style: TextStyle( style: const TextStyle(
fontWeight: FontWeight.w700, fontSize: 14)), fontWeight: FontWeight.w700, fontSize: 14)),
), ),
), ),
@@ -1,11 +1,13 @@
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 '../../../core/constants.dart'; import '../../../core/constants.dart';
import '../../../core/l10n/app_strings.dart';
import '../../../core/services/haptic_service.dart'; import '../../../core/services/haptic_service.dart';
import '../provider.dart'; import '../provider.dart';
class FilterChips extends ConsumerWidget { class FilterChips extends ConsumerWidget {
const FilterChips({super.key}); final AppStrings strings;
const FilterChips({super.key, required this.strings});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
@@ -16,13 +18,13 @@ class FilterChips extends ConsumerWidget {
return Row( return Row(
children: [ children: [
_FilterChip( _FilterChip(
label: 'All Time', label: strings.filterAllTime,
isSelected: timeFilter == TimeFilter.allTime, isSelected: timeFilter == TimeFilter.allTime,
onTap: () => ref.read(timeFilterProvider.notifier).state = TimeFilter.allTime, onTap: () => ref.read(timeFilterProvider.notifier).state = TimeFilter.allTime,
), ),
const SizedBox(width: 6), const SizedBox(width: 6),
_FilterChip( _FilterChip(
label: 'Month', label: strings.filterMonth,
isSelected: timeFilter == TimeFilter.lastMonth, isSelected: timeFilter == TimeFilter.lastMonth,
onTap: () => ref.read(timeFilterProvider.notifier).state = TimeFilter.lastMonth, onTap: () => ref.read(timeFilterProvider.notifier).state = TimeFilter.lastMonth,
), ),
@@ -37,20 +39,20 @@ class FilterChips extends ConsumerWidget {
), ),
), ),
_FilterChip( _FilterChip(
label: 'All', label: strings.filterAll,
isSelected: typeFilter == TransactionFilter.all, isSelected: typeFilter == TransactionFilter.all,
onTap: () => ref.read(transactionFilterProvider.notifier).state = TransactionFilter.all, onTap: () => ref.read(transactionFilterProvider.notifier).state = TransactionFilter.all,
), ),
const SizedBox(width: 6), const SizedBox(width: 6),
_FilterChip( _FilterChip(
label: 'Income', label: strings.filterIncome,
isSelected: typeFilter == TransactionFilter.income, isSelected: typeFilter == TransactionFilter.income,
color: AppColors.income, color: AppColors.income,
onTap: () => ref.read(transactionFilterProvider.notifier).state = TransactionFilter.income, onTap: () => ref.read(transactionFilterProvider.notifier).state = TransactionFilter.income,
), ),
const SizedBox(width: 6), const SizedBox(width: 6),
_FilterChip( _FilterChip(
label: 'Expense', label: strings.filterExpense,
isSelected: typeFilter == TransactionFilter.expense, isSelected: typeFilter == TransactionFilter.expense,
color: AppColors.expense, color: AppColors.expense,
onTap: () => ref.read(transactionFilterProvider.notifier).state = TransactionFilter.expense, onTap: () => ref.read(transactionFilterProvider.notifier).state = TransactionFilter.expense,
@@ -1,5 +1,6 @@
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 '../../../core/l10n/app_strings.dart';
import '../provider.dart'; import '../provider.dart';
class SearchBar extends StatelessWidget { class SearchBar extends StatelessWidget {
@@ -7,12 +8,14 @@ class SearchBar extends StatelessWidget {
final FocusNode focusNode; final FocusNode focusNode;
final VoidCallback onTap; final VoidCallback onTap;
final WidgetRef ref; final WidgetRef ref;
final AppStrings strings;
const SearchBar({ const SearchBar({
super.key, super.key,
required this.controller, required this.controller,
required this.focusNode, required this.focusNode,
required this.onTap, required this.onTap,
required this.ref, required this.ref,
required this.strings,
}); });
@override @override
@@ -23,7 +26,7 @@ class SearchBar extends StatelessWidget {
focusNode: focusNode, focusNode: focusNode,
onTap: onTap, onTap: onTap,
decoration: InputDecoration( decoration: InputDecoration(
hintText: 'Search transactions...', hintText: strings.searchHint,
prefixIcon: Icon( prefixIcon: Icon(
Icons.search_rounded, Icons.search_rounded,
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
@@ -1,6 +1,7 @@
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 '../../../core/constants.dart'; import '../../../core/constants.dart';
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';
@@ -10,11 +11,13 @@ class SummaryRow extends StatelessWidget {
final double income; final double income;
final double expense; final double expense;
final CurrencyInfo currencyInfo; final CurrencyInfo currencyInfo;
final AppStrings strings;
const SummaryRow({ const SummaryRow({
super.key, super.key,
required this.income, required this.income,
required this.expense, required this.expense,
required this.currencyInfo, required this.currencyInfo,
required this.strings,
}); });
@override @override
@@ -23,7 +26,7 @@ class SummaryRow extends StatelessWidget {
children: [ children: [
Expanded( Expanded(
child: SummaryCard( child: SummaryCard(
label: 'Income', label: strings.income,
amount: income, amount: income,
color: AppColors.income, color: AppColors.income,
icon: Icons.arrow_downward_rounded, icon: Icons.arrow_downward_rounded,
@@ -33,7 +36,7 @@ class SummaryRow extends StatelessWidget {
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded( Expanded(
child: SummaryCard( child: SummaryCard(
label: 'Expenses', label: strings.expenses,
amount: expense, amount: expense,
color: AppColors.expense, color: AppColors.expense,
icon: Icons.arrow_upward_rounded, icon: Icons.arrow_upward_rounded,
@@ -3,6 +3,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.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/app_strings.dart';
import '../../../core/l10n/locale_provider.dart';
import '../../../shared/models/transaction.dart'; import '../../../shared/models/transaction.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';
@@ -18,6 +20,7 @@ class TransactionTile 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 isIncome = transaction.type == TransactionType.income; final isIncome = transaction.type == TransactionType.income;
final color = isIncome ? AppColors.income : AppColors.expense; final color = isIncome ? AppColors.income : AppColors.expense;
@@ -51,7 +54,7 @@ class TransactionTile extends ConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
transaction.category, s.categoryLabel(transaction.category),
style: Theme.of(context).textTheme.bodyMedium?.copyWith( style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.onSurface, color: Theme.of(context).colorScheme.onSurface,
@@ -69,7 +72,7 @@ class TransactionTile extends ConsumerWidget {
) )
else else
Text( Text(
DateFormat('MMM d, yyyy · HH:mm').format(transaction.date), DateFormat('d MMM yyyy · HH:mm', s.dateLocale).format(transaction.date),
style: Theme.of(context).textTheme.bodySmall?.copyWith( style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of( color: Theme.of(
context, context,
@@ -94,7 +97,8 @@ class TransactionTile extends ConsumerWidget {
} }
class EmptyState extends StatelessWidget { class EmptyState extends StatelessWidget {
const EmptyState({super.key}); final AppStrings strings;
const EmptyState({super.key, required this.strings});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -116,7 +120,7 @@ class EmptyState extends StatelessWidget {
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Text( Text(
'No transactions found', strings.noTransactions,
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,
@@ -124,7 +128,7 @@ class EmptyState extends StatelessWidget {
), ),
const SizedBox(height: 6), const SizedBox(height: 6),
Text( Text(
'Tap + to add your first transaction', strings.addFirstTx,
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),
), ),
+149 -33
View File
@@ -4,6 +4,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.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/app_strings.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/utils/currency_utils.dart';
@@ -54,15 +56,16 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
} }
void _confirmClearData(BuildContext context, WidgetRef ref) { void _confirmClearData(BuildContext context, WidgetRef ref) {
final s = ref.read(stringsProvider);
showDialog( showDialog(
context: context, context: context,
builder: (ctx) => AlertDialog( builder: (ctx) => AlertDialog(
title: const Text('Clear all transactions?'), title: Text(s.clearDataConfirm),
content: const Text('This will permanently delete all your transaction history. This cannot be undone.'), content: Text(s.clearDataWarning),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(ctx), onPressed: () => Navigator.pop(ctx),
child: const Text('Cancel'), child: Text(s.cancel),
), ),
TextButton( TextButton(
onPressed: () { onPressed: () {
@@ -70,30 +73,30 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
showDialog( showDialog(
context: context, context: context,
builder: (ctx2) => AlertDialog( builder: (ctx2) => AlertDialog(
title: const Text('Are you absolutely sure?'), title: Text(s.areYouSure),
content: const Text('All transactions will be deleted forever. There is no way to recover them.'), content: Text(s.allTransactionsWillBeDeleted),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.pop(ctx2), onPressed: () => Navigator.pop(ctx2),
child: const Text('No, keep them'), 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(
const SnackBar(content: Text('All transactions deleted')), SnackBar(content: Text(s.allTransactionsDeleted)),
); );
}, },
style: TextButton.styleFrom(foregroundColor: const Color(0xFFE05C6B)), style: TextButton.styleFrom(foregroundColor: const Color(0xFFE05C6B)),
child: const Text('Yes, delete everything'), child: Text(s.yesDeleteEverything),
), ),
], ],
), ),
); );
}, },
style: TextButton.styleFrom(foregroundColor: const Color(0xFFE05C6B)), style: TextButton.styleFrom(foregroundColor: const Color(0xFFE05C6B)),
child: const Text('Delete'), child: Text(s.delete),
), ),
], ],
), ),
@@ -102,6 +105,7 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final s = ref.watch(stringsProvider);
final budget = ref.watch(budgetProvider); final budget = ref.watch(budgetProvider);
final themeMode = ref.watch(themeProvider); final themeMode = ref.watch(themeProvider);
final isDarkMode = themeMode == ThemeMode.dark; final isDarkMode = themeMode == ThemeMode.dark;
@@ -118,14 +122,14 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
'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(
'Manage your preferences', s.managePreferences,
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),
), ),
@@ -165,14 +169,14 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
'Dark Mode', s.darkMode,
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(
isDarkMode ? 'Enabled' : 'Disabled', 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),
), ),
@@ -223,14 +227,14 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
'Haptic Feedback', s.hapticFeedback,
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(
'Vibration on interactions', s.vibrationOnInteractions,
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),
), ),
@@ -252,6 +256,112 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
const _BiometricSection(), const _BiometricSection(),
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(16),
border: isDark ? null : Border.all(color: const Color(0xFFDDDDEE), width: 1),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: AppColors.accent.withOpacity(0.15),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(
Icons.language_rounded,
color: AppColors.accent,
size: 20,
),
),
const SizedBox(width: 12),
Expanded(
child: Text(
s.language,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.onSurface,
),
),
),
],
),
const SizedBox(height: 16),
Consumer(
builder: (context, ref, _) {
final currentLocale = ref.watch(localeProvider);
return Row(
children: [
Expanded(
child: GestureDetector(
onTap: () => ref.read(localeProvider.notifier).setLocale(AppLocale.en),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: currentLocale == AppLocale.en
? AppColors.accent.withOpacity(0.2)
: Theme.of(context).scaffoldBackgroundColor,
borderRadius: BorderRadius.circular(12),
border: currentLocale == AppLocale.en
? Border.all(color: AppColors.accent, width: 1.5)
: (isDark ? null : Border.all(color: const Color(0xFFDDDDEE), width: 1)),
),
child: Text(
s.langEn,
textAlign: TextAlign.center,
style: TextStyle(
color: currentLocale == AppLocale.en
? AppColors.accent
: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
fontWeight: currentLocale == AppLocale.en ? FontWeight.w600 : FontWeight.normal,
),
),
),
),
),
const SizedBox(width: 8),
Expanded(
child: GestureDetector(
onTap: () => ref.read(localeProvider.notifier).setLocale(AppLocale.ru),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: currentLocale == AppLocale.ru
? AppColors.accent.withOpacity(0.2)
: Theme.of(context).scaffoldBackgroundColor,
borderRadius: BorderRadius.circular(12),
border: currentLocale == AppLocale.ru
? Border.all(color: AppColors.accent, width: 1.5)
: (isDark ? null : Border.all(color: const Color(0xFFDDDDEE), width: 1)),
),
child: Text(
s.langRu,
textAlign: TextAlign.center,
style: TextStyle(
color: currentLocale == AppLocale.ru
? AppColors.accent
: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
fontWeight: currentLocale == AppLocale.ru ? FontWeight.w600 : FontWeight.normal,
),
),
),
),
),
],
);
},
),
],
),
),
const SizedBox(height: 16),
Container( Container(
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
decoration: BoxDecoration( decoration: BoxDecoration(
@@ -279,7 +389,7 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded( Expanded(
child: Text( child: Text(
'Currency', s.currency,
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,
@@ -372,7 +482,7 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded( Expanded(
child: Text( child: Text(
'Amount Format', s.amountFormat,
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,
@@ -455,7 +565,7 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded( Expanded(
child: Text( child: Text(
'Monthly Budget', s.monthlyBudgetSetting,
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,
@@ -486,7 +596,7 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
? '${currencyInfo.symbol} ' ? '${currencyInfo.symbol} '
: currencyInfo.symbol, : currencyInfo.symbol,
hintText: '0.00', hintText: '0.00',
helperText: 'Leave empty to remove budget limit', helperText: s.leaveEmptyToRemove,
), ),
autofocus: true, autofocus: true,
), ),
@@ -500,7 +610,7 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
_budgetController.text = budget?.toStringAsFixed(2) ?? ''; _budgetController.text = budget?.toStringAsFixed(2) ?? '';
setState(() => _isEditing = false); setState(() => _isEditing = false);
}, },
child: const Text('Cancel'), child: Text(s.cancel),
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
ElevatedButton( ElevatedButton(
@@ -508,7 +618,7 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
minimumSize: const Size(80, 40), minimumSize: const Size(80, 40),
), ),
child: const Text('Save'), child: Text(s.save),
), ),
], ],
), ),
@@ -521,7 +631,7 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
Text( Text(
budget != null budget != null
? formatAmount(currencyInfo.symbol, budget, fmt) ? formatAmount(currencyInfo.symbol, budget, fmt)
: 'Not set', : s.budgetNone,
style: Theme.of(context).textTheme.headlineSmall?.copyWith( style: Theme.of(context).textTheme.headlineSmall?.copyWith(
color: budget != null ? AppColors.accent : Theme.of(context).colorScheme.onSurface.withOpacity(0.6), color: budget != null ? AppColors.accent : Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
@@ -530,8 +640,8 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
budget != null budget != null
? 'Your monthly spending limit' ? s.yourMonthlySpendingLimit
: 'Set a monthly spending limit to track your budget', : s.setMonthlySpendingLimit,
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),
), ),
@@ -545,7 +655,7 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
const SizedBox(height: 24), const SizedBox(height: 24),
Text( Text(
'Danger Zone', s.dangerZone,
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
letterSpacing: 1.2, letterSpacing: 1.2,
@@ -559,9 +669,9 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
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: const Text( label: Text(
'Clear All Transactions', s.clearAllTransactions,
style: 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)),
@@ -621,14 +731,14 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
} }
} }
class _BiometricSection extends StatefulWidget { class _BiometricSection extends ConsumerStatefulWidget {
const _BiometricSection(); const _BiometricSection();
@override @override
State<_BiometricSection> createState() => _BiometricSectionState(); ConsumerState<_BiometricSection> createState() => _BiometricSectionState();
} }
class _BiometricSectionState extends State<_BiometricSection> { class _BiometricSectionState extends ConsumerState<_BiometricSection> {
bool _available = false; bool _available = false;
bool _enabled = false; bool _enabled = false;
bool _loading = true; bool _loading = true;
@@ -667,6 +777,10 @@ class _BiometricSectionState extends State<_BiometricSection> {
final isDark = Theme.of(context).brightness == Brightness.dark; final isDark = Theme.of(context).brightness == Brightness.dark;
return Consumer(
builder: (context, ref, _) {
final s = ref.watch(stringsProvider);
return Column( return Column(
children: [ children: [
Container( Container(
@@ -696,14 +810,14 @@ class _BiometricSectionState extends State<_BiometricSection> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
'Biometric Lock', 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(
'Require fingerprint on app launch', 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),
), ),
@@ -722,5 +836,7 @@ class _BiometricSectionState extends State<_BiometricSection> {
const SizedBox(height: 16), const SizedBox(height: 16),
], ],
); );
},
);
} }
} }
+8
View File
@@ -1,5 +1,6 @@
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:intl/date_symbol_data_local.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'app/app.dart'; import 'app/app.dart';
import 'core/services/haptic_service.dart'; import 'core/services/haptic_service.dart';
@@ -7,6 +8,13 @@ import 'features/dashboard/provider.dart';
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
// Initialize date formatting for both locales
await initializeDateFormatting('en_US', null);
await initializeDateFormatting('ru_RU', null);
await initializeDateFormatting('en', null);
await initializeDateFormatting('ru', null);
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
await HapticService.init(); await HapticService.init();