diff --git a/l10n.yaml b/l10n.yaml new file mode 100644 index 0000000..15338f2 --- /dev/null +++ b/l10n.yaml @@ -0,0 +1,3 @@ +arb-dir: lib/l10n +template-arb-file: app_en.arb +output-localization-file: app_localizations.dart diff --git a/lib/data/repositories/hive_category_repository.dart b/lib/data/repositories/hive_category_repository.dart index dc4d876..e9277e4 100644 --- a/lib/data/repositories/hive_category_repository.dart +++ b/lib/data/repositories/hive_category_repository.dart @@ -65,4 +65,12 @@ class HiveCategoryRepository implements ICategoryRepository { .where((c) => c.userId == userId && !c.isIncome) .toList(); } + + @override + Future addAll(List categories) async { + final Map categoryMap = { + for (var cat in categories) cat.id: cat + }; + await _box.putAll(categoryMap); + } } diff --git a/lib/data/repositories/hive_tag_repository.dart b/lib/data/repositories/hive_tag_repository.dart index 7dd5ffc..7b4ff47 100644 --- a/lib/data/repositories/hive_tag_repository.dart +++ b/lib/data/repositories/hive_tag_repository.dart @@ -40,4 +40,12 @@ class HiveTagRepository implements ITagRepository { // у которых поле `userId` совпадает с идентификатором, переданным в метод. return _box.values.where((tag) => tag.userId == userId).toList(); } + + @override + Future addAll(List tags) async { + final Map tagMap = { + for (var tag in tags) tag.id: tag + }; + await _box.putAll(tagMap); + } } diff --git a/lib/data/repositories/hive_transaction_repository.dart b/lib/data/repositories/hive_transaction_repository.dart index 593883c..36b0457 100644 --- a/lib/data/repositories/hive_transaction_repository.dart +++ b/lib/data/repositories/hive_transaction_repository.dart @@ -93,4 +93,12 @@ class HiveTransactionRepository implements ITransactionRepository { .where((t) => t.userId == userId && t.tag?.id == tagId) .toList(); } + + @override + Future addAll(List transactions) async { + final Map transactionMap = { + for (var tr in transactions) tr.id: tr + }; + await _box.putAll(transactionMap); + } } diff --git a/lib/data/repositories/interfaces/icategory_repository.dart b/lib/data/repositories/interfaces/icategory_repository.dart index ec41521..a2eb91c 100644 --- a/lib/data/repositories/interfaces/icategory_repository.dart +++ b/lib/data/repositories/interfaces/icategory_repository.dart @@ -19,4 +19,7 @@ abstract class ICategoryRepository { /// Получить категории расходов конкретного пользователя Future> getExpenseCategoriesByUser(String userId); + + /// Добавить список категорий + Future addAll(List categories); } diff --git a/lib/data/repositories/interfaces/itag_repository.dart b/lib/data/repositories/interfaces/itag_repository.dart index cf5c73d..266e9a3 100644 --- a/lib/data/repositories/interfaces/itag_repository.dart +++ b/lib/data/repositories/interfaces/itag_repository.dart @@ -12,4 +12,7 @@ abstract class ITagRepository { // реализацию этого метода. Это гарантирует, что наш репозиторий // сможет получать теги для конкретного пользователя. Future> getAllByUser(String userId); + + /// Добавить список тегов + Future addAll(List tags); } diff --git a/lib/data/repositories/interfaces/itransaction_repository.dart b/lib/data/repositories/interfaces/itransaction_repository.dart index 1e36afd..876652d 100644 --- a/lib/data/repositories/interfaces/itransaction_repository.dart +++ b/lib/data/repositories/interfaces/itransaction_repository.dart @@ -17,4 +17,7 @@ abstract class ITransactionRepository { Future> getByUserAndDateRange(String userId, DateTime from, DateTime to); Future> getByUserAndCategory(String userId, String categoryId); Future> getByUserAndTag(String userId, String tagId); + + /// Добавить список транзакций + Future addAll(List transactions); } diff --git a/lib/injection_container.dart b/lib/injection_container.dart index 1b0fcd1..e297650 100644 --- a/lib/injection_container.dart +++ b/lib/injection_container.dart @@ -40,7 +40,7 @@ Future initDependencies() async { ); // Services - getIt.registerSingleton(UserService(getIt())); + getIt.registerSingleton(UserService(getIt(), getIt(), getIt(), getIt())); // Blocs getIt.registerFactory(() => AuthBloc(userService: getIt())); diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb new file mode 100644 index 0000000..c671fd5 --- /dev/null +++ b/lib/l10n/app_en.arb @@ -0,0 +1,24 @@ +{ + "@@locale": "en", + "appTitle": "Budget App", + "homePageTitle": "Home", + "reportsPageTitle": "Reports", + "settingsPageTitle": "Settings", + "addTransactionButton": "Add new transaction", + "noTransactionsText": "No transactions yet", + "loadingTransactionsText": "Loading transactions...", + "transactionErrorText": "Error: {message}", + "loginPageTitle": "Login", + "nameFieldLabel": "Name", + "emailFieldLabel": "Email", + "nameFieldEmptyError": "Please enter your name", + "emailFieldEmptyError": "Please enter your email", + "loginButtonText": "Login / Register", + "darkModeSetting": "Dark Theme", + "darkModeDescription": "Toggle between light and dark theme", + "languageSetting": "Language", + "languageDescription": "Change application language", + "russianLanguage": "Russian", + "englishLanguage": "English", + "defaultUser": "Default User" +} \ No newline at end of file diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart new file mode 100644 index 0000000..60e25fb --- /dev/null +++ b/lib/l10n/app_localizations.dart @@ -0,0 +1,260 @@ +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; +} + +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 new file mode 100644 index 0000000..9301836 --- /dev/null +++ b/lib/l10n/app_localizations_en.dart @@ -0,0 +1,75 @@ +// 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'; +} diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart new file mode 100644 index 0000000..e99b099 --- /dev/null +++ b/lib/l10n/app_localizations_ru.dart @@ -0,0 +1,75 @@ +// 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 => 'Пользователь по умолчанию'; +} diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb new file mode 100644 index 0000000..4e1e496 --- /dev/null +++ b/lib/l10n/app_ru.arb @@ -0,0 +1,24 @@ +{ + "@@locale": "ru", + "appTitle": "Бюджетное приложение", + "homePageTitle": "Главная", + "reportsPageTitle": "Отчеты", + "settingsPageTitle": "Настройки", + "addTransactionButton": "Добавить новую транзакцию", + "noTransactionsText": "Нет транзакций", + "loadingTransactionsText": "Загрузка транзакций...", + "transactionErrorText": "Ошибка: {message}", + "loginPageTitle": "Вход", + "nameFieldLabel": "Имя", + "emailFieldLabel": "Email", + "nameFieldEmptyError": "Пожалуйста, введите имя", + "emailFieldEmptyError": "Пожалуйста, введите email", + "loginButtonText": "Войти / Зарегистрироваться", + "darkModeSetting": "Темная тема", + "darkModeDescription": "Переключить между светлой и темной темой", + "languageSetting": "Язык", + "languageDescription": "Изменить язык приложения", + "russianLanguage": "Русский", + "englishLanguage": "Английский", + "defaultUser": "Пользователь по умолчанию" +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 6b5f233..9b3f1ee 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:get_it/get_it.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; // Добавляем импортсса +import 'l10n/app_localizations.dart'; import 'logic/auth/auth_bloc.dart'; +import 'pages/login/login_page.dart'; import 'package:budget_app/pages/home_page.dart'; import 'theme/app_theme.dart'; import 'package:hive_ce_flutter/hive_flutter.dart'; @@ -14,66 +18,62 @@ void main() async { runApp(const MyApp()); } -class MyApp extends StatelessWidget { +class MyApp extends StatefulWidget { const MyApp({super.key}); @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => GetIt.instance()..add(AuthStarted()), - child: const AppView(), - ); - } + State createState() => _MyAppState(); } -class AppView extends StatefulWidget { - const AppView({super.key}); - - @override - State createState() => _AppViewState(); -} - -class _AppViewState extends State { +class _MyAppState extends State { late final SettingsService _settingsService; @override void initState() { super.initState(); _settingsService = GetIt.instance(); - _settingsService.addListener(_onThemeChanged); + _settingsService.addListener(_onSettingsChanged); } @override void dispose() { - _settingsService.removeListener(_onThemeChanged); + _settingsService.removeListener(_onSettingsChanged); super.dispose(); } - void _onThemeChanged() { + void _onSettingsChanged() { setState(() {}); } @override Widget build(BuildContext context) { - return MaterialApp( - title: 'Budget App', - theme: AppTheme.lightTheme(), - darkTheme: AppTheme.darkTheme(), - themeMode: _settingsService.isDarkMode ? ThemeMode.dark : ThemeMode.light, - home: BlocBuilder( - builder: (context, state) { - if (state is AuthAuthenticated) { - return const HomePage(); - } else { - // Здесь будет страница входа, пока просто заглушка - return const Scaffold( - body: Center( - child: Text('Требуется аутентификация'), - ), - ); - } - }, + return BlocProvider( + create: (context) => GetIt.instance()..add(AuthStarted()), + child: MaterialApp( + title: AppLocalizations.of(context)?.appTitle ?? 'Budget App', // Используем локализованный заголовок + theme: AppTheme.lightTheme(), + darkTheme: AppTheme.darkTheme(), + themeMode: _settingsService.isDarkMode ? ThemeMode.dark : ThemeMode.light, + // Добавляем локализацию + localizationsDelegates: const [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: AppLocalizations.supportedLocales, + locale: Locale(_settingsService.languageCode), // Устанавливаем текущий язык из настроек + home: BlocBuilder( + builder: (context, state) { + if (state is AuthAuthenticated) { + return const HomePage(); + } else { + return const LoginPage(); + } + }, + ), ), ); } } + diff --git a/lib/models/user.dart b/lib/models/user.dart index 6337552..e424c03 100644 --- a/lib/models/user.dart +++ b/lib/models/user.dart @@ -21,12 +21,16 @@ class User { @HiveField(2) // Поле 2 в Hive - третье поле модели final String email; + @HiveField(3) // Поле 3 в Hive - язык пользователя + final String language; + /// Конструктор пользователя /// id генерируется автоматически, если не передан User({ String? id, // Опциональный параметр - если null, сгенерируется автоматически required this.name, // Обязательный параметр required this.email, // Обязательный параметр + this.language = 'ru', // Язык по умолчанию - русский }) : id = id ?? IdGenerator.generateId(); // Если id не передан, генерируем новый /// Преобразование объекта в Map для сохранения в JSON или передачи по сети @@ -35,6 +39,7 @@ class User { 'id': id, 'name': name, 'email': email, + 'language': language, }; } @@ -44,6 +49,7 @@ class User { id: map['id'], name: map['name'], email: map['email'], + language: map['language'] ?? 'ru', // Устанавливаем русский по умолчанию, если язык не указан ); } diff --git a/lib/models/user.g.dart b/lib/models/user.g.dart index cea03e5..9ff78d3 100644 --- a/lib/models/user.g.dart +++ b/lib/models/user.g.dart @@ -20,19 +20,22 @@ class UserAdapter extends TypeAdapter { id: fields[0] as String?, name: fields[1] as String, email: fields[2] as String, + language: fields[3] == null ? 'ru' : fields[3] as String, ); } @override void write(BinaryWriter writer, User obj) { writer - ..writeByte(3) + ..writeByte(4) ..writeByte(0) ..write(obj.id) ..writeByte(1) ..write(obj.name) ..writeByte(2) - ..write(obj.email); + ..write(obj.email) + ..writeByte(3) + ..write(obj.language); } @override diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index ea14564..aefcd6b 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -1,24 +1,148 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +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 '../models/transaction_record.dart'; import 'settings_page.dart'; +import 'reports_page.dart'; // Импортируем новую страницу отчетов -/** - * Главная страница приложения для управления бюджетом. - * - * Этот виджет является основным экраном приложения и содержит: - * - AppBar с заголовком и кнопкой настроек - * - Основной контент с приветственным сообщением - * - * Особенности реализации: - * 1. Наследуется от StatelessWidget, так как не содержит собственного состояния - * 2. Использует Material Design через Scaffold - * 3. Управление темой вынесено в SettingsService - */ -class HomePage extends StatelessWidget { - /// Конструктор виджета - /// - /// @param key - опциональный ключ для идентификации виджета +class HomePage extends StatefulWidget { const HomePage({super.key}); + @override + State createState() => _HomePageState(); +} + +class _HomePageState extends State { + int _selectedIndex = 0; // Индекс выбранной вкладки + late String _currentUserId; // ID текущего пользователя + + // Список виджетов для каждой вкладки нижней навигации + static final List _widgetOptions = [ + const TransactionsPage(), // Главная страница с транзакциями + const ReportsPage(), // Страница отчетов + const SettingsPage(), // Страница настроек + ]; + + @override + void initState() { + super.initState(); + // Получаем ID текущего пользователя из AuthBloc + final authState = context.read().state; + if (authState is AuthAuthenticated) { + _currentUserId = authState.user.id; + } else { + // Если пользователь не аутентифицирован, можно перенаправить на страницу входа + // или использовать ID по умолчанию, если это применимо. + // В данном случае, для простоты, мы предполагаем, что пользователь всегда аутентифицирован. + _currentUserId = 'default_user'; // Заглушка, если что-то пошло не так + } + } + + void _onItemTapped(int index) { + setState(() { + _selectedIndex = index; + }); + } + + @override + Widget build(BuildContext context) { + final localizations = AppLocalizations.of(context)!; // Получаем экземпляр локализации + + return BlocProvider( + create: (context) { + return GetIt.instance()..add(LoadTransactions(userId: _currentUserId)); + }, + child: Scaffold( + appBar: AppBar( + title: Text(localizations.appTitle), // Локализованный заголовок + // Кнопки действий в AppBar теперь не нужны, так как настройки перенесены в BottomNavigationBar + ), + body: Center( + child: _widgetOptions[_selectedIndex], // Отображаем выбранный виджет + ), + floatingActionButton: FloatingActionButton( + onPressed: () { + // TODO: Добавить логику для добавления новой транзакции + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(localizations.addTransactionButton)), // Локализованный текст + ); + }, + child: const Icon(Icons.add), + ), + bottomNavigationBar: BottomNavigationBar( + items: [ + BottomNavigationBarItem( + icon: const Icon(Icons.home), + label: localizations.homePageTitle, // Локализованный текст + ), + BottomNavigationBarItem( + icon: const Icon(Icons.bar_chart), + label: localizations.reportsPageTitle, // Локализованный текст + ), + BottomNavigationBarItem( + icon: const Icon(Icons.settings), + label: localizations.settingsPageTitle, // Локализованный текст + ), + ], + currentIndex: _selectedIndex, + selectedItemColor: Theme.of(context).colorScheme.primary, + onTap: _onItemTapped, + ), + ), + ); + } +} + +// Отдельный виджет для страницы транзакций, чтобы HomeView был чище +class TransactionsPage extends StatelessWidget { + const TransactionsPage({super.key}); + + @override + Widget build(BuildContext context) { + final localizations = AppLocalizations.of(context)!; // Получаем экземпляр локализации + + return BlocBuilder( + builder: (context, state) { + if (state is TransactionLoading) { + return Center(child: CircularProgressIndicator()); + } else if (state is TransactionLoaded) { + if (state.transactions.isEmpty) { + return Center(child: Text(localizations.noTransactionsText)); // Локализованный текст + } + return ListView.builder( + itemCount: state.transactions.length, + itemBuilder: (context, index) { + final transaction = state.transactions[index]; + return ListTile( + leading: Icon(transaction.category.icon, color: transaction.category.color), + title: Text(transaction.vendor), + subtitle: Text(transaction.category.name), + trailing: Text( + '${transaction.amount.toStringAsFixed(2)} ${transaction.currency}', + style: TextStyle( + color: transaction.isIncome ? Colors.green : Colors.red, + ), + ), + ); + }, + ); + } else if (state is TransactionError) { + return Center(child: Text(localizations.transactionErrorText(state.message))); // Локализованный текст + } else { + return Center(child: Text(localizations.loadingTransactionsText)); // Локализованный текст + } + }, + ); + } +} + +class HomeView extends StatelessWidget { + const HomeView({super.key}); + + /** * Основной метод построения интерфейса виджета. * @@ -29,6 +153,8 @@ class HomePage extends StatelessWidget { */ @override Widget build(BuildContext context) { + final localizations = AppLocalizations.of(context)!; // Получаем экземпляр локализации + return Scaffold( // Верхняя панель приложения appBar: AppBar( @@ -51,28 +177,37 @@ class HomePage extends StatelessWidget { ], ), - // Основное содержимое страницы - body: const Center( - // Вертикальное расположение элементов - child: Column( - // Выравнивание по центру по вертикали - mainAxisAlignment: MainAxisAlignment.center, - - // Дочерние виджеты колонки - children: [ - // Иконка кошелька - Icon(Icons.account_balance_wallet, size: 64), - - // Отступ между элементами - SizedBox(height: 16), - - // Приветственный текст - Text( - 'Добро пожаловать в Budget App!', - style: TextStyle(fontSize: 24), - ), - ], - ), + body: BlocBuilder( + builder: (context, state) { + if (state is TransactionLoading) { + return const Center(child: CircularProgressIndicator()); + } else if (state is TransactionLoaded) { + if (state.transactions.isEmpty) { + return const Center(child: Text('Нет транзакций')); + } + return ListView.builder( + itemCount: state.transactions.length, + itemBuilder: (context, index) { + final transaction = state.transactions[index]; + return ListTile( + leading: Icon(transaction.category.icon, color: transaction.category.color), + title: Text(transaction.vendor), + subtitle: Text(transaction.category.name), + trailing: Text( + '${transaction.amount.toStringAsFixed(2)} ${transaction.currency}', + style: TextStyle( + color: transaction.isIncome ? Colors.green : Colors.red, + ), + ), + ); + }, + ); + } else if (state is TransactionError) { + return Center(child: Text(localizations.transactionErrorText(state.message))); // Локализованный текст + } else { + return Center(child: Text(localizations.loadingTransactionsText)); // Локализованный текст + } + }, ), ); } diff --git a/lib/pages/login/.placeholder b/lib/pages/login/.placeholder new file mode 100644 index 0000000..e69de29 diff --git a/lib/pages/login/login_page.dart b/lib/pages/login/login_page.dart new file mode 100644 index 0000000..d42642d --- /dev/null +++ b/lib/pages/login/login_page.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:get_it/get_it.dart'; + +import '/l10n/app_localizations.dart'; +import '../../logic/auth/auth_bloc.dart'; +import '../../services/user_service.dart'; + +class LoginPage extends StatefulWidget { + const LoginPage({super.key}); + + @override + State createState() => _LoginPageState(); +} + +class _LoginPageState extends State { + final _formKey = GlobalKey(); + final _emailController = TextEditingController(); + final _nameController = TextEditingController(); + + @override + Widget build(BuildContext context) { + final localizations = AppLocalizations.of(context)!; // Получаем экземпляр локализации + + return Scaffold( + appBar: AppBar(title: Text(localizations.loginPageTitle)), // Локализованный заголовок + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Form( + key: _formKey, + child: Column( + children: [ + TextFormField( + controller: _nameController, + decoration: InputDecoration(labelText: localizations.nameFieldLabel), // Локализованный текст + validator: (value) { + if (value == null || value.isEmpty) { + return localizations.nameFieldEmptyError; // Локализованный текст + } + return null; + }, + ), + TextFormField( + controller: _emailController, + decoration: InputDecoration(labelText: localizations.emailFieldLabel), // Локализованный текст + validator: (value) { + if (value == null || value.isEmpty) { + return localizations.emailFieldEmptyError; // Локализованный текст + } + return null; + }, + ), + const SizedBox(height: 20), + ElevatedButton( + onPressed: () { + if (_formKey.currentState!.validate()) { + final userService = GetIt.instance(); + userService.createAndSetUser( + _nameController.text, + _emailController.text, + ).then((_) { + final user = userService.currentUser; + if (user != null) { + context.read().add(AuthLoggedIn(user: user)); + } + }); + } + }, + child: Text(localizations.loginButtonText), // Локализованный текст + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/pages/reports_page.dart b/lib/pages/reports_page.dart new file mode 100644 index 0000000..b1ab5d3 --- /dev/null +++ b/lib/pages/reports_page.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; +import '/l10n/app_localizations.dart'; + +class ReportsPage extends StatelessWidget { + const ReportsPage({super.key}); + + @override + Widget build(BuildContext context) { + final localizations = AppLocalizations.of(context)!; // Получаем экземпляр локализации + + return Scaffold( + appBar: AppBar( + title: Text(localizations.reportsPageTitle), // Локализованный заголовок + ), + body: Center( + child: Text(localizations.reportsPageTitle), // Локализованный текст + ), + ); + } +} diff --git a/lib/pages/settings_page.dart b/lib/pages/settings_page.dart index 75cda47..b5815cc 100644 --- a/lib/pages/settings_page.dart +++ b/lib/pages/settings_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; +import '/l10n/app_localizations.dart'; import '../services/settings_service.dart'; class SettingsPage extends StatefulWidget { @@ -31,9 +32,11 @@ class _SettingsPageState extends State { @override Widget build(BuildContext context) { + final localizations = AppLocalizations.of(context)!; // Получаем экземпляр локализации + return Scaffold( appBar: AppBar( - title: const Text('Настройки'), + title: Text(localizations.settingsPageTitle), // Локализованный заголовок ), body: Padding( padding: const EdgeInsets.all(16.0), @@ -41,14 +44,34 @@ class _SettingsPageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ SwitchListTile( - title: const Text('Темная тема'), - subtitle: const Text('Переключить между светлой и темной темой'), + title: Text(localizations.darkModeSetting), // Локализованный текст + subtitle: Text(localizations.darkModeDescription), // Локализованный текст value: _settingsService.isDarkMode, onChanged: (value) async { await _settingsService.setDarkMode(value); }, ), const Divider(), + ListTile( + title: Text(localizations.languageSetting), // Локализованный текст + subtitle: Text(localizations.languageDescription), // Локализованный текст + trailing: DropdownButton( + value: _settingsService.languageCode, // Текущий выбранный язык + onChanged: (String? newValue) async { + if (newValue != null) { + await _settingsService.setLanguageCode(newValue); + } + }, + items: ['en', 'ru'] // Доступные языки + .map>((String value) { + return DropdownMenuItem( + value: value, + child: Text(value == 'en' ? localizations.englishLanguage : localizations.russianLanguage), // Локализованные названия языков + ); + }).toList(), + ), + ), + const Divider(), // Здесь можно добавить другие настройки ], ), diff --git a/lib/services/settings_service.dart b/lib/services/settings_service.dart index 67ca0ed..9fff468 100644 --- a/lib/services/settings_service.dart +++ b/lib/services/settings_service.dart @@ -3,6 +3,7 @@ import 'package:hive_ce/hive.dart'; class SettingsService extends ChangeNotifier { static const String _darkModeKey = 'darkMode'; + static const String _languageCodeKey = 'languageCode'; // Новый ключ для языка late final Box _settingsBox; SettingsService() { @@ -19,4 +20,15 @@ class SettingsService extends ChangeNotifier { Future toggleTheme() async { await setDarkMode(!isDarkMode); } + + // Новый геттер для получения кода языка + String get languageCode => _settingsBox.get(_languageCodeKey, defaultValue: 'ru'); + + // Новый метод для установки кода языка + Future setLanguageCode(String code) async { + await _settingsBox.put(_languageCodeKey, code); + notifyListeners(); + } } + + diff --git a/lib/services/user_service.dart b/lib/services/user_service.dart index 8b9d8f8..bb4691c 100644 --- a/lib/services/user_service.dart +++ b/lib/services/user_service.dart @@ -1,3 +1,9 @@ +import 'package:budget_app/data/repositories/interfaces/icategory_repository.dart'; +import 'package:budget_app/data/repositories/interfaces/itag_repository.dart'; +import 'package:budget_app/data/repositories/interfaces/itransaction_repository.dart'; +import 'package:budget_app/utils/category_utils.dart'; +import 'package:budget_app/utils/tag_utils.dart'; +import 'package:budget_app/utils/transaction_utils.dart'; import 'package:flutter/foundation.dart'; import 'package:hive_ce/hive.dart'; import '/models/user.dart'; @@ -10,7 +16,10 @@ class UserService extends ChangeNotifier { static const String _currentUserKey = 'currentUserId'; late final Box _settingsBox; // Box для хранения настроек - final IUserRepository _userRepository; // Репозиторий для работы с пользователями + final IUserRepository _userRepository; + final ICategoryRepository _categoryRepository; + final ITagRepository _tagRepository; + final ITransactionRepository _transactionRepository; User? _currentUser; // Текущий активный пользователь @@ -21,7 +30,7 @@ class UserService extends ChangeNotifier { bool get hasCurrentUser => _currentUser != null; /// Конструктор сервиса - UserService(this._userRepository) { + UserService(this._userRepository, this._categoryRepository, this._tagRepository, this._transactionRepository) { _settingsBox = Hive.box('settings'); _loadCurrentUser(); // Загружаем сохраненного пользователя при запуске } @@ -52,8 +61,14 @@ class UserService extends ChangeNotifier { /// Создание нового пользователя и установка его как текущего Future createAndSetUser(String name, String email) async { - final user = User(name: name, email: email); + final user = User(name: name, email: email, language: 'ru'); // Устанавливаем язык по умолчанию await _userRepository.add(user); + + // Добавляем начальные категории, теги и транзакции для нового пользователя + await _categoryRepository.addAll(CategoryUtils.getDefaultCategories(user.id)); + await _tagRepository.addAll(TagUtils.getDefaultTags(user.id)); + await _transactionRepository.addAll(TransactionUtils.getSampleTransactions(user.id)); + await setCurrentUser(user); } diff --git a/pubspec.lock b/pubspec.lock index 33da270..fb48554 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,18 +5,18 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: e55636ed79578b9abca5fecf9437947798f5ef7456308b5cb85720b793eac92f + sha256: da0d9209ca76bde579f2da330aeb9df62b6319c834fa7baae052021b0462401f url: "https://pub.dev" source: hosted - version: "82.0.0" + version: "85.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: "904ae5bb474d32c38fb9482e2d925d5454cda04ddd0e55d2e6826bc72f6ba8c0" + sha256: f6154230675c44a191f2e20d16eeceb4aa18b30ca732db4efaf94c6a7d43cfa6 url: "https://pub.dev" source: hosted - version: "7.4.5" + version: "7.5.2" args: dependency: transitive description: @@ -34,7 +34,7 @@ packages: source: hosted version: "2.13.0" bloc: - dependency: transitive + dependency: "direct main" description: name: bloc sha256: "52c10575f4445c61dd9e0cafcc6356fdd827c4c64dd7945ef3c4105f6b6ac189" @@ -53,10 +53,10 @@ packages: dependency: transitive description: name: build - sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0 + sha256: "51dc711996cbf609b90cbe5b335bbce83143875a9d58e4b5c6d3c4f684d3dda7" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.5.4" build_config: dependency: transitive description: @@ -77,26 +77,26 @@ packages: dependency: transitive description: name: build_resolvers - sha256: b9e4fda21d846e192628e7a4f6deda6888c36b5b69ba02ff291a01fd529140f0 + sha256: ee4257b3f20c0c90e72ed2b57ad637f694ccba48839a821e87db762548c22a62 url: "https://pub.dev" source: hosted - version: "2.4.4" + version: "2.5.4" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "058fe9dce1de7d69c4b84fada934df3e0153dd000758c4d65964d0166779aa99" + sha256: "382a4d649addbfb7ba71a3631df0ec6a45d5ab9b098638144faf27f02778eb53" url: "https://pub.dev" source: hosted - version: "2.4.15" + version: "2.5.4" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021" + sha256: "85fbbb1036d576d966332a3f5ce83f2ce66a40bea1a94ad2d5fc29a19a0d3792" url: "https://pub.dev" source: hosted - version: "8.0.0" + version: "9.1.2" built_collection: dependency: transitive description: @@ -246,6 +246,11 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.0" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" flutter_test: dependency: "direct dev" description: flutter @@ -331,6 +336,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.2" + intl: + dependency: "direct main" + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.dev" + source: hosted + version: "0.20.2" io: dependency: transitive description: @@ -399,10 +412,10 @@ packages: dependency: "direct main" description: name: logger - sha256: be4b23575aac7ebf01f225a241eb7f6b5641eeaf43c6a8613510fc2f8cf187d1 + sha256: "2621da01aabaf223f8f961e751f2c943dbb374dc3559b982f200ccedadaa6999" url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.6.0" logging: dependency: transitive description: @@ -700,10 +713,10 @@ packages: dependency: transitive description: name: watcher - sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104" + sha256: "0b7fd4a0bbc4b92641dbf20adfd7e3fd1398fe17102d94b674234563e110088a" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" web: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 8d8e2bd..fa544d3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -42,6 +42,10 @@ dependencies: logger: ^2.5.0 flutter_bloc: ^9.1.1 equatable: ^2.0.7 + flutter_localizations: + sdk: flutter + intl: ^0.20.2 + bloc: dev_dependencies: @@ -57,6 +61,7 @@ dev_dependencies: # The following section is specific to Flutter packages. flutter: + generate: true # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in