diff --git a/GEMINI.md b/GEMINI.md index d8f8e9f..93a84bf 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -10,6 +10,7 @@ - Все настройки цветов выноси в тему - Все элементы должны разбиваться на мелкие и иметь логичную структуру по попкам - Весь текст должен иметь локализацию чрезе flutter_localizations. Смотри папку l10n +- Разработка ведется под windows, ты можешь исопльзовать его консольные команды ## Coding Style: diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 0000000..fa0b357 --- /dev/null +++ b/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 4ccbd10..8e365e4 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -26,5 +26,13 @@ "transactionsHistoryTitle": "Transactions History", "balance": "Balance", "income": "Income", - "expense": "Expense" + "expense": "Expense", + "amount": "Amount", + "vendor": "Vendor", + "category": "Category", + "date": "Date", + "requiredField": "Required field", + "invalidNumber": "Invalid number", + "cancel": "Cancel", + "save": "Save" } \ No newline at end of file diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart deleted file mode 100644 index c3bf6eb..0000000 --- a/lib/l10n/app_localizations.dart +++ /dev/null @@ -1,296 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter_localizations/flutter_localizations.dart'; -import 'package:intl/intl.dart' as intl; - -import 'app_localizations_en.dart'; -import 'app_localizations_ru.dart'; - -// ignore_for_file: type=lint - -/// Callers can lookup localized strings with an instance of AppLocalizations -/// returned by `AppLocalizations.of(context)`. -/// -/// Applications need to include `AppLocalizations.delegate()` in their app's -/// `localizationDelegates` list, and the locales they support in the app's -/// `supportedLocales` list. For example: -/// -/// ```dart -/// import 'l10n/app_localizations.dart'; -/// -/// return MaterialApp( -/// localizationsDelegates: AppLocalizations.localizationsDelegates, -/// supportedLocales: AppLocalizations.supportedLocales, -/// home: MyApplicationHome(), -/// ); -/// ``` -/// -/// ## Update pubspec.yaml -/// -/// Please make sure to update your pubspec.yaml to include the following -/// packages: -/// -/// ```yaml -/// dependencies: -/// # Internationalization support. -/// flutter_localizations: -/// sdk: flutter -/// intl: any # Use the pinned version from flutter_localizations -/// -/// # Rest of dependencies -/// ``` -/// -/// ## iOS Applications -/// -/// iOS applications define key application metadata, including supported -/// locales, in an Info.plist file that is built into the application bundle. -/// To configure the locales supported by your app, you’ll need to edit this -/// file. -/// -/// First, open your project’s ios/Runner.xcworkspace Xcode workspace file. -/// Then, in the Project Navigator, open the Info.plist file under the Runner -/// project’s Runner folder. -/// -/// Next, select the Information Property List item, select Add Item from the -/// Editor menu, then select Localizations from the pop-up menu. -/// -/// Select and expand the newly-created Localizations item then, for each -/// locale your application supports, add a new item and select the locale -/// you wish to add from the pop-up menu in the Value field. This list should -/// be consistent with the languages listed in the AppLocalizations.supportedLocales -/// property. -abstract class AppLocalizations { - AppLocalizations(String locale) - : localeName = intl.Intl.canonicalizedLocale(locale.toString()); - - final String localeName; - - static AppLocalizations? of(BuildContext context) { - return Localizations.of(context, AppLocalizations); - } - - static const LocalizationsDelegate delegate = - _AppLocalizationsDelegate(); - - /// A list of this localizations delegate along with the default localizations - /// delegates. - /// - /// Returns a list of localizations delegates containing this delegate along with - /// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate, - /// and GlobalWidgetsLocalizations.delegate. - /// - /// Additional delegates can be added by appending to this list in - /// MaterialApp. This list does not have to be used at all if a custom list - /// of delegates is preferred or required. - static const List> localizationsDelegates = - >[ - delegate, - GlobalMaterialLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - ]; - - /// A list of this localizations delegate's supported locales. - static const List supportedLocales = [ - Locale('en'), - Locale('ru'), - ]; - - /// No description provided for @appTitle. - /// - /// In en, this message translates to: - /// **'Budget App'** - String get appTitle; - - /// No description provided for @homePageTitle. - /// - /// In en, this message translates to: - /// **'Home'** - String get homePageTitle; - - /// No description provided for @reportsPageTitle. - /// - /// In en, this message translates to: - /// **'Reports'** - String get reportsPageTitle; - - /// No description provided for @settingsPageTitle. - /// - /// In en, this message translates to: - /// **'Settings'** - String get settingsPageTitle; - - /// No description provided for @addTransactionButton. - /// - /// In en, this message translates to: - /// **'Add new transaction'** - String get addTransactionButton; - - /// No description provided for @noTransactionsText. - /// - /// In en, this message translates to: - /// **'No transactions yet'** - String get noTransactionsText; - - /// No description provided for @loadingTransactionsText. - /// - /// In en, this message translates to: - /// **'Loading transactions...'** - String get loadingTransactionsText; - - /// No description provided for @transactionErrorText. - /// - /// In en, this message translates to: - /// **'Error: {message}'** - String transactionErrorText(Object message); - - /// No description provided for @loginPageTitle. - /// - /// In en, this message translates to: - /// **'Login'** - String get loginPageTitle; - - /// No description provided for @nameFieldLabel. - /// - /// In en, this message translates to: - /// **'Name'** - String get nameFieldLabel; - - /// No description provided for @emailFieldLabel. - /// - /// In en, this message translates to: - /// **'Email'** - String get emailFieldLabel; - - /// No description provided for @nameFieldEmptyError. - /// - /// In en, this message translates to: - /// **'Please enter your name'** - String get nameFieldEmptyError; - - /// No description provided for @emailFieldEmptyError. - /// - /// In en, this message translates to: - /// **'Please enter your email'** - String get emailFieldEmptyError; - - /// No description provided for @loginButtonText. - /// - /// In en, this message translates to: - /// **'Login / Register'** - String get loginButtonText; - - /// No description provided for @darkModeSetting. - /// - /// In en, this message translates to: - /// **'Dark Theme'** - String get darkModeSetting; - - /// No description provided for @darkModeDescription. - /// - /// In en, this message translates to: - /// **'Toggle between light and dark theme'** - String get darkModeDescription; - - /// No description provided for @languageSetting. - /// - /// In en, this message translates to: - /// **'Language'** - String get languageSetting; - - /// No description provided for @languageDescription. - /// - /// In en, this message translates to: - /// **'Change application language'** - String get languageDescription; - - /// No description provided for @russianLanguage. - /// - /// In en, this message translates to: - /// **'Russian'** - String get russianLanguage; - - /// No description provided for @englishLanguage. - /// - /// In en, this message translates to: - /// **'English'** - String get englishLanguage; - - /// No description provided for @defaultUser. - /// - /// In en, this message translates to: - /// **'Default User'** - String get defaultUser; - - /// No description provided for @currencySetting. - /// - /// In en, this message translates to: - /// **'Default Currency'** - String get currencySetting; - - /// No description provided for @currencyDescription. - /// - /// In en, this message translates to: - /// **'Set the default currency for transactions'** - String get currencyDescription; - - /// No description provided for @transactionsHistoryTitle. - /// - /// In en, this message translates to: - /// **'Transactions History'** - String get transactionsHistoryTitle; - - /// No description provided for @balance. - /// - /// In en, this message translates to: - /// **'Balance'** - String get balance; - - /// No description provided for @income. - /// - /// In en, this message translates to: - /// **'Income'** - String get income; - - /// No description provided for @expense. - /// - /// In en, this message translates to: - /// **'Expense'** - String get expense; -} - -class _AppLocalizationsDelegate - extends LocalizationsDelegate { - const _AppLocalizationsDelegate(); - - @override - Future load(Locale locale) { - return SynchronousFuture(lookupAppLocalizations(locale)); - } - - @override - bool isSupported(Locale locale) => - ['en', 'ru'].contains(locale.languageCode); - - @override - bool shouldReload(_AppLocalizationsDelegate old) => false; -} - -AppLocalizations lookupAppLocalizations(Locale locale) { - // Lookup logic when only language code is specified. - switch (locale.languageCode) { - case 'en': - return AppLocalizationsEn(); - case 'ru': - return AppLocalizationsRu(); - } - - throw FlutterError( - 'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely ' - 'an issue with the localizations generation tool. Please file an issue ' - 'on GitHub with a reproducible sample app and the gen-l10n configuration ' - 'that was used.', - ); -} diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart deleted file mode 100644 index 46a21c7..0000000 --- a/lib/l10n/app_localizations_en.dart +++ /dev/null @@ -1,93 +0,0 @@ -// ignore: unused_import -import 'package:intl/intl.dart' as intl; -import 'app_localizations.dart'; - -// ignore_for_file: type=lint - -/// The translations for English (`en`). -class AppLocalizationsEn extends AppLocalizations { - AppLocalizationsEn([String locale = 'en']) : super(locale); - - @override - String get appTitle => 'Budget App'; - - @override - String get homePageTitle => 'Home'; - - @override - String get reportsPageTitle => 'Reports'; - - @override - String get settingsPageTitle => 'Settings'; - - @override - String get addTransactionButton => 'Add new transaction'; - - @override - String get noTransactionsText => 'No transactions yet'; - - @override - String get loadingTransactionsText => 'Loading transactions...'; - - @override - String transactionErrorText(Object message) { - return 'Error: $message'; - } - - @override - String get loginPageTitle => 'Login'; - - @override - String get nameFieldLabel => 'Name'; - - @override - String get emailFieldLabel => 'Email'; - - @override - String get nameFieldEmptyError => 'Please enter your name'; - - @override - String get emailFieldEmptyError => 'Please enter your email'; - - @override - String get loginButtonText => 'Login / Register'; - - @override - String get darkModeSetting => 'Dark Theme'; - - @override - String get darkModeDescription => 'Toggle between light and dark theme'; - - @override - String get languageSetting => 'Language'; - - @override - String get languageDescription => 'Change application language'; - - @override - String get russianLanguage => 'Russian'; - - @override - String get englishLanguage => 'English'; - - @override - String get defaultUser => 'Default User'; - - @override - String get currencySetting => 'Default Currency'; - - @override - String get currencyDescription => 'Set the default currency for transactions'; - - @override - String get transactionsHistoryTitle => 'Transactions History'; - - @override - String get balance => 'Balance'; - - @override - String get income => 'Income'; - - @override - String get expense => 'Expense'; -} diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart deleted file mode 100644 index aee81cf..0000000 --- a/lib/l10n/app_localizations_ru.dart +++ /dev/null @@ -1,94 +0,0 @@ -// ignore: unused_import -import 'package:intl/intl.dart' as intl; -import 'app_localizations.dart'; - -// ignore_for_file: type=lint - -/// The translations for Russian (`ru`). -class AppLocalizationsRu extends AppLocalizations { - AppLocalizationsRu([String locale = 'ru']) : super(locale); - - @override - String get appTitle => 'Бюджетное приложение'; - - @override - String get homePageTitle => 'Главная'; - - @override - String get reportsPageTitle => 'Отчеты'; - - @override - String get settingsPageTitle => 'Настройки'; - - @override - String get addTransactionButton => 'Добавить новую транзакцию'; - - @override - String get noTransactionsText => 'Нет транзакций'; - - @override - String get loadingTransactionsText => 'Загрузка транзакций...'; - - @override - String transactionErrorText(Object message) { - return 'Ошибка: $message'; - } - - @override - String get loginPageTitle => 'Вход'; - - @override - String get nameFieldLabel => 'Имя'; - - @override - String get emailFieldLabel => 'Email'; - - @override - String get nameFieldEmptyError => 'Пожалуйста, введите имя'; - - @override - String get emailFieldEmptyError => 'Пожалуйста, введите email'; - - @override - String get loginButtonText => 'Войти / Зарегистрироваться'; - - @override - String get darkModeSetting => 'Темная тема'; - - @override - String get darkModeDescription => 'Переключить между светлой и темной темой'; - - @override - String get languageSetting => 'Язык'; - - @override - String get languageDescription => 'Изменить язык приложения'; - - @override - String get russianLanguage => 'Русский'; - - @override - String get englishLanguage => 'Английский'; - - @override - String get defaultUser => 'Пользователь по умолчанию'; - - @override - String get currencySetting => 'Валюта по умолчанию'; - - @override - String get currencyDescription => - 'Установить валюту по умолчанию для транзакций'; - - @override - String get transactionsHistoryTitle => 'История транзакций'; - - @override - String get balance => 'Баланс'; - - @override - String get income => 'Доходы'; - - @override - String get expense => 'Расходы'; -} diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 710a626..5edc8c2 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -26,5 +26,13 @@ "transactionsHistoryTitle": "История транзакций", "balance": "Баланс", "income": "Доходы", - "expense": "Расходы" + "expense": "Расходы", + "amount": "Сумма", + "vendor": "Название", + "category": "Категория", + "date": "Дата", + "requiredField": "Обязательное поле", + "invalidNumber": "Неверный формат числа", + "cancel": "Отмена", + "save": "Сохранить" } \ No newline at end of file diff --git a/lib/models/category.dart b/lib/models/category.dart index 8c0045c..99c2817 100644 --- a/lib/models/category.dart +++ b/lib/models/category.dart @@ -1,3 +1,4 @@ +import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; import 'package:hive_ce/hive.dart'; import '../utils/id_generator.dart'; @@ -8,7 +9,7 @@ part 'category.g.dart'; /// Модель категории для группировки транзакций /// Содержит основные параметры для визуализации и классификации -class Category { +class Category extends Equatable { /// Уникальный идентификатор категории @HiveField(0) final String id; @@ -97,4 +98,9 @@ class Category { updatedAt: DateTime.now(), // Обновляем updatedAt при каждом копировании ); } + + // Используем Equatable для сравнения объектов по их свойствам. + // В данном случае, мы считаем категории уникальными по их 'id'. + @override + List get props => [id]; } diff --git a/lib/models/tag.dart b/lib/models/tag.dart index 51f743d..07eb45b 100644 --- a/lib/models/tag.dart +++ b/lib/models/tag.dart @@ -1,10 +1,11 @@ +import 'package:equatable/equatable.dart'; import 'package:hive_ce/hive.dart'; import '../../utils/id_generator.dart'; part 'tag.g.dart'; @HiveType(typeId: 1001) -class Tag { +class Tag extends Equatable { @HiveField(0) /// Уникальный идентификатор тега final String id; @@ -71,4 +72,9 @@ class Tag { updatedAt: DateTime.now(), // Обновляем updatedAt при каждом копировании ); } + + // Используем Equatable для сравнения объектов по их свойствам. + // В данном случае, мы считаем теги уникальными по их 'id'. + @override + List get props => [id]; } diff --git a/lib/models/transaction_record.dart b/lib/models/transaction_record.dart index afcf3a1..53f670e 100644 --- a/lib/models/transaction_record.dart +++ b/lib/models/transaction_record.dart @@ -1,3 +1,4 @@ +import 'package:equatable/equatable.dart'; import 'package:hive_ce/hive.dart'; import '../../utils/id_generator.dart'; @@ -9,7 +10,7 @@ part 'transaction_record.g.dart'; @HiveType(typeId: 1002) /// Модель записи о транзакции - основной элемент учета бюджета /// Содержит все детали финансовой операции -class TransactionRecord { +class TransactionRecord extends Equatable { /// Уникальный идентификатор транзакции @HiveField(0) final String id; @@ -121,4 +122,9 @@ class TransactionRecord { updatedAt: DateTime.now(), // Обновляем updatedAt при каждом копировании ); } + + // Используем Equatable для сравнения объектов по их свойствам. + // В данном случае, мы считаем записи транзакций уникальными по их 'id'. + @override + List get props => [id]; } diff --git a/lib/models/user.dart b/lib/models/user.dart index ebcc600..b21bae0 100644 --- a/lib/models/user.dart +++ b/lib/models/user.dart @@ -1,3 +1,4 @@ +import 'package:equatable/equatable.dart'; import 'package:hive_ce/hive.dart'; import '../utils/id_generator.dart'; @@ -8,7 +9,7 @@ part 'user.g.dart'; @HiveType(typeId: 1003) /// Модель пользователя приложения /// Содержит основную информацию для идентификации пользователя -class User { +class User extends Equatable { /// Уникальный идентификатор пользователя @HiveField(0) // Поле 0 в Hive - первое поле модели final String id; @@ -98,4 +99,9 @@ class User { defaultCurrency: defaultCurrency ?? this.defaultCurrency, ); } + + // Используем Equatable для сравнения объектов по их свойствам. + // В данном случае, мы считаем пользователей уникальными по их 'id'. + @override + List get props => [id]; } diff --git a/lib/pages/home/home_page.dart b/lib/pages/home/home_page.dart index d7dd992..29e7d5c 100644 --- a/lib/pages/home/home_page.dart +++ b/lib/pages/home/home_page.dart @@ -5,10 +5,11 @@ import 'package:get_it/get_it.dart'; import '/l10n/app_localizations.dart'; import '../../logic/auth/auth_bloc.dart'; import '../../logic/transaction/transaction_bloc.dart'; +import '../../services/user_service.dart'; import '../home/widgets/summary_widget.dart'; // Импортируем новый виджет сводки import '../reports_page.dart'; // Импортируем новую страницу отчетов import '../settings_page.dart'; -import '../../services/user_service.dart'; // Добавлен импорт для UserService +import 'widgets/add_transaction_dialog.dart'; // Добавлен импорт для UserService class HomePage extends StatefulWidget { const HomePage({super.key}); @@ -70,11 +71,11 @@ class _HomePageState extends State { ), floatingActionButton: FloatingActionButton( onPressed: () { - // TODO: Добавить логику для добавления новой транзакции - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(localizations.addTransactionButton), - ), // Локализованный текст + showDialog( + context: context, + builder: (BuildContext context) { + return const AddTransactionDialog(); + }, ); }, child: const Icon(Icons.add), @@ -137,7 +138,9 @@ class TransactionsPage extends StatelessWidget { final balance = income - expense; // Получаем текущего пользователя для определения валюты final userService = GetIt.instance(); - final currencySymbol = userService.currentUser?.defaultCurrency ?? '₽'; // Валюта по умолчанию, если не найдена + final currencySymbol = + userService.currentUser?.defaultCurrency ?? + '₽'; // Валюта по умолчанию, если не найдена return SummaryWidget( income: income, diff --git a/lib/pages/home/widgets/add_transaction_dialog.dart b/lib/pages/home/widgets/add_transaction_dialog.dart new file mode 100644 index 0000000..7eb7dd4 --- /dev/null +++ b/lib/pages/home/widgets/add_transaction_dialog.dart @@ -0,0 +1,200 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:get_it/get_it.dart'; +import 'package:intl/intl.dart'; + +import '../../../l10n/app_localizations.dart'; +import '../../../logic/auth/auth_bloc.dart'; +import '../../../logic/transaction/transaction_bloc.dart'; +import '../../../models/category.dart'; +import '../../../models/transaction_record.dart'; +import '../../../services/user_service.dart'; +import '../../../utils/category_utils.dart'; + +class AddTransactionDialog extends StatefulWidget { + const AddTransactionDialog({super.key}); + + @override + State createState() => _AddTransactionDialogState(); +} + +class _AddTransactionDialogState extends State { + final _formKey = GlobalKey(); + final _amountController = TextEditingController(); + final _vendorController = TextEditingController(); + final _dateController = TextEditingController(); + + bool _isIncome = false; + Category? _selectedCategory; + DateTime _selectedDate = DateTime.now(); + + @override + void initState() { + super.initState(); + _dateController.text = DateFormat.yMd().format(_selectedDate); + } + + @override + void dispose() { + _amountController.dispose(); + _vendorController.dispose(); + _dateController.dispose(); + super.dispose(); + } + + Future _selectDate(BuildContext context) async { + final DateTime? picked = await showDatePicker( + context: context, + initialDate: _selectedDate, + firstDate: DateTime(2000), + lastDate: DateTime(2101), + ); + if (picked != null && picked != _selectedDate) { + setState(() { + _selectedDate = picked; + _dateController.text = DateFormat.yMd().format(_selectedDate); + }); + } + } + + void _submitForm() { + if (_formKey.currentState!.validate()) { + final amount = double.tryParse(_amountController.text); + if (amount == null || _selectedCategory == null) { + // Показать ошибку, если сумма некорректна или категория не выбрана + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + AppLocalizations.of( + context, + )!.transactionErrorText('Invalid data'), + ), + ), + ); + return; + } + + final authState = context.read().state; + if (authState is! AuthAuthenticated) { + // Показать ошибку, если пользователь не аутентифицирован + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + AppLocalizations.of( + context, + )!.transactionErrorText('User not authenticated'), + ), + ), + ); + return; + } + + final newTransaction = TransactionRecord( + amount: amount, + vendor: _vendorController.text, + category: _selectedCategory!, + dateTime: _selectedDate, + currency: + GetIt.instance().currentUser?.defaultCurrency ?? 'USD', + userId: authState.user.id, + ); + + context.read().add( + AddTransaction(transaction: newTransaction), + ); + Navigator.of(context).pop(); + } + } + + @override + Widget build(BuildContext context) { + final localizations = AppLocalizations.of(context)!; + final categories = CategoryUtils.getDefaultCategories( + (context.read().state as AuthAuthenticated).user.id, + ).where((c) => c.isIncome == _isIncome).toList(); + + return AlertDialog( + title: Text(localizations.addTransactionButton), + content: Form( + key: _formKey, + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SwitchListTile( + title: Text(localizations.income), + value: _isIncome, + onChanged: (bool value) { + setState(() { + _isIncome = value; + _selectedCategory = + null; // Сбрасываем категорию при смене типа + }); + }, + ), + TextFormField( + controller: _amountController, + decoration: InputDecoration(labelText: localizations.amount), + keyboardType: TextInputType.number, + validator: (value) { + if (value == null || value.isEmpty) { + return localizations.requiredField; + } + if (double.tryParse(value) == null) { + return localizations.invalidNumber; + } + return null; + }, + ), + TextFormField( + controller: _vendorController, + decoration: InputDecoration(labelText: localizations.vendor), + validator: (value) { + if (value == null || value.isEmpty) { + return localizations.requiredField; + } + return null; + }, + ), + DropdownButtonFormField( + value: _selectedCategory, + decoration: InputDecoration(labelText: localizations.category), + items: categories.map((Category category) { + return DropdownMenuItem( + value: category, + child: Text(category.name), + ); + }).toList(), + onChanged: (Category? newValue) { + setState(() { + _selectedCategory = newValue; + }); + }, + validator: (value) => + value == null ? localizations.requiredField : null, + ), + TextFormField( + controller: _dateController, + decoration: InputDecoration( + labelText: localizations.date, + suffixIcon: IconButton( + icon: const Icon(Icons.calendar_today), + onPressed: () => _selectDate(context), + ), + ), + readOnly: true, + ), + ], + ), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(localizations.cancel), + ), + ElevatedButton(onPressed: _submitForm, child: Text(localizations.save)), + ], + ); + } +} diff --git a/lib/theme/custom_colors.dart b/lib/theme/custom_colors.dart new file mode 100644 index 0000000..43d6bdd --- /dev/null +++ b/lib/theme/custom_colors.dart @@ -0,0 +1,33 @@ + +import 'package:flutter/material.dart'; + +// Расширение темы для добавления пользовательских цветов. +@immutable +class CustomColors extends ThemeExtension { + const CustomColors({ + required this.income, + required this.expense, + }); + + final Color? income; + final Color? expense; + + @override + CustomColors copyWith({Color? income, Color? expense}) { + return CustomColors( + income: income ?? this.income, + expense: expense ?? this.expense, + ); + } + + @override + CustomColors lerp(ThemeExtension? other, double t) { + if (other is! CustomColors) { + return this; + } + return CustomColors( + income: Color.lerp(income, other.income, t), + expense: Color.lerp(expense, other.expense, t), + ); + } +}