Improves: Internationalization and data models

- Adds new localization keys for transaction details.
- Deletes obsolete localization files, switching to a single ARB file per language.
- Adds `Equatable` to data models (`Category`, `Tag`, `TransactionRecord`, `User`) for improved state management and change detection.
- Introduces the transaction creation dialog.
- Adds development note to GEMINI.md

Improves internationalization and data models

- Adds new localization keys for transaction details, supporting enhanced user experience.
- Migrates to single ARB file for each language, streamlining the localization process.
- Implements Equatable in data models (Category, Tag, TransactionRecord, User) for improved state management and simplified change detection.
- Introduces transaction creation dialog, simplifying the transaction creation process.
- Adds development note to GEMINI.md
This commit is contained in:
2025-06-29 23:03:17 +03:00
parent e0cb2d1e2c
commit 42f8e20bf0
14 changed files with 293 additions and 496 deletions
+1
View File
@@ -10,6 +10,7 @@
- Все настройки цветов выноси в тему - Все настройки цветов выноси в тему
- Все элементы должны разбиваться на мелкие и иметь логичную структуру по попкам - Все элементы должны разбиваться на мелкие и иметь логичную структуру по попкам
- Весь текст должен иметь локализацию чрезе flutter_localizations. Смотри папку l10n - Весь текст должен иметь локализацию чрезе flutter_localizations. Смотри папку l10n
- Разработка ведется под windows, ты можешь исопльзовать его консольные команды
## Coding Style: ## Coding Style:
+3
View File
@@ -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:
+9 -1
View File
@@ -26,5 +26,13 @@
"transactionsHistoryTitle": "Transactions History", "transactionsHistoryTitle": "Transactions History",
"balance": "Balance", "balance": "Balance",
"income": "Income", "income": "Income",
"expense": "Expense" "expense": "Expense",
"amount": "Amount",
"vendor": "Vendor",
"category": "Category",
"date": "Date",
"requiredField": "Required field",
"invalidNumber": "Invalid number",
"cancel": "Cancel",
"save": "Save"
} }
-296
View File
@@ -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, youll need to edit this
/// file.
///
/// First, open your projects ios/Runner.xcworkspace Xcode workspace file.
/// Then, in the Project Navigator, open the Info.plist file under the Runner
/// projects 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<AppLocalizations>(context, AppLocalizations);
}
static const LocalizationsDelegate<AppLocalizations> 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<LocalizationsDelegate<dynamic>> localizationsDelegates =
<LocalizationsDelegate<dynamic>>[
delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
];
/// A list of this localizations delegate's supported locales.
static const List<Locale> supportedLocales = <Locale>[
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<AppLocalizations> {
const _AppLocalizationsDelegate();
@override
Future<AppLocalizations> load(Locale locale) {
return SynchronousFuture<AppLocalizations>(lookupAppLocalizations(locale));
}
@override
bool isSupported(Locale locale) =>
<String>['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.',
);
}
-93
View File
@@ -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';
}
-94
View File
@@ -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 => 'Расходы';
}
+9 -1
View File
@@ -26,5 +26,13 @@
"transactionsHistoryTitle": "История транзакций", "transactionsHistoryTitle": "История транзакций",
"balance": "Баланс", "balance": "Баланс",
"income": "Доходы", "income": "Доходы",
"expense": "Расходы" "expense": "Расходы",
"amount": "Сумма",
"vendor": "Название",
"category": "Категория",
"date": "Дата",
"requiredField": "Обязательное поле",
"invalidNumber": "Неверный формат числа",
"cancel": "Отмена",
"save": "Сохранить"
} }
+7 -1
View File
@@ -1,3 +1,4 @@
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hive_ce/hive.dart'; import 'package:hive_ce/hive.dart';
import '../utils/id_generator.dart'; import '../utils/id_generator.dart';
@@ -8,7 +9,7 @@ part 'category.g.dart';
/// Модель категории для группировки транзакций /// Модель категории для группировки транзакций
/// Содержит основные параметры для визуализации и классификации /// Содержит основные параметры для визуализации и классификации
class Category { class Category extends Equatable {
/// Уникальный идентификатор категории /// Уникальный идентификатор категории
@HiveField(0) @HiveField(0)
final String id; final String id;
@@ -97,4 +98,9 @@ class Category {
updatedAt: DateTime.now(), // Обновляем updatedAt при каждом копировании updatedAt: DateTime.now(), // Обновляем updatedAt при каждом копировании
); );
} }
// Используем Equatable для сравнения объектов по их свойствам.
// В данном случае, мы считаем категории уникальными по их 'id'.
@override
List<Object?> get props => [id];
} }
+7 -1
View File
@@ -1,10 +1,11 @@
import 'package:equatable/equatable.dart';
import 'package:hive_ce/hive.dart'; import 'package:hive_ce/hive.dart';
import '../../utils/id_generator.dart'; import '../../utils/id_generator.dart';
part 'tag.g.dart'; part 'tag.g.dart';
@HiveType(typeId: 1001) @HiveType(typeId: 1001)
class Tag { class Tag extends Equatable {
@HiveField(0) @HiveField(0)
/// Уникальный идентификатор тега /// Уникальный идентификатор тега
final String id; final String id;
@@ -71,4 +72,9 @@ class Tag {
updatedAt: DateTime.now(), // Обновляем updatedAt при каждом копировании updatedAt: DateTime.now(), // Обновляем updatedAt при каждом копировании
); );
} }
// Используем Equatable для сравнения объектов по их свойствам.
// В данном случае, мы считаем теги уникальными по их 'id'.
@override
List<Object?> get props => [id];
} }
+7 -1
View File
@@ -1,3 +1,4 @@
import 'package:equatable/equatable.dart';
import 'package:hive_ce/hive.dart'; import 'package:hive_ce/hive.dart';
import '../../utils/id_generator.dart'; import '../../utils/id_generator.dart';
@@ -9,7 +10,7 @@ part 'transaction_record.g.dart';
@HiveType(typeId: 1002) @HiveType(typeId: 1002)
/// Модель записи о транзакции - основной элемент учета бюджета /// Модель записи о транзакции - основной элемент учета бюджета
/// Содержит все детали финансовой операции /// Содержит все детали финансовой операции
class TransactionRecord { class TransactionRecord extends Equatable {
/// Уникальный идентификатор транзакции /// Уникальный идентификатор транзакции
@HiveField(0) @HiveField(0)
final String id; final String id;
@@ -121,4 +122,9 @@ class TransactionRecord {
updatedAt: DateTime.now(), // Обновляем updatedAt при каждом копировании updatedAt: DateTime.now(), // Обновляем updatedAt при каждом копировании
); );
} }
// Используем Equatable для сравнения объектов по их свойствам.
// В данном случае, мы считаем записи транзакций уникальными по их 'id'.
@override
List<Object?> get props => [id];
} }
+7 -1
View File
@@ -1,3 +1,4 @@
import 'package:equatable/equatable.dart';
import 'package:hive_ce/hive.dart'; import 'package:hive_ce/hive.dart';
import '../utils/id_generator.dart'; import '../utils/id_generator.dart';
@@ -8,7 +9,7 @@ part 'user.g.dart';
@HiveType(typeId: 1003) @HiveType(typeId: 1003)
/// Модель пользователя приложения /// Модель пользователя приложения
/// Содержит основную информацию для идентификации пользователя /// Содержит основную информацию для идентификации пользователя
class User { class User extends Equatable {
/// Уникальный идентификатор пользователя /// Уникальный идентификатор пользователя
@HiveField(0) // Поле 0 в Hive - первое поле модели @HiveField(0) // Поле 0 в Hive - первое поле модели
final String id; final String id;
@@ -98,4 +99,9 @@ class User {
defaultCurrency: defaultCurrency ?? this.defaultCurrency, defaultCurrency: defaultCurrency ?? this.defaultCurrency,
); );
} }
// Используем Equatable для сравнения объектов по их свойствам.
// В данном случае, мы считаем пользователей уникальными по их 'id'.
@override
List<Object?> get props => [id];
} }
+10 -7
View File
@@ -5,10 +5,11 @@ import 'package:get_it/get_it.dart';
import '/l10n/app_localizations.dart'; import '/l10n/app_localizations.dart';
import '../../logic/auth/auth_bloc.dart'; import '../../logic/auth/auth_bloc.dart';
import '../../logic/transaction/transaction_bloc.dart'; import '../../logic/transaction/transaction_bloc.dart';
import '../../services/user_service.dart';
import '../home/widgets/summary_widget.dart'; // Импортируем новый виджет сводки import '../home/widgets/summary_widget.dart'; // Импортируем новый виджет сводки
import '../reports_page.dart'; // Импортируем новую страницу отчетов import '../reports_page.dart'; // Импортируем новую страницу отчетов
import '../settings_page.dart'; import '../settings_page.dart';
import '../../services/user_service.dart'; // Добавлен импорт для UserService import 'widgets/add_transaction_dialog.dart'; // Добавлен импорт для UserService
class HomePage extends StatefulWidget { class HomePage extends StatefulWidget {
const HomePage({super.key}); const HomePage({super.key});
@@ -70,11 +71,11 @@ class _HomePageState extends State<HomePage> {
), ),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
onPressed: () { onPressed: () {
// TODO: Добавить логику для добавления новой транзакции showDialog(
ScaffoldMessenger.of(context).showSnackBar( context: context,
SnackBar( builder: (BuildContext context) {
content: Text(localizations.addTransactionButton), return const AddTransactionDialog();
), // Локализованный текст },
); );
}, },
child: const Icon(Icons.add), child: const Icon(Icons.add),
@@ -137,7 +138,9 @@ class TransactionsPage extends StatelessWidget {
final balance = income - expense; final balance = income - expense;
// Получаем текущего пользователя для определения валюты // Получаем текущего пользователя для определения валюты
final userService = GetIt.instance<UserService>(); final userService = GetIt.instance<UserService>();
final currencySymbol = userService.currentUser?.defaultCurrency ?? ''; // Валюта по умолчанию, если не найдена final currencySymbol =
userService.currentUser?.defaultCurrency ??
''; // Валюта по умолчанию, если не найдена
return SummaryWidget( return SummaryWidget(
income: income, income: income,
@@ -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<AddTransactionDialog> createState() => _AddTransactionDialogState();
}
class _AddTransactionDialogState extends State<AddTransactionDialog> {
final _formKey = GlobalKey<FormState>();
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<void> _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<AuthBloc>().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<UserService>().currentUser?.defaultCurrency ?? 'USD',
userId: authState.user.id,
);
context.read<TransactionBloc>().add(
AddTransaction(transaction: newTransaction),
);
Navigator.of(context).pop();
}
}
@override
Widget build(BuildContext context) {
final localizations = AppLocalizations.of(context)!;
final categories = CategoryUtils.getDefaultCategories(
(context.read<AuthBloc>().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<Category>(
value: _selectedCategory,
decoration: InputDecoration(labelText: localizations.category),
items: categories.map((Category category) {
return DropdownMenuItem<Category>(
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)),
],
);
}
}
+33
View File
@@ -0,0 +1,33 @@
import 'package:flutter/material.dart';
// Расширение темы для добавления пользовательских цветов.
@immutable
class CustomColors extends ThemeExtension<CustomColors> {
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<CustomColors>? 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),
);
}
}