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:
@@ -10,6 +10,7 @@
|
|||||||
- Все настройки цветов выноси в тему
|
- Все настройки цветов выноси в тему
|
||||||
- Все элементы должны разбиваться на мелкие и иметь логичную структуру по попкам
|
- Все элементы должны разбиваться на мелкие и иметь логичную структуру по попкам
|
||||||
- Весь текст должен иметь локализацию чрезе flutter_localizations. Смотри папку l10n
|
- Весь текст должен иметь локализацию чрезе flutter_localizations. Смотри папку l10n
|
||||||
|
- Разработка ведется под windows, ты можешь исопльзовать его консольные команды
|
||||||
|
|
||||||
## Coding Style:
|
## Coding Style:
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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"
|
||||||
}
|
}
|
||||||
@@ -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<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.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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';
|
|
||||||
}
|
|
||||||
@@ -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
@@ -26,5 +26,13 @@
|
|||||||
"transactionsHistoryTitle": "История транзакций",
|
"transactionsHistoryTitle": "История транзакций",
|
||||||
"balance": "Баланс",
|
"balance": "Баланс",
|
||||||
"income": "Доходы",
|
"income": "Доходы",
|
||||||
"expense": "Расходы"
|
"expense": "Расходы",
|
||||||
|
"amount": "Сумма",
|
||||||
|
"vendor": "Название",
|
||||||
|
"category": "Категория",
|
||||||
|
"date": "Дата",
|
||||||
|
"requiredField": "Обязательное поле",
|
||||||
|
"invalidNumber": "Неверный формат числа",
|
||||||
|
"cancel": "Отмена",
|
||||||
|
"save": "Сохранить"
|
||||||
}
|
}
|
||||||
@@ -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
@@ -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];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user