This commit is contained in:
2025-06-27 18:42:02 +03:00
parent b2c8189cb2
commit 5856b59b54
25 changed files with 903 additions and 100 deletions
+3
View File
@@ -0,0 +1,3 @@
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
@@ -65,4 +65,12 @@ class HiveCategoryRepository implements ICategoryRepository {
.where((c) => c.userId == userId && !c.isIncome) .where((c) => c.userId == userId && !c.isIncome)
.toList(); .toList();
} }
@override
Future<void> addAll(List<Category> categories) async {
final Map<String, Category> categoryMap = {
for (var cat in categories) cat.id: cat
};
await _box.putAll(categoryMap);
}
} }
@@ -40,4 +40,12 @@ class HiveTagRepository implements ITagRepository {
// у которых поле `userId` совпадает с идентификатором, переданным в метод. // у которых поле `userId` совпадает с идентификатором, переданным в метод.
return _box.values.where((tag) => tag.userId == userId).toList(); return _box.values.where((tag) => tag.userId == userId).toList();
} }
@override
Future<void> addAll(List<Tag> tags) async {
final Map<String, Tag> tagMap = {
for (var tag in tags) tag.id: tag
};
await _box.putAll(tagMap);
}
} }
@@ -93,4 +93,12 @@ class HiveTransactionRepository implements ITransactionRepository {
.where((t) => t.userId == userId && t.tag?.id == tagId) .where((t) => t.userId == userId && t.tag?.id == tagId)
.toList(); .toList();
} }
@override
Future<void> addAll(List<TransactionRecord> transactions) async {
final Map<String, TransactionRecord> transactionMap = {
for (var tr in transactions) tr.id: tr
};
await _box.putAll(transactionMap);
}
} }
@@ -19,4 +19,7 @@ abstract class ICategoryRepository {
/// Получить категории расходов конкретного пользователя /// Получить категории расходов конкретного пользователя
Future<List<Category>> getExpenseCategoriesByUser(String userId); Future<List<Category>> getExpenseCategoriesByUser(String userId);
/// Добавить список категорий
Future<void> addAll(List<Category> categories);
} }
@@ -12,4 +12,7 @@ abstract class ITagRepository {
// реализацию этого метода. Это гарантирует, что наш репозиторий // реализацию этого метода. Это гарантирует, что наш репозиторий
// сможет получать теги для конкретного пользователя. // сможет получать теги для конкретного пользователя.
Future<List<Tag>> getAllByUser(String userId); Future<List<Tag>> getAllByUser(String userId);
/// Добавить список тегов
Future<void> addAll(List<Tag> tags);
} }
@@ -17,4 +17,7 @@ abstract class ITransactionRepository {
Future<List<TransactionRecord>> getByUserAndDateRange(String userId, DateTime from, DateTime to); Future<List<TransactionRecord>> getByUserAndDateRange(String userId, DateTime from, DateTime to);
Future<List<TransactionRecord>> getByUserAndCategory(String userId, String categoryId); Future<List<TransactionRecord>> getByUserAndCategory(String userId, String categoryId);
Future<List<TransactionRecord>> getByUserAndTag(String userId, String tagId); Future<List<TransactionRecord>> getByUserAndTag(String userId, String tagId);
/// Добавить список транзакций
Future<void> addAll(List<TransactionRecord> transactions);
} }
+1 -1
View File
@@ -40,7 +40,7 @@ Future<void> initDependencies() async {
); );
// Services // Services
getIt.registerSingleton<UserService>(UserService(getIt())); getIt.registerSingleton<UserService>(UserService(getIt(), getIt(), getIt(), getIt()));
// Blocs // Blocs
getIt.registerFactory<AuthBloc>(() => AuthBloc(userService: getIt())); getIt.registerFactory<AuthBloc>(() => AuthBloc(userService: getIt()));
+24
View File
@@ -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"
}
+260
View File
@@ -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, 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;
}
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.',
);
}
+75
View File
@@ -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';
}
+75
View File
@@ -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 => 'Пользователь по умолчанию';
}
+24
View File
@@ -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": "Пользователь по умолчанию"
}
+36 -36
View File
@@ -1,6 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.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 'logic/auth/auth_bloc.dart';
import 'pages/login/login_page.dart';
import 'package:budget_app/pages/home_page.dart'; import 'package:budget_app/pages/home_page.dart';
import 'theme/app_theme.dart'; import 'theme/app_theme.dart';
import 'package:hive_ce_flutter/hive_flutter.dart'; import 'package:hive_ce_flutter/hive_flutter.dart';
@@ -14,66 +18,62 @@ void main() async {
runApp(const MyApp()); runApp(const MyApp());
} }
class MyApp extends StatelessWidget { class MyApp extends StatefulWidget {
const MyApp({super.key}); const MyApp({super.key});
@override @override
Widget build(BuildContext context) { State<MyApp> createState() => _MyAppState();
return BlocProvider(
create: (context) => GetIt.instance<AuthBloc>()..add(AuthStarted()),
child: const AppView(),
);
}
} }
class AppView extends StatefulWidget { class _MyAppState extends State<MyApp> {
const AppView({super.key});
@override
State<AppView> createState() => _AppViewState();
}
class _AppViewState extends State<AppView> {
late final SettingsService _settingsService; late final SettingsService _settingsService;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_settingsService = GetIt.instance<SettingsService>(); _settingsService = GetIt.instance<SettingsService>();
_settingsService.addListener(_onThemeChanged); _settingsService.addListener(_onSettingsChanged);
} }
@override @override
void dispose() { void dispose() {
_settingsService.removeListener(_onThemeChanged); _settingsService.removeListener(_onSettingsChanged);
super.dispose(); super.dispose();
} }
void _onThemeChanged() { void _onSettingsChanged() {
setState(() {}); setState(() {});
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp( return BlocProvider(
title: 'Budget App', create: (context) => GetIt.instance<AuthBloc>()..add(AuthStarted()),
theme: AppTheme.lightTheme(), child: MaterialApp(
darkTheme: AppTheme.darkTheme(), title: AppLocalizations.of(context)?.appTitle ?? 'Budget App', // Используем локализованный заголовок
themeMode: _settingsService.isDarkMode ? ThemeMode.dark : ThemeMode.light, theme: AppTheme.lightTheme(),
home: BlocBuilder<AuthBloc, AuthState>( darkTheme: AppTheme.darkTheme(),
builder: (context, state) { themeMode: _settingsService.isDarkMode ? ThemeMode.dark : ThemeMode.light,
if (state is AuthAuthenticated) { // Добавляем локализацию
return const HomePage(); localizationsDelegates: const [
} else { AppLocalizations.delegate,
// Здесь будет страница входа, пока просто заглушка GlobalMaterialLocalizations.delegate,
return const Scaffold( GlobalWidgetsLocalizations.delegate,
body: Center( GlobalCupertinoLocalizations.delegate,
child: Text('Требуется аутентификация'), ],
), supportedLocales: AppLocalizations.supportedLocales,
); locale: Locale(_settingsService.languageCode), // Устанавливаем текущий язык из настроек
} home: BlocBuilder<AuthBloc, AuthState>(
}, builder: (context, state) {
if (state is AuthAuthenticated) {
return const HomePage();
} else {
return const LoginPage();
}
},
),
), ),
); );
} }
} }
+6
View File
@@ -21,12 +21,16 @@ class User {
@HiveField(2) // Поле 2 в Hive - третье поле модели @HiveField(2) // Поле 2 в Hive - третье поле модели
final String email; final String email;
@HiveField(3) // Поле 3 в Hive - язык пользователя
final String language;
/// Конструктор пользователя /// Конструктор пользователя
/// id генерируется автоматически, если не передан /// id генерируется автоматически, если не передан
User({ User({
String? id, // Опциональный параметр - если null, сгенерируется автоматически String? id, // Опциональный параметр - если null, сгенерируется автоматически
required this.name, // Обязательный параметр required this.name, // Обязательный параметр
required this.email, // Обязательный параметр required this.email, // Обязательный параметр
this.language = 'ru', // Язык по умолчанию - русский
}) : id = id ?? IdGenerator.generateId(); // Если id не передан, генерируем новый }) : id = id ?? IdGenerator.generateId(); // Если id не передан, генерируем новый
/// Преобразование объекта в Map для сохранения в JSON или передачи по сети /// Преобразование объекта в Map для сохранения в JSON или передачи по сети
@@ -35,6 +39,7 @@ class User {
'id': id, 'id': id,
'name': name, 'name': name,
'email': email, 'email': email,
'language': language,
}; };
} }
@@ -44,6 +49,7 @@ class User {
id: map['id'], id: map['id'],
name: map['name'], name: map['name'],
email: map['email'], email: map['email'],
language: map['language'] ?? 'ru', // Устанавливаем русский по умолчанию, если язык не указан
); );
} }
+5 -2
View File
@@ -20,19 +20,22 @@ class UserAdapter extends TypeAdapter<User> {
id: fields[0] as String?, id: fields[0] as String?,
name: fields[1] as String, name: fields[1] as String,
email: fields[2] as String, email: fields[2] as String,
language: fields[3] == null ? 'ru' : fields[3] as String,
); );
} }
@override @override
void write(BinaryWriter writer, User obj) { void write(BinaryWriter writer, User obj) {
writer writer
..writeByte(3) ..writeByte(4)
..writeByte(0) ..writeByte(0)
..write(obj.id) ..write(obj.id)
..writeByte(1) ..writeByte(1)
..write(obj.name) ..write(obj.name)
..writeByte(2) ..writeByte(2)
..write(obj.email); ..write(obj.email)
..writeByte(3)
..write(obj.language);
} }
@override @override
+173 -38
View File
@@ -1,24 +1,148 @@
import 'package:flutter/material.dart'; 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 'settings_page.dart';
import 'reports_page.dart'; // Импортируем новую страницу отчетов
/** class HomePage extends StatefulWidget {
* Главная страница приложения для управления бюджетом.
*
* Этот виджет является основным экраном приложения и содержит:
* - AppBar с заголовком и кнопкой настроек
* - Основной контент с приветственным сообщением
*
* Особенности реализации:
* 1. Наследуется от StatelessWidget, так как не содержит собственного состояния
* 2. Использует Material Design через Scaffold
* 3. Управление темой вынесено в SettingsService
*/
class HomePage extends StatelessWidget {
/// Конструктор виджета
///
/// @param key - опциональный ключ для идентификации виджета
const HomePage({super.key}); const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
int _selectedIndex = 0; // Индекс выбранной вкладки
late String _currentUserId; // ID текущего пользователя
// Список виджетов для каждой вкладки нижней навигации
static final List<Widget> _widgetOptions = <Widget>[
const TransactionsPage(), // Главная страница с транзакциями
const ReportsPage(), // Страница отчетов
const SettingsPage(), // Страница настроек
];
@override
void initState() {
super.initState();
// Получаем ID текущего пользователя из AuthBloc
final authState = context.read<AuthBloc>().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<TransactionBloc>()..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>[
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<TransactionBloc, TransactionState>(
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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final localizations = AppLocalizations.of(context)!; // Получаем экземпляр локализации
return Scaffold( return Scaffold(
// Верхняя панель приложения // Верхняя панель приложения
appBar: AppBar( appBar: AppBar(
@@ -51,28 +177,37 @@ class HomePage extends StatelessWidget {
], ],
), ),
// Основное содержимое страницы body: BlocBuilder<TransactionBloc, TransactionState>(
body: const Center( builder: (context, state) {
// Вертикальное расположение элементов if (state is TransactionLoading) {
child: Column( return const Center(child: CircularProgressIndicator());
// Выравнивание по центру по вертикали } else if (state is TransactionLoaded) {
mainAxisAlignment: MainAxisAlignment.center, if (state.transactions.isEmpty) {
return const Center(child: Text('Нет транзакций'));
// Дочерние виджеты колонки }
children: [ return ListView.builder(
// Иконка кошелька itemCount: state.transactions.length,
Icon(Icons.account_balance_wallet, size: 64), itemBuilder: (context, index) {
final transaction = state.transactions[index];
// Отступ между элементами return ListTile(
SizedBox(height: 16), leading: Icon(transaction.category.icon, color: transaction.category.color),
title: Text(transaction.vendor),
// Приветственный текст subtitle: Text(transaction.category.name),
Text( trailing: Text(
'Добро пожаловать в Budget App!', '${transaction.amount.toStringAsFixed(2)} ${transaction.currency}',
style: TextStyle(fontSize: 24), 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)); // Локализованный текст
}
},
), ),
); );
} }
View File
+77
View File
@@ -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<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
final _formKey = GlobalKey<FormState>();
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>();
userService.createAndSetUser(
_nameController.text,
_emailController.text,
).then((_) {
final user = userService.currentUser;
if (user != null) {
context.read<AuthBloc>().add(AuthLoggedIn(user: user));
}
});
}
},
child: Text(localizations.loginButtonText), // Локализованный текст
),
],
),
),
),
);
}
}
+20
View File
@@ -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), // Локализованный текст
),
);
}
}
+26 -3
View File
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import '/l10n/app_localizations.dart';
import '../services/settings_service.dart'; import '../services/settings_service.dart';
class SettingsPage extends StatefulWidget { class SettingsPage extends StatefulWidget {
@@ -31,9 +32,11 @@ class _SettingsPageState extends State<SettingsPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final localizations = AppLocalizations.of(context)!; // Получаем экземпляр локализации
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('Настройки'), title: Text(localizations.settingsPageTitle), // Локализованный заголовок
), ),
body: Padding( body: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
@@ -41,14 +44,34 @@ class _SettingsPageState extends State<SettingsPage> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
SwitchListTile( SwitchListTile(
title: const Text('Темная тема'), title: Text(localizations.darkModeSetting), // Локализованный текст
subtitle: const Text('Переключить между светлой и темной темой'), subtitle: Text(localizations.darkModeDescription), // Локализованный текст
value: _settingsService.isDarkMode, value: _settingsService.isDarkMode,
onChanged: (value) async { onChanged: (value) async {
await _settingsService.setDarkMode(value); await _settingsService.setDarkMode(value);
}, },
), ),
const Divider(), const Divider(),
ListTile(
title: Text(localizations.languageSetting), // Локализованный текст
subtitle: Text(localizations.languageDescription), // Локализованный текст
trailing: DropdownButton<String>(
value: _settingsService.languageCode, // Текущий выбранный язык
onChanged: (String? newValue) async {
if (newValue != null) {
await _settingsService.setLanguageCode(newValue);
}
},
items: <String>['en', 'ru'] // Доступные языки
.map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value == 'en' ? localizations.englishLanguage : localizations.russianLanguage), // Локализованные названия языков
);
}).toList(),
),
),
const Divider(),
// Здесь можно добавить другие настройки // Здесь можно добавить другие настройки
], ],
), ),
+12
View File
@@ -3,6 +3,7 @@ import 'package:hive_ce/hive.dart';
class SettingsService extends ChangeNotifier { class SettingsService extends ChangeNotifier {
static const String _darkModeKey = 'darkMode'; static const String _darkModeKey = 'darkMode';
static const String _languageCodeKey = 'languageCode'; // Новый ключ для языка
late final Box _settingsBox; late final Box _settingsBox;
SettingsService() { SettingsService() {
@@ -19,4 +20,15 @@ class SettingsService extends ChangeNotifier {
Future<void> toggleTheme() async { Future<void> toggleTheme() async {
await setDarkMode(!isDarkMode); await setDarkMode(!isDarkMode);
} }
// Новый геттер для получения кода языка
String get languageCode => _settingsBox.get(_languageCodeKey, defaultValue: 'ru');
// Новый метод для установки кода языка
Future<void> setLanguageCode(String code) async {
await _settingsBox.put(_languageCodeKey, code);
notifyListeners();
}
} }
+18 -3
View File
@@ -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:flutter/foundation.dart';
import 'package:hive_ce/hive.dart'; import 'package:hive_ce/hive.dart';
import '/models/user.dart'; import '/models/user.dart';
@@ -10,7 +16,10 @@ class UserService extends ChangeNotifier {
static const String _currentUserKey = 'currentUserId'; static const String _currentUserKey = 'currentUserId';
late final Box _settingsBox; // Box для хранения настроек late final Box _settingsBox; // Box для хранения настроек
final IUserRepository _userRepository; // Репозиторий для работы с пользователями final IUserRepository _userRepository;
final ICategoryRepository _categoryRepository;
final ITagRepository _tagRepository;
final ITransactionRepository _transactionRepository;
User? _currentUser; // Текущий активный пользователь User? _currentUser; // Текущий активный пользователь
@@ -21,7 +30,7 @@ class UserService extends ChangeNotifier {
bool get hasCurrentUser => _currentUser != null; bool get hasCurrentUser => _currentUser != null;
/// Конструктор сервиса /// Конструктор сервиса
UserService(this._userRepository) { UserService(this._userRepository, this._categoryRepository, this._tagRepository, this._transactionRepository) {
_settingsBox = Hive.box('settings'); _settingsBox = Hive.box('settings');
_loadCurrentUser(); // Загружаем сохраненного пользователя при запуске _loadCurrentUser(); // Загружаем сохраненного пользователя при запуске
} }
@@ -52,8 +61,14 @@ class UserService extends ChangeNotifier {
/// Создание нового пользователя и установка его как текущего /// Создание нового пользователя и установка его как текущего
Future<void> createAndSetUser(String name, String email) async { Future<void> 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 _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); await setCurrentUser(user);
} }
+30 -17
View File
@@ -5,18 +5,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: _fe_analyzer_shared name: _fe_analyzer_shared
sha256: e55636ed79578b9abca5fecf9437947798f5ef7456308b5cb85720b793eac92f sha256: da0d9209ca76bde579f2da330aeb9df62b6319c834fa7baae052021b0462401f
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "82.0.0" version: "85.0.0"
analyzer: analyzer:
dependency: transitive dependency: transitive
description: description:
name: analyzer name: analyzer
sha256: "904ae5bb474d32c38fb9482e2d925d5454cda04ddd0e55d2e6826bc72f6ba8c0" sha256: f6154230675c44a191f2e20d16eeceb4aa18b30ca732db4efaf94c6a7d43cfa6
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "7.4.5" version: "7.5.2"
args: args:
dependency: transitive dependency: transitive
description: description:
@@ -34,7 +34,7 @@ packages:
source: hosted source: hosted
version: "2.13.0" version: "2.13.0"
bloc: bloc:
dependency: transitive dependency: "direct main"
description: description:
name: bloc name: bloc
sha256: "52c10575f4445c61dd9e0cafcc6356fdd827c4c64dd7945ef3c4105f6b6ac189" sha256: "52c10575f4445c61dd9e0cafcc6356fdd827c4c64dd7945ef3c4105f6b6ac189"
@@ -53,10 +53,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: build name: build
sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0 sha256: "51dc711996cbf609b90cbe5b335bbce83143875a9d58e4b5c6d3c4f684d3dda7"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.2" version: "2.5.4"
build_config: build_config:
dependency: transitive dependency: transitive
description: description:
@@ -77,26 +77,26 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: build_resolvers name: build_resolvers
sha256: b9e4fda21d846e192628e7a4f6deda6888c36b5b69ba02ff291a01fd529140f0 sha256: ee4257b3f20c0c90e72ed2b57ad637f694ccba48839a821e87db762548c22a62
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.4" version: "2.5.4"
build_runner: build_runner:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: build_runner name: build_runner
sha256: "058fe9dce1de7d69c4b84fada934df3e0153dd000758c4d65964d0166779aa99" sha256: "382a4d649addbfb7ba71a3631df0ec6a45d5ab9b098638144faf27f02778eb53"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.15" version: "2.5.4"
build_runner_core: build_runner_core:
dependency: transitive dependency: transitive
description: description:
name: build_runner_core name: build_runner_core
sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021" sha256: "85fbbb1036d576d966332a3f5ce83f2ce66a40bea1a94ad2d5fc29a19a0d3792"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "8.0.0" version: "9.1.2"
built_collection: built_collection:
dependency: transitive dependency: transitive
description: description:
@@ -246,6 +246,11 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.0.0" version: "5.0.0"
flutter_localizations:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_test: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
@@ -331,6 +336,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.1.2" version: "4.1.2"
intl:
dependency: "direct main"
description:
name: intl
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
url: "https://pub.dev"
source: hosted
version: "0.20.2"
io: io:
dependency: transitive dependency: transitive
description: description:
@@ -399,10 +412,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: logger name: logger
sha256: be4b23575aac7ebf01f225a241eb7f6b5641eeaf43c6a8613510fc2f8cf187d1 sha256: "2621da01aabaf223f8f961e751f2c943dbb374dc3559b982f200ccedadaa6999"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.5.0" version: "2.6.0"
logging: logging:
dependency: transitive dependency: transitive
description: description:
@@ -700,10 +713,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: watcher name: watcher
sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104" sha256: "0b7fd4a0bbc4b92641dbf20adfd7e3fd1398fe17102d94b674234563e110088a"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.1" version: "1.1.2"
web: web:
dependency: transitive dependency: transitive
description: description:
+5
View File
@@ -42,6 +42,10 @@ dependencies:
logger: ^2.5.0 logger: ^2.5.0
flutter_bloc: ^9.1.1 flutter_bloc: ^9.1.1
equatable: ^2.0.7 equatable: ^2.0.7
flutter_localizations:
sdk: flutter
intl: ^0.20.2
bloc:
dev_dependencies: dev_dependencies:
@@ -57,6 +61,7 @@ dev_dependencies:
# The following section is specific to Flutter packages. # The following section is specific to Flutter packages.
flutter: flutter:
generate: true
# The following line ensures that the Material Icons font is # The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in # included with your application, so that you can use the icons in