Refactors user management to use UserCubit

This commit refactors the user management system to utilize UserCubit, improving state management and separation of concerns.

- Replaces UserService with UserCubit for managing user state.
- Introduces AppSettings and GlobalSettings models and repositories for managing app-level settings and configurations.
- Modifies dependency injection to register the new repositories and cubits.
- Updates UI components (LoginPage, SettingsPage, HomePage, AddTransactionDialog, CategoryListPage, TagListPage) to interact with UserCubit and SettingsCubit.
- Removes direct dependency on UserRepository in favor of UserCubit for accessing user information.
- Streamlines the authentication process by using UserCubit to handle user creation and login.
- Improves settings management by using dedicated repositories and cubits for app settings.
This commit is contained in:
2025-07-07 00:29:15 +03:00
parent a89ab1c910
commit 9bae137d08
30 changed files with 958 additions and 444 deletions
+22 -41
View File
@@ -1,4 +1,6 @@
import 'package:budget_app/hive/hive_registrar.g.dart';
import 'package:budget_app/models/app_settings.dart'; // Добавлен импорт AppSettings
import 'package:budget_app/models/global_settings.dart';
import 'package:hive_ce_flutter/hive_flutter.dart';
import 'package:logger/logger.dart';
@@ -7,44 +9,44 @@ import '/models/sms_message.dart';
import '/models/tag.dart';
import '/models/transaction_record.dart';
import '/models/user.dart';
import '/utils/category_utils.dart';
import '/utils/tag_utils.dart';
import '/utils/transaction_utils.dart';
final _logger = Logger();
class HiveService {
static const String _settingsBox = 'settings';
static const String _appSettingsBox =
'app_settings'; // Переименовано для настроек приложения
static const String _categoryBox = 'categories';
static const String _tagBox = 'tags';
static const String _transactionBox = 'transactions';
static const String _userBox = 'users';
static const String _smsMessageBox = 'smsMessages';
// ID системного пользователя по умолчанию
static const String _defaultUserId = 'default_user';
static const String _globalSettings = 'global_settings';
static Future<void> init() async {
_logger.i('Initializing Hive database');
await Hive.initFlutter();
//zfinal path = Directory.current.path;
Hive.registerAdapters();
// Открытие всех Box'ов
await Future.wait([
Hive.openBox(_settingsBox),
Hive.openBox<AppSettings>(
_appSettingsBox,
), // Открываем Box для AppSettings
Hive.openBox<Category>(_categoryBox),
Hive.openBox<Tag>(_tagBox),
Hive.openBox<TransactionRecord>(_transactionBox),
Hive.openBox<User>(_userBox),
Hive.openBox<SmsMessage>(_smsMessageBox),
Hive.openBox<GlobalSettings>(_globalSettings),
]);
// Проверка и заполнение начальными данными
await _checkAndFillInitialData();
_checkAndFillInitialData();
}
static Box<AppSettings> get appSettings =>
Hive.box<AppSettings>(_appSettingsBox); // Геттер для настроек
static Box<Category> get categories => Hive.box<Category>(_categoryBox);
static Box<Tag> get tags => Hive.box<Tag>(_tagBox);
static Box<TransactionRecord> get transactions =>
@@ -52,44 +54,23 @@ class HiveService {
static Box<User> get users => Hive.box<User>(_userBox);
static Box<SmsMessage> get smsMessages =>
Hive.box<SmsMessage>(_smsMessageBox);
static Box<GlobalSettings> get globalSettings =>
Hive.box<GlobalSettings>(_globalSettings);
/// Проверяет и заполняет боксы начальными данными при первом запуске
static Future<void> _checkAndFillInitialData() async {
// Создаем пользователя по умолчанию, если его нет
static void _checkAndFillInitialData() {
final userBox = users;
if (userBox.isEmpty) {
_logger.i('Creating default user');
final defaultUser = User(
id: _defaultUserId,
name: 'Пользователь по умолчанию',
email: 'default@example.com',
);
await userBox.put(_defaultUserId, defaultUser);
await userBox.flush();
}
final catBox = categories;
if (catBox.isEmpty) {
_logger.i('Filling initial categories for default user');
// Теперь передаем userId в getDefaultCategories
await catBox.addAll(CategoryUtils.getDefaultCategories(_defaultUserId));
await catBox.flush();
}
final tagBox = tags;
if (tagBox.isEmpty) {
_logger.i('Filling initial tags');
await tagBox.addAll(TagUtils.getDefaultTags(_defaultUserId));
await tagBox.flush();
}
final transactionBox = transactions;
if (transactionBox.isEmpty) {
_logger.i('Filling sample transactions');
await transactionBox.addAll(
TransactionUtils.getSampleTransactions(_defaultUserId),
if (userBox.isEmpty &&
catBox.isEmpty &&
tagBox.isEmpty &&
transactionBox.isEmpty) {
_logger.i(
'Initial data is empty. It will be filled when a user is created.',
);
await transactionBox.flush();
}
}
}
@@ -0,0 +1,33 @@
import 'package:hive_ce/hive.dart';
import '../../models/global_settings.dart';
import 'interfaces/iglobal_settings_repository.dart';
/// Реализация репозитория глобальных настроек с использованием Hive
class HiveGlobalSettingsRepository implements IGlobalSettingsRepository {
static const String _settingsKey = 'global_settings';
final Box<GlobalSettings> _box;
HiveGlobalSettingsRepository(this._box);
@override
Future<String?> getCurrentUserId() async {
try {
final settings = _box.get(_settingsKey);
return settings?.currentUserId;
} catch (e) {
throw Exception('Ошибка получения ID текущего пользователя: $e');
}
}
@override
Future<void> setCurrentUserId(String? userId) async {
try {
final settings = _box.get(_settingsKey) ?? GlobalSettings();
settings.currentUserId = userId;
await _box.put(_settingsKey, settings);
} catch (e) {
throw Exception('Ошибка сохранения ID текущего пользователя: $e');
}
}
}
@@ -0,0 +1,40 @@
import 'package:budget_app/data/repositories/interfaces/isettings_repository.dart';
import 'package:budget_app/models/app_settings.dart';
import 'package:hive_ce/hive.dart';
/// Реализация репозитория настроек с использованием Hive
class HiveSettingsRepository implements ISettingsRepository {
final Box<AppSettings> _box;
HiveSettingsRepository(this._box);
@override
Future<AppSettings> getSettings(String userId) async {
try {
// Возвращаем настройки пользователя или создаем новые по умолчанию
final settings = _box.get(userId, defaultValue: AppSettings(userId: userId));
return settings!; // Гарантируем возврат не-null значения
} catch (e) {
throw Exception('Ошибка получения настроек: $e');
}
}
@override
Future<void> saveSettings(AppSettings settings) async {
try {
// Сохраняем настройки с ключом = userId
await _box.put(settings.userId, settings);
} catch (e) {
throw Exception('Ошибка сохранения настроек: $e');
}
}
@override
Future<void> deleteSettings(String userId) async {
try {
await _box.delete(userId);
} catch (e) {
throw Exception('Ошибка удаления настроек: $e');
}
}
}
@@ -0,0 +1,9 @@
/// Интерфейс для глобальных настроек, не связанных с конкретным пользователем
abstract class IGlobalSettingsRepository {
/// Получает ID текущего пользователя
Future<String?> getCurrentUserId();
/// Устанавливает ID текущего пользователя
Future<void> setCurrentUserId(String? userId);
}
@@ -0,0 +1,16 @@
import 'package:budget_app/models/app_settings.dart';
/// Интерфейс репозитория для работы с настройками приложения
abstract class ISettingsRepository {
/// Получает настройки для указанного пользователя
/// [userId] - идентификатор пользователя
Future<AppSettings> getSettings(String userId);
/// Сохраняет настройки
/// [settings] - объект настроек для сохранения
Future<void> saveSettings(AppSettings settings);
/// Удаляет настройки для указанного пользователя
/// [userId] - идентификатор пользователя
Future<void> deleteSettings(String userId);
}
+6
View File
@@ -4,7 +4,9 @@
import 'package:hive_ce/hive.dart';
import 'package:budget_app/hive/hive_adapters.dart';
import 'package:budget_app/models/app_settings.dart';
import 'package:budget_app/models/category.dart';
import 'package:budget_app/models/global_settings.dart';
import 'package:budget_app/models/sms_message.dart';
import 'package:budget_app/models/tag.dart';
import 'package:budget_app/models/transaction_record.dart';
@@ -12,8 +14,10 @@ import 'package:budget_app/models/user.dart';
extension HiveRegistrar on HiveInterface {
void registerAdapters() {
registerAdapter(AppSettingsAdapter());
registerAdapter(CategoryAdapter());
registerAdapter(ColorAdapter());
registerAdapter(GlobalSettingsAdapter());
registerAdapter(IconDataAdapter());
registerAdapter(SmsMessageAdapter());
registerAdapter(TagAdapter());
@@ -24,8 +28,10 @@ extension HiveRegistrar on HiveInterface {
extension IsolatedHiveRegistrar on IsolatedHiveInterface {
void registerAdapters() {
registerAdapter(AppSettingsAdapter());
registerAdapter(CategoryAdapter());
registerAdapter(ColorAdapter());
registerAdapter(GlobalSettingsAdapter());
registerAdapter(IconDataAdapter());
registerAdapter(SmsMessageAdapter());
registerAdapter(TagAdapter());
+36 -13
View File
@@ -4,10 +4,14 @@ import 'package:get_it/get_it.dart';
import 'data/database/hive_service.dart';
import 'data/repositories/hive_category_repository.dart';
import 'data/repositories/hive_global_settings_repository.dart';
import 'data/repositories/hive_settings_repository.dart'; // Добавляем импорт HiveSettingsRepository
import 'data/repositories/hive_tag_repository.dart';
import 'data/repositories/hive_transaction_repository.dart';
import 'data/repositories/hive_user_repository.dart';
import 'data/repositories/interfaces/icategory_repository.dart';
import 'data/repositories/interfaces/iglobal_settings_repository.dart';
import 'data/repositories/interfaces/isettings_repository.dart'; // Добавляем импорт ISettingsRepository
import 'data/repositories/interfaces/itag_repository.dart';
import 'data/repositories/interfaces/itransaction_repository.dart';
import 'data/repositories/interfaces/iuser_repository.dart';
@@ -17,8 +21,8 @@ import 'logic/settings/settings_cubit.dart'; // Импортируем SettingsC
import 'logic/sms/sms_cubit.dart';
import 'logic/tag/tag_cubit.dart';
import 'logic/transaction/transaction_bloc.dart';
import 'logic/user/user_cubit.dart'; // Импортируем UserCubit вместо UserService
import 'services/sms_service.dart';
import 'services/user_service.dart';
final getIt = GetIt.instance;
@@ -31,9 +35,6 @@ Future<void> initDependencies() async {
HiveCategoryRepository(HiveService.categories),
);
getIt.registerSingleton<ISmsMessageRepository>(
HiveSmsMessageRepository(HiveService.smsMessages),
);
getIt.registerSingleton<ITagRepository>(HiveTagRepository(HiveService.tags));
getIt.registerSingleton<ITransactionRepository>(
@@ -44,20 +45,31 @@ Future<void> initDependencies() async {
HiveUserRepository(HiveService.users),
);
// Services
getIt.registerSingleton<UserService>(
UserService(getIt(), getIt(), getIt(), getIt()),
// Регистрируем ISettingsRepository с реализацией HiveSettingsRepository
getIt.registerSingleton<ISettingsRepository>(
HiveSettingsRepository(HiveService.appSettings),
);
getIt.registerSingleton<IGlobalSettingsRepository>(
HiveGlobalSettingsRepository(HiveService.globalSettings),
);
// Services
getIt.registerSingleton<SmsService>(SmsService());
// Регистрация сервисов
getIt.registerSingleton<SettingsCubit>(
SettingsCubit(getIt()),
); // Регистрируем Cubit и передаем IUserRepository
// Регистрируем UserCubit с передачей всех необходимых репозиториев
getIt.registerSingleton<UserCubit>(
UserCubit(
settingsRepository: getIt<IGlobalSettingsRepository>(),
userRepository: getIt<IUserRepository>(),
categoryRepository: getIt<ICategoryRepository>(),
tagRepository: getIt<ITagRepository>(),
transactionRepository: getIt<ITransactionRepository>(),
),
);
// Blocs
getIt.registerFactory<AuthBloc>(() => AuthBloc(userService: getIt()));
getIt.registerFactory<AuthBloc>(() => AuthBloc(userCubit: getIt()));
getIt.registerFactory<TransactionBloc>(
() => TransactionBloc(transactionRepository: getIt()),
);
@@ -65,7 +77,18 @@ Future<void> initDependencies() async {
getIt.registerFactory<SmsCubit>(() => SmsCubit(getIt(), getIt(), getIt()));
getIt.registerFactory<CategoryCubit>(
() => CategoryCubit(getIt()),
); // Регистрируем CategoryCubit с зависимостью от UserService
); // Регистрируем CategoryCubit с зависимостью от UserCubit
getIt.registerFactory<TagCubit>(() => TagCubit(getIt()));
getIt.registerSingleton<ISmsMessageRepository>(
HiveSmsMessageRepository(HiveService.smsMessages),
);
// Регистрация сервисов
getIt.registerSingleton<SettingsCubit>(
SettingsCubit(getIt()),
); // Регистрируем Cubit и передаем IUserRepository и UserService
}
+9 -7
View File
@@ -1,24 +1,25 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:budget_app/models/user.dart';
import 'package:budget_app/services/user_service.dart';
import 'package:budget_app/logic/user/user_cubit.dart';
part 'auth_event.dart';
part 'auth_state.dart';
class AuthBloc extends Bloc<AuthEvent, AuthState> {
final UserService _userService;
final UserCubit _userCubit;
AuthBloc({required UserService userService}) : _userService = userService, super(AuthInitial()) {
AuthBloc({required UserCubit userCubit}) : _userCubit = userCubit, super(AuthInitial()) {
on<AuthStarted>(_onAuthStarted);
on<AuthLoggedIn>(_onAuthLoggedIn);
on<AuthLoggedOut>(_onAuthLoggedOut);
}
void _onAuthStarted(AuthStarted event, Emitter<AuthState> emit) {
final user = _userService.currentUser;
if (user != null) {
emit(AuthAuthenticated(user: user));
// Получаем текущее состояние пользователя из UserCubit
final userState = _userCubit.state;
if (userState is UserLoaded && userState.user != null) {
emit(AuthAuthenticated(user: userState.user!));
} else {
emit(AuthUnauthenticated());
}
@@ -29,7 +30,8 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> {
}
void _onAuthLoggedOut(AuthLoggedOut event, Emitter<AuthState> emit) {
_userService.logout();
// Вызываем logout у UserCubit
_userCubit.logout();
emit(AuthUnauthenticated());
}
}
+11 -7
View File
@@ -2,20 +2,22 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:budget_app/models/category.dart';
import 'package:hive_ce/hive.dart';
import 'package:budget_app/services/user_service.dart'; // Импортируем UserService
import 'package:budget_app/logic/user/user_cubit.dart'; // Импортируем UserCubit
class CategoryCubit extends Cubit<List<Category>> {
final UserService _userService; // Добавляем зависимость от UserService
final UserCubit _userCubit; // Добавляем зависимость от UserCubit
final Box<Category> _categoryBox; // Используем Box<Category> для типизации
CategoryCubit(this._userService) // Принимаем UserService через конструктор
CategoryCubit(this._userCubit) // Принимаем UserCubit через конструктор
: _categoryBox = Hive.box<Category>('categories'),
super([]);
// Загружает категории, фильтруя их по userId текущего пользователя
void loadCategories() {
final currentUserId = _userService.currentUser?.id;
if (currentUserId != null) {
// Получаем текущего пользователя из состояния UserCubit
final userState = _userCubit.state;
if (userState is UserLoaded && userState.user != null) {
final currentUserId = userState.user!.id;
final userCategories = _categoryBox.values
.where((category) => category.userId == currentUserId)
.toList();
@@ -27,8 +29,10 @@ class CategoryCubit extends Cubit<List<Category>> {
// Добавляет новую категорию, присваивая ей userId текущего пользователя
void addCategory(Category category) {
final currentUserId = _userService.currentUser?.id;
if (currentUserId != null) {
// Получаем текущего пользователя из состояния UserCubit
final userState = _userCubit.state;
if (userState is UserLoaded && userState.user != null) {
final currentUserId = userState.user!.id;
final newCategory = category.copyWith(userId: currentUserId); // Присваиваем userId
_categoryBox.put(newCategory.id, newCategory);
loadCategories();
+59 -50
View File
@@ -1,68 +1,77 @@
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:budget_app/data/repositories/interfaces/iuser_repository.dart'; // Комментарий: Импортируем интерфейс репозитория пользователя
import 'package:budget_app/models/user.dart'; // Комментарий: Импортируем модель пользователя
import 'package:flutter/foundation.dart';
import '../../data/repositories/interfaces/isettings_repository.dart';
import '../../models/app_settings.dart';
part 'settings_state.dart';
class SettingsCubit extends Cubit<SettingsState> {
final IUserRepository _userRepository; // Комментарий: Зависимость от репозитория пользователя
final ISettingsRepository _settingsRepository;
SettingsCubit(this._userRepository) : super(const SettingsInitial()) { // Комментарий: Принимаем репозиторий в конструкторе
_loadSettings();
}
SettingsCubit(this._settingsRepository) : super(SettingsInitial());
void _loadSettings() async {
// Комментарий: Получаем всех пользователей. В реальном приложении здесь будет логика получения текущего залогиненного пользователя.
final users = await _userRepository.getAll();
User currentUser;
if (users.isEmpty) {
// Комментарий: Если пользователей нет, создаем нового с дефолтными настройками.
currentUser = User(name: 'Default User', email: 'default@example.com');
await _userRepository.add(currentUser);
} else {
// Комментарий: Используем первого пользователя как текущего.
currentUser = users.first;
}
emit(state.copyWith(
isDarkMode: currentUser.isDarkMode,
languageCode: currentUser.languageCode,
defaultCurrency: currentUser.defaultCurrency,
));
}
Future<void> setDarkMode(bool value) async {
final users = await _userRepository.getAll();
if (users.isNotEmpty) {
final currentUser = users.first;
final updatedUser = currentUser.copyWith(isDarkMode: value);
await _userRepository.update(updatedUser);
emit(state.copyWith(isDarkMode: value));
Future<void> loadSettings(String userId) async {
emit(SettingsLoading());
try {
final settings = await _settingsRepository.getSettings(userId);
emit(SettingsLoaded(
isDarkMode: settings.isDarkMode,
languageCode: settings.languageCode,
defaultCurrency: settings.defaultCurrency,
));
} catch (e) {
emit(SettingsError(e.toString()));
}
}
Future<void> setLanguageCode(String code) async {
final users = await _userRepository.getAll();
if (users.isNotEmpty) {
final currentUser = users.first;
final updatedUser = currentUser.copyWith(languageCode: code);
await _userRepository.update(updatedUser);
emit(state.copyWith(languageCode: code));
Future<void> toggleDarkMode(bool value, String userId) async {
if (state is SettingsLoaded) {
try {
final currentState = state as SettingsLoaded;
final newState = currentState.copyWith(isDarkMode: value);
emit(newState);
await _saveSettings(newState, userId);
} catch (e) {
emit(SettingsError(e.toString()));
}
}
}
Future<void> setDefaultCurrency(String currencyCode) async {
final users = await _userRepository.getAll();
if (users.isNotEmpty) {
final currentUser = users.first;
final updatedUser = currentUser.copyWith(defaultCurrency: currencyCode);
await _userRepository.update(updatedUser);
emit(state.copyWith(defaultCurrency: currencyCode));
Future<void> changeLanguage(String languageCode, String userId) async {
if (state is SettingsLoaded) {
try {
final currentState = state as SettingsLoaded;
final newState = currentState.copyWith(languageCode: languageCode);
emit(newState);
await _saveSettings(newState, userId);
} catch (e) {
emit(SettingsError(e.toString()));
}
}
}
void toggleTheme() {
setDarkMode(!state.isDarkMode);
Future<void> changeCurrency(String currency, String userId) async {
if (state is SettingsLoaded) {
try {
final currentState = state as SettingsLoaded;
final newState = currentState.copyWith(defaultCurrency: currency);
emit(newState);
await _saveSettings(newState, userId);
} catch (e) {
emit(SettingsError(e.toString()));
}
}
}
}
Future<void> _saveSettings(SettingsLoaded settings, String userId) async {
final appSettings = AppSettings(
userId: userId,
isDarkMode: settings.isDarkMode,
languageCode: settings.languageCode,
defaultCurrency: settings.defaultCurrency,
);
await _settingsRepository.saveSettings(appSettings);
}
}
+29 -10
View File
@@ -1,32 +1,51 @@
part of 'settings_cubit.dart';
class SettingsState extends Equatable {
@immutable
abstract class SettingsState extends Equatable {
const SettingsState();
@override
List<Object> get props => [];
}
class SettingsInitial extends SettingsState {
const SettingsInitial();
}
class SettingsLoading extends SettingsState {}
class SettingsLoaded extends SettingsState {
final bool isDarkMode;
final String languageCode;
final String defaultCurrency; // Новое поле для валюты по умолчанию
final String defaultCurrency;
const SettingsState({
const SettingsLoaded({
required this.isDarkMode,
required this.languageCode,
required this.defaultCurrency, // Теперь обязательный параметр
required this.defaultCurrency,
});
@override
List<Object> get props => [isDarkMode, languageCode, defaultCurrency];
SettingsState copyWith({
SettingsLoaded copyWith({
bool? isDarkMode,
String? languageCode,
String? defaultCurrency, // Добавляем в copyWith
String? defaultCurrency,
}) {
return SettingsState(
return SettingsLoaded(
isDarkMode: isDarkMode ?? this.isDarkMode,
languageCode: languageCode ?? this.languageCode,
defaultCurrency: defaultCurrency ?? this.defaultCurrency, // Обновляем значение
defaultCurrency: defaultCurrency ?? this.defaultCurrency,
);
}
}
class SettingsInitial extends SettingsState {
const SettingsInitial() : super(isDarkMode: false, languageCode: 'ru', defaultCurrency: 'RUB'); // Инициализируем валюту по умолчанию
class SettingsError extends SettingsState {
final String message;
const SettingsError(this.message);
@override
List<Object> get props => [message];
}
+6 -5
View File
@@ -2,7 +2,7 @@ import 'package:budget_app/logic/sms/sms_state.dart';
import 'package:budget_app/models/sms_message.dart';
import 'package:budget_app/models/transaction_record.dart';
import 'package:budget_app/services/sms_service.dart';
import 'package:budget_app/services/user_service.dart';
import 'package:budget_app/logic/user/user_cubit.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:budget_app/data/repositories/interfaces/isms_message_repository.dart';
@@ -12,12 +12,12 @@ import 'package:budget_app/data/repositories/interfaces/isms_message_repository.
class SmsCubit extends Cubit<SmsState> {
final SmsService _smsService;
final ISmsMessageRepository _smsRepository;
final UserService _userService;
final UserCubit _userCubit;
SmsCubit(
this._smsService,
this._smsRepository,
this._userService
this._userCubit
) : super(SmsInitial());
/// Загружает последние 10 SMS сообщений.
@@ -49,8 +49,9 @@ class SmsCubit extends Cubit<SmsState> {
// Комментарий: Запрашиваем разрешение на чтение SMS.
final hasPermissions = await _smsService.requestPermissions();
if (hasPermissions) {
// Комментарий: Получаем время последней синхронизации из настроек пользователя.
final user = _userService.currentUser;
// Комментарий: Получаем текущего пользователя из состояния UserCubit.
final userState = _userCubit.state;
final user = userState is UserLoaded ? userState.user : null;
// Комментарий: Получаем все SMS-сообщения с момента последней синхронизации.
final messages = await _smsService.getSmsMessagesSince(DateTime.now());
+11 -7
View File
@@ -2,20 +2,22 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:budget_app/models/tag.dart';
import 'package:hive_ce/hive.dart';
import 'package:budget_app/services/user_service.dart';
import 'package:budget_app/logic/user/user_cubit.dart';
class TagCubit extends Cubit<List<Tag>> {
final UserService _userService;
final UserCubit _userCubit;
final Box<Tag> _tagBox;
TagCubit(this._userService)
TagCubit(this._userCubit)
: _tagBox = Hive.box<Tag>('tags'),
super([]);
void loadTags() {
final currentUserId = _userService.currentUser?.id;
if (currentUserId != null) {
// Получаем текущего пользователя из состояния UserCubit
final userState = _userCubit.state;
if (userState is UserLoaded && userState.user != null) {
final currentUserId = userState.user!.id;
final userTags = _tagBox.values
.where((tag) => tag.userId == currentUserId)
.toList();
@@ -27,8 +29,10 @@ class TagCubit extends Cubit<List<Tag>> {
void addTag(Tag tag) {
final currentUserId = _userService.currentUser?.id;
if (currentUserId != null) {
// Получаем текущего пользователя из состояния UserCubit
final userState = _userCubit.state;
if (userState is UserLoaded && userState.user != null) {
final currentUserId = userState.user!.id;
final newTag = tag.copyWith(userId: currentUserId);
_tagBox.put(newTag.id, newTag);
loadTags();
+171
View File
@@ -0,0 +1,171 @@
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:flutter/foundation.dart';
import 'package:logger/logger.dart';
import '/data/repositories/interfaces/iglobal_settings_repository.dart';
import '/data/repositories/interfaces/iuser_repository.dart';
import '/data/repositories/interfaces/icategory_repository.dart';
import '/data/repositories/interfaces/itag_repository.dart';
import '/data/repositories/interfaces/itransaction_repository.dart';
import '/models/user.dart';
import '/utils/category_utils.dart';
import '/utils/tag_utils.dart';
import '/utils/transaction_utils.dart';
part 'user_state.dart';
/// Cubit для управления состоянием пользователя
class UserCubit extends Cubit<UserState> {
final IGlobalSettingsRepository _settingsRepository;
final IUserRepository _userRepository;
final ICategoryRepository _categoryRepository;
final ITagRepository _tagRepository;
final ITransactionRepository _transactionRepository;
final Logger _logger = Logger();
UserCubit({
required IGlobalSettingsRepository settingsRepository,
required IUserRepository userRepository,
required ICategoryRepository categoryRepository,
required ITagRepository tagRepository,
required ITransactionRepository transactionRepository,
}) : _settingsRepository = settingsRepository,
_userRepository = userRepository,
_categoryRepository = categoryRepository,
_tagRepository = tagRepository,
_transactionRepository = transactionRepository,
super(UserInitial()) {
_init();
}
Future<void> _init() async {
emit(UserLoading());
try {
final userId = await _settingsRepository.getCurrentUserId();
User? currentUser;
if (userId != null) {
currentUser = await _userRepository.getById(userId);
if (currentUser == null) {
_logger.w('User with ID $userId not found. Clearing key.');
await _settingsRepository.setCurrentUserId(null);
}
}
if (currentUser == null) {
final allUsers = await _userRepository.getAll();
if (allUsers.isEmpty) {
_logger.i('No users found, creating default user');
currentUser = await _createDefaultUser();
} else {
_logger.i('Setting first user as current');
currentUser = allUsers.first;
await _setCurrentUser(currentUser);
}
}
emit(UserLoaded(currentUser));
} catch (e, stack) {
_logger.e('Error initializing user cubit', error: e, stackTrace: stack);
emit(UserError('Ошибка загрузки пользователя: ${e.toString()}'));
}
}
Future<User> _createDefaultUser() async {
final user = User(
name: 'Пользователь по умолчанию',
email: 'default@example.com',
);
await _userRepository.add(user);
// Создаем начальные данные для нового пользователя
await _createInitialData(user.id);
await _setCurrentUser(user);
return user;
}
/// Создает начальные данные для нового пользователя (категории, теги, транзакции)
Future<void> _createInitialData(String userId) async {
try {
// Добавляем начальные категории
await _categoryRepository.addAll(
CategoryUtils.getDefaultCategories(userId),
);
// Добавляем начальные теги
await _tagRepository.addAll(
TagUtils.getDefaultTags(userId),
);
// Добавляем примеры транзакций
await _transactionRepository.addAll(
TransactionUtils.getSampleTransactions(userId),
);
_logger.i('Initial data created for user: $userId');
} catch (e, stack) {
_logger.e('Error creating initial data for user: $userId', error: e, stackTrace: stack);
// Не прерываем создание пользователя из-за ошибки создания начальных данных
}
}
Future<void> _setCurrentUser(User user) async {
await _settingsRepository.setCurrentUserId(user.id);
emit(UserLoaded(user));
}
/// Устанавливает текущего пользователя
Future<void> setCurrentUser(User user) async {
emit(UserLoading());
try {
await _setCurrentUser(user);
} catch (e, stack) {
_logger.e('Error setting current user', error: e, stackTrace: stack);
emit(UserError('Ошибка установки пользователя: ${e.toString()}'));
}
}
/// Выход из системы
Future<void> logout() async {
emit(UserLoading());
try {
await _settingsRepository.setCurrentUserId(null);
emit(UserLoaded(null));
} catch (e, stack) {
_logger.e('Error during logout', error: e, stackTrace: stack);
emit(UserError('Ошибка выхода из системы: ${e.toString()}'));
}
}
/// Создает нового пользователя и устанавливает его как текущего
Future<void> createAndSetUser(String name, String email) async {
emit(UserLoading());
try {
final user = User(
name: name,
email: email,
);
await _userRepository.add(user);
// Создаем начальные данные для нового пользователя
await _createInitialData(user.id);
await _setCurrentUser(user);
} catch (e, stack) {
_logger.e('Error creating user', error: e, stackTrace: stack);
emit(UserError('Ошибка создания пользователя: ${e.toString()}'));
}
}
/// Возвращает всех пользователей
Future<List<User>> getAllUsers() async {
try {
return await _userRepository.getAll();
} catch (e, stack) {
_logger.e('Error getting all users', error: e, stackTrace: stack);
return [];
}
}
}
+31
View File
@@ -0,0 +1,31 @@
part of 'user_cubit.dart';
/// Состояния управления пользователем
@immutable
abstract class UserState {
const UserState();
}
/// Начальное состояние (инициализация)
class UserInitial extends UserState {
const UserInitial();
}
/// Состояние загрузки данных
class UserLoading extends UserState {
const UserLoading();
}
/// Состояние успешной загрузки пользователя
class UserLoaded extends UserState {
final User? user;
const UserLoaded(this.user);
}
/// Состояние ошибки
class UserError extends UserState {
final String message;
const UserError(this.message);
}
+13 -4
View File
@@ -11,6 +11,7 @@ import 'injection_container.dart' as di;
import 'logic/settings/settings_cubit.dart'; // Импортируем SettingsCubit
import 'logic/sms/sms_cubit.dart';
import 'logic/transaction/transaction_bloc.dart';
import 'logic/user/user_cubit.dart'; // Импортируем UserCubit
void main() async {
WidgetsFlutterBinding.ensureInitialized();
@@ -30,6 +31,7 @@ class _MyAppState extends State<MyApp> {
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider(create: (context) => GetIt.instance<UserCubit>()), // Добавляем UserCubit для управления пользователями
BlocProvider(create: (context) => GetIt.instance<AuthBloc>()..add(AuthStarted())),
BlocProvider(create: (context) => GetIt.instance<SettingsCubit>()),
BlocProvider(create: (context) => GetIt.instance<TransactionBloc>()),
@@ -37,12 +39,19 @@ class _MyAppState extends State<MyApp> {
],
child: BlocBuilder<SettingsCubit, SettingsState>(
builder: (context, settingsState) {
// Используем значения по умолчанию, если настройки не загружены
final isDarkMode = settingsState is SettingsLoaded
? settingsState.isDarkMode
: false;
final languageCode = settingsState is SettingsLoaded
? settingsState.languageCode
: 'ru';
return MaterialApp(
title: AppLocalizations.of(context)?.appTitle ?? 'Budget App', // Используем локализованный заголовок
title: AppLocalizations.of(context)?.appTitle ?? 'Budget App',
theme: AppTheme.lightTheme(),
darkTheme: AppTheme.darkTheme(),
themeMode: settingsState.isDarkMode ? ThemeMode.dark : ThemeMode.light,
// Добавляем локализацию
themeMode: isDarkMode ? ThemeMode.dark : ThemeMode.light,
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
@@ -50,7 +59,7 @@ class _MyAppState extends State<MyApp> {
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: AppLocalizations.supportedLocales,
locale: Locale(settingsState.languageCode), // Устанавливаем текущий язык из настроек
locale: Locale(languageCode),
home: BlocBuilder<AuthBloc, AuthState>(
builder: (context, authState) {
if (authState is AuthAuthenticated) {
+88
View File
@@ -0,0 +1,88 @@
import 'package:equatable/equatable.dart';
import 'package:hive_ce/hive.dart';
part 'app_settings.g.dart';
@HiveType(typeId: 1005)
class AppSettings extends Equatable {
/// Ссылка на ID пользователя, которому принадлежат настройки
@HiveField(0)
final String userId;
/// Код языка интерфейса (например, 'ru', 'en')
@HiveField(1)
final String languageCode;
/// Режим темной темы
@HiveField(2)
final bool isDarkMode;
/// Валюта по умолчанию
@HiveField(3)
final String defaultCurrency;
/// Дата последнего обновления настроек
@HiveField(4)
final DateTime updatedAt;
AppSettings({
required this.userId,
this.languageCode = 'ru',
this.isDarkMode = false,
this.defaultCurrency = 'RUB',
DateTime? updatedAt,
}) : updatedAt = updatedAt ?? DateTime.now();
/// Преобразование в Map для сохранения
Map<String, dynamic> toMap() {
return {
'userId': userId,
'languageCode': languageCode,
'isDarkMode': isDarkMode,
'defaultCurrency': defaultCurrency,
'updatedAt': updatedAt.toIso8601String(),
};
}
/// Создание из Map
factory AppSettings.fromMap(Map<String, dynamic> map) {
return AppSettings(
userId: map['userId'],
languageCode: map['languageCode'] ?? 'ru',
isDarkMode: map['isDarkMode'] ?? false,
defaultCurrency: map['defaultCurrency'] ?? 'RUB',
updatedAt: DateTime.parse(map['updatedAt']),
);
}
/// Создание копии с обновленными значениями
AppSettings copyWith({
String? userId,
String? languageCode,
bool? isDarkMode,
String? defaultCurrency,
}) {
return AppSettings(
userId: userId ?? this.userId,
languageCode: languageCode ?? this.languageCode,
isDarkMode: isDarkMode ?? this.isDarkMode,
defaultCurrency: defaultCurrency ?? this.defaultCurrency,
updatedAt: DateTime.now(),
);
}
@override
List<Object> get props => [
userId,
languageCode,
isDarkMode,
defaultCurrency,
updatedAt,
];
@override
String toString() {
return 'AppSettings(userId: $userId, languageCode: $languageCode, '
'isDarkMode: $isDarkMode, defaultCurrency: $defaultCurrency)';
}
}
+53
View File
@@ -0,0 +1,53 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'app_settings.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class AppSettingsAdapter extends TypeAdapter<AppSettings> {
@override
final typeId = 1005;
@override
AppSettings read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return AppSettings(
userId: fields[0] as String,
languageCode: fields[1] == null ? 'ru' : fields[1] as String,
isDarkMode: fields[2] == null ? false : fields[2] as bool,
defaultCurrency: fields[3] == null ? 'RUB' : fields[3] as String,
updatedAt: fields[4] as DateTime?,
);
}
@override
void write(BinaryWriter writer, AppSettings obj) {
writer
..writeByte(5)
..writeByte(0)
..write(obj.userId)
..writeByte(1)
..write(obj.languageCode)
..writeByte(2)
..write(obj.isDarkMode)
..writeByte(3)
..write(obj.defaultCurrency)
..writeByte(4)
..write(obj.updatedAt);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is AppSettingsAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
+13
View File
@@ -0,0 +1,13 @@
import 'package:hive_ce/hive.dart';
part 'global_settings.g.dart';
@HiveType(typeId: 1006)
class GlobalSettings extends HiveObject {
@HiveField(0)
String? currentUserId;
GlobalSettings({
this.currentUserId,
});
}
+39
View File
@@ -0,0 +1,39 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'global_settings.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class GlobalSettingsAdapter extends TypeAdapter<GlobalSettings> {
@override
final typeId = 1006;
@override
GlobalSettings read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return GlobalSettings(currentUserId: fields[0] as String?);
}
@override
void write(BinaryWriter writer, GlobalSettings obj) {
writer
..writeByte(1)
..writeByte(0)
..write(obj.currentUserId);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is GlobalSettingsAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
+19 -46
View File
@@ -3,7 +3,7 @@ import 'package:hive_ce/hive.dart';
import '../utils/id_generator.dart';
// Указываем Hive, что это модель для хранения в базе данных
// typeId: 3 - уникальный идентификатор типа для Hive (у нас уже есть 0,1,2)
// typeId: 1003 - уникальный идентификатор типа для Hive
part 'user.g.dart';
@HiveType(typeId: 1003)
@@ -11,48 +11,35 @@ part 'user.g.dart';
/// Содержит основную информацию для идентификации пользователя
class User extends Equatable {
/// Уникальный идентификатор пользователя
@HiveField(0) // Поле 0 в Hive - первое поле модели
@HiveField(0)
final String id;
/// Имя пользователя для отображения в интерфейсе
@HiveField(1) // Поле 1 в Hive - второе поле модели
@HiveField(1)
final String name;
/// Email пользователя (может использоваться для входа в будущем)
@HiveField(2) // Поле 2 в Hive - третье поле модели
@HiveField(2)
final String email;
@HiveField(3) // Поле 3 в Hive - язык пользователя
final String languageCode; // Переименовано с 'language' на 'languageCode'
@HiveField(4)
@HiveField(3)
/// Дата и время последнего обновления объекта
final DateTime updatedAt;
@HiveField(5) // Новое поле для режима темы (светлая/темная)
final bool isDarkMode;
@HiveField(6) // Новое поле для валюты по умолчанию
final String defaultCurrency;
// Комментарий: Добавлено поле для хранения времени последней синхронизации SMS.
@HiveField(7)
@HiveField(4)
final DateTime lastSmsSyncTime;
/// Конструктор пользователя
/// id генерируется автоматически, если не передан
User({
String? id, // Опциональный параметр - если null, сгенерируется автоматически
required this.name, // Обязательный параметр
required this.email, // Обязательный параметр
this.languageCode = 'ru', // Язык по умолчанию - русский
DateTime? updatedAt, // Добавлено поле updatedAt, теперь необязательное
this.isDarkMode = false, // По умолчанию светлая тема
this.defaultCurrency = 'RUB', // Валюта по умолчанию - RUB
String? id,
required this.name,
required this.email,
DateTime? updatedAt,
DateTime? lastSmsSyncTime,
}) : id = id ?? IdGenerator.generateId(), // Если id не передан, генерируем новый
}) : id = id ?? IdGenerator.generateId(),
updatedAt = updatedAt ?? DateTime.now(),
// Комментарий: Устанавливаем время последней синхронизации SMS по умолчанию на начало предыдущего месяца.
lastSmsSyncTime = lastSmsSyncTime ??
DateTime(DateTime.now().year, DateTime.now().month - 1, 1);
@@ -62,10 +49,7 @@ class User extends Equatable {
'id': id,
'name': name,
'email': email,
'languageCode': languageCode, // Обновлено имя поля
'updatedAt': updatedAt.toIso8601String(), // Добавлено updatedAt в Map
'isDarkMode': isDarkMode,
'defaultCurrency': defaultCurrency,
'updatedAt': updatedAt.toIso8601String(),
'lastSmsSyncTime': lastSmsSyncTime.toIso8601String(),
};
}
@@ -76,10 +60,7 @@ class User extends Equatable {
id: map['id'],
name: map['name'],
email: map['email'],
languageCode: map['languageCode'] ?? 'ru', // Обновлено имя поля
updatedAt: DateTime.parse(map['updatedAt']), // Добавлено updatedAt при создании из Map
isDarkMode: map['isDarkMode'] ?? false,
defaultCurrency: map['defaultCurrency'] ?? 'RUB',
updatedAt: DateTime.parse(map['updatedAt']),
lastSmsSyncTime: map['lastSmsSyncTime'] != null
? DateTime.parse(map['lastSmsSyncTime'])
: DateTime(DateTime.now().year, DateTime.now().month - 1, 1),
@@ -89,33 +70,25 @@ class User extends Equatable {
/// Переопределяем toString для удобного отображения в логах
@override
String toString() {
return 'User(id: $id, name: $name, email: $email, languageCode: $languageCode, isDarkMode: $isDarkMode, defaultCurrency: $defaultCurrency, lastSmsSyncTime: $lastSmsSyncTime)';
return 'User(id: $id, name: $name, email: $email, lastSmsSyncTime: $lastSmsSyncTime)';
}
/// Метод для создания копии объекта с возможностью изменения полей
// Используем Equatable для сравнения объектов по их свойствам.
@override
List<Object?> get props => [id];
User copyWith({
String? id,
String? name,
String? email,
String? languageCode, // Обновлено имя поля
bool? isDarkMode,
String? defaultCurrency,
DateTime? lastSmsSyncTime,
}) {
return User(
id: id ?? this.id,
name: name ?? this.name,
email: email ?? this.email,
languageCode: languageCode ?? this.languageCode, // Обновлено имя поля
updatedAt: DateTime.now(), // Обновляем updatedAt при каждом копировании
isDarkMode: isDarkMode ?? this.isDarkMode,
defaultCurrency: defaultCurrency ?? this.defaultCurrency,
updatedAt: DateTime.now(),
lastSmsSyncTime: lastSmsSyncTime ?? this.lastSmsSyncTime,
);
}
// Используем Equatable для сравнения объектов по их свойствам.
// В данном случае, мы считаем пользователей уникальными по их 'id'.
@override
List<Object?> get props => [id];
}
+4 -13
View File
@@ -20,18 +20,15 @@ class UserAdapter extends TypeAdapter<User> {
id: fields[0] as String?,
name: fields[1] as String,
email: fields[2] as String,
languageCode: fields[3] == null ? 'ru' : fields[3] as String,
updatedAt: fields[4] as DateTime?,
isDarkMode: fields[5] == null ? false : fields[5] as bool,
defaultCurrency: fields[6] == null ? 'RUB' : fields[6] as String,
lastSmsSyncTime: fields[7] as DateTime?,
updatedAt: fields[3] as DateTime?,
lastSmsSyncTime: fields[4] as DateTime?,
);
}
@override
void write(BinaryWriter writer, User obj) {
writer
..writeByte(8)
..writeByte(5)
..writeByte(0)
..write(obj.id)
..writeByte(1)
@@ -39,14 +36,8 @@ class UserAdapter extends TypeAdapter<User> {
..writeByte(2)
..write(obj.email)
..writeByte(3)
..write(obj.languageCode)
..writeByte(4)
..write(obj.updatedAt)
..writeByte(5)
..write(obj.isDarkMode)
..writeByte(6)
..write(obj.defaultCurrency)
..writeByte(7)
..writeByte(4)
..write(obj.lastSmsSyncTime);
}
+11 -2
View File
@@ -2,8 +2,9 @@ import 'package:budget_app/l10n/app_localizations.dart';
import 'package:budget_app/logic/category/category_cubit.dart';
import 'package:budget_app/models/category.dart';
import 'package:budget_app/pages/category/category_edit_page.dart';
import 'package:budget_app/pages/category/widgets/category_list_item.dart';
import 'package:budget_app/pages/category/widgets/add_category_button.dart';
import 'package:budget_app/pages/category/widgets/category_list_item.dart';
import 'package:budget_app/logic/user/user_cubit.dart'; // Импортируем UserCubit
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:get_it/get_it.dart';
@@ -64,12 +65,20 @@ class _CategoryListPageState extends State<CategoryListPage> {
MaterialPageRoute(
builder: (_) => CategoryEditPage(
onSave: (name, color, icon, isIncome) {
// Получаем ID текущего пользователя из UserCubit
final userState = context.read<UserCubit>().state;
if (userState is! UserLoaded || userState.user == null) {
// Обработка случая, когда пользователь не аутентифицирован
// Возможно, показать сообщение об ошибке или перенаправить на страницу входа
return;
}
final currentUserId = userState.user!.id;
final newCategory = Category(
name: name,
color: color,
icon: icon,
isIncome: isIncome,
userId: 'default_user',
userId: currentUserId,
);
context.read<CategoryCubit>().addCategory(newCategory);
_listKey.currentState?.insertItem(0);
+20 -21
View File
@@ -1,12 +1,11 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:get_it/get_it.dart';
import '../../theme/custom_colors.dart';
import '/l10n/app_localizations.dart';
import '../../logic/auth/auth_bloc.dart';
import '../../logic/settings/settings_cubit.dart';
import '../../logic/transaction/transaction_bloc.dart';
import '../../services/user_service.dart';
import '../../logic/user/user_cubit.dart';
import '../home/widgets/summary_widget.dart'; // Импортируем новый виджет сводки
import '../reports_page.dart'; // Импортируем новую страницу отчетов
import '../settings_page.dart';
@@ -36,17 +35,12 @@ class _HomePageState extends State<HomePage> {
@override
void initState() {
super.initState();
// Получаем ID текущего пользователя из AuthBloc
final authState = context.read<AuthBloc>().state;
if (authState is AuthAuthenticated) {
_currentUserId = authState.user.id;
// Получаем ID текущего пользователя из UserCubit
final userState = context.read<UserCubit>().state;
if (userState is UserLoaded && userState.user != null) {
_currentUserId = userState.user!.id;
// Загружаем транзакции через глобальный TransactionBloc
context.read<TransactionBloc>().add(LoadTransactions(userId: _currentUserId));
} else {
// Если пользователь не аутентифицирован, можно перенаправить на страницу входа
// или использовать ID по умолчанию, если это применимо.
// В данном случае, для простоты, мы предполагаем, что пользователь всегда аутентифицирован.
_currentUserId = 'default_user'; // Заглушка, если что-то пошло не так
}
}
@@ -153,16 +147,21 @@ class _TransactionsPageState extends State<TransactionsPage> {
children: [
// Изменение: SummaryWidget теперь получает все транзакции,
// выбранный месяц и колбэк для его изменения.
Builder(
builder: (context) {
final userService = GetIt.instance<UserService>();
final currencySymbol = userService.currentUser?.defaultCurrency ?? '';
BlocBuilder<SettingsCubit, SettingsState>(
builder: (context, settingsState) {
if (settingsState is SettingsLoaded) {
return SummaryWidget(
transactions: state.transactions,
selectedMonth: _selectedMonth,
onMonthChanged: _onMonthChanged,
currencySymbol: settingsState.defaultCurrency,
);
}
return SummaryWidget(
transactions: state.transactions, // Передаем все транзакции для расчетов
selectedMonth: _selectedMonth, // Передаем текущий выбранный месяц
onMonthChanged: _onMonthChanged, // Передаем колбэк
currencySymbol: currencySymbol,
transactions: state.transactions,
selectedMonth: _selectedMonth,
onMonthChanged: _onMonthChanged,
currencySymbol: 'RUB',
);
},
),
@@ -6,12 +6,13 @@ import 'package:intl/intl.dart';
import '../../../l10n/app_localizations.dart';
import '../../../logic/auth/auth_bloc.dart';
import '../../../logic/settings/settings_cubit.dart'; // Добавлен импорт SettingsCubit
import '../../../logic/transaction/transaction_bloc.dart';
import '../../../data/repositories/interfaces/itag_repository.dart'; // Импортируем интерфейс репозитория тегов
import '../../../data/repositories/interfaces/itag_repository.dart';
import '../../../models/category.dart';
import '../../../models/tag.dart'; // Импортируем модель тега
import '../../../models/tag.dart';
import '../../../models/transaction_record.dart';
import '../../../services/user_service.dart';
import '../../../logic/user/user_cubit.dart';
import '../../../utils/category_utils.dart';
class AddTransactionDialog extends StatefulWidget {
@@ -114,17 +115,33 @@ class _AddTransactionDialogState extends State<AddTransactionDialog> {
return;
}
// Получаем валюту из настроек
final settingsState = context.read<SettingsCubit>().state;
final currency = (settingsState is SettingsLoaded)
? settingsState.defaultCurrency
: 'RUB';
// Получаем ID текущего пользователя из UserCubit
final userState = context.read<UserCubit>().state;
if (userState is! UserLoaded || userState.user == null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
AppLocalizations.of(context)!.transactionErrorText('User not found'),
),
),
);
return;
}
final newTransaction = TransactionRecord(
amount: amount,
vendor: _vendorController.text,
category: _selectedCategory!,
// Комментарий: Используем _selectedDateTime для сохранения точного времени транзакции.
dateTime: _selectedDateTime,
// Комментарий: Добавляем выбранный тег в транзакцию.
tag: _selectedTag,
currency:
GetIt.instance<UserService>().currentUser?.defaultCurrency ?? 'USD',
userId: authState.user.id,
currency: currency,
userId: userState.user!.id,
);
context.read<TransactionBloc>().add(
@@ -137,9 +154,15 @@ class _AddTransactionDialogState extends State<AddTransactionDialog> {
@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();
// Получаем ID текущего пользователя из UserCubit
final userState = context.read<UserCubit>().state;
final userId = userState is UserLoaded && userState.user != null
? userState.user!.id
: '';
final categories = CategoryUtils.getDefaultCategories(userId)
.where((c) => c.isIncome == _isIncome).toList();
return AlertDialog(
title: Text(localizations.addTransactionButton),
@@ -210,9 +233,7 @@ class _AddTransactionDialogState extends State<AddTransactionDialog> {
// Комментарий: Добавляем выпадающий список для выбора тега.
// Он будет загружать теги асинхронно для текущего пользователя.
FutureBuilder<List<Tag>>(
future: GetIt.instance<ITagRepository>().getAllByUser(
(context.read<AuthBloc>().state as AuthAuthenticated).user.id,
),
future: GetIt.instance<ITagRepository>().getAllByUser(userId),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
+82 -49
View File
@@ -1,10 +1,9 @@
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';
import '../../logic/user/user_cubit.dart'; // Импортируем UserCubit вместо UserService
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
@@ -20,56 +19,90 @@ class _LoginPageState extends State<LoginPage> {
@override
Widget build(BuildContext context) {
final localizations = AppLocalizations.of(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: () async { // Комментарий: Делаем onPressed асинхронным
if (_formKey.currentState!.validate()) {
final userService = GetIt.instance<UserService>();
final authBloc = context.read<AuthBloc>(); // Комментарий: Получаем AuthBloc до асинхронной операции
await userService.createAndSetUser( // Комментарий: Используем await
_nameController.text,
_emailController.text,
);
if (!mounted) return; // Комментарий: Проверяем, что виджет все еще в дереве виджетов после await
final user = userService.currentUser;
if (user != null) {
authBloc.add(AuthLoggedIn(user: user)); // Комментарий: Используем полученный AuthBloc
appBar: AppBar(
title: Text(localizations.loginPageTitle),
), // Локализованный заголовок
body: BlocListener<UserCubit, UserState>(
// Слушаем изменения состояния UserCubit
listener: (context, state) {
if (state is UserLoaded && state.user != null) {
// Комментарий: При успешном создании пользователя отправляем событие в AuthBloc
context.read<AuthBloc>().add(AuthLoggedIn(user: state.user!));
} else if (state is UserError) {
// Комментарий: Показываем ошибку пользователю
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(state.message)),
);
}
},
child: 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; // Локализованный текст
}
}
},
child: Text(localizations.loginButtonText), // Локализованный текст
),
],
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),
BlocBuilder<UserCubit, UserState>(
// Строим кнопку в зависимости от состояния UserCubit
builder: (context, state) {
final isLoading = state is UserLoading;
return ElevatedButton(
onPressed: isLoading ? null : () {
// Комментарий: Блокируем кнопку во время загрузки
if (_formKey.currentState!.validate()) {
// Комментарий: Используем UserCubit для создания пользователя
context.read<UserCubit>().createAndSetUser(
_nameController.text,
_emailController.text,
);
}
},
child: isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
) // Показываем индикатор загрузки
: Text(
localizations.loginButtonText,
), // Локализованный текст
);
},
),
],
),
),
),
),
+79 -58
View File
@@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '/l10n/app_localizations.dart';
import '../logic/settings/settings_cubit.dart';
import '../logic/user/user_cubit.dart';
// Комментарий: Мы преобразуем SettingsPage из StatefulWidget в StatelessWidget.
// Это возможно, потому что теперь состояние управляется SettingsCubit,
@@ -14,54 +15,66 @@ class SettingsPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
// Комментарий: Получаем экземпляр локализации для использования в тексте.
// Это позволяет нам отображать текст на языке, выбранном пользователем.
final localizations = AppLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(
// Комментарий: Используем локализованную строку для заголовка страницы.
title: Text(localizations.settingsPageTitle),
),
// Комментарий: Используем BlocBuilder для перестройки UI при изменении состояния SettingsCubit.
// Он будет "слушать" изменения в SettingsCubit и автоматически перестраивать дочерние виджеты
// с новым состоянием (state).
body: BlocBuilder<SettingsCubit, SettingsState>(
builder: (context, state) {
// Комментарий: `state` - это текущее состояние настроек (тема и язык).
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Комментарий: SwitchListTile для переключения темной/светлой темы.
// Это удобный виджет, который объединяет переключатель с текстом.
SwitchListTile(
title: Text(localizations.darkModeSetting),
subtitle: Text(localizations.darkModeDescription),
// Комментарий: Значение переключателя (включен/выключен) берется из `state.isDarkMode`.
value: state.isDarkMode,
// Комментарий: При изменении положения переключателя (onChanged) мы вызываем метод `setDarkMode` у Cubit.
// `context.read<SettingsCubit>()` используется для доступа к Cubit без подписки на его изменения.
// Это хорошо для вызова методов.
onChanged: (value) {
context.read<SettingsCubit>().setDarkMode(value);
},
),
const Divider(),
// Комментарий: ListTile для смены языка.
ListTile(
title: Text(localizations.languageSetting),
subtitle: Text(localizations.languageDescription),
trailing: DropdownButton<String>(
// Комментарий: Текущее значение языка для выпадающего списка берется из `state.languageCode`.
value: state.languageCode,
// Комментарий: При выборе нового языка вызываем метод `setLanguageCode` у Cubit.
onChanged: (String? newValue) {
if (newValue != null) {
context.read<SettingsCubit>().setLanguageCode(newValue);
body: BlocListener<SettingsCubit, SettingsState>(
listener: (context, state) {
// Комментарий: Обрабатываем ошибки и показываем SnackBar пользователю
if (state is SettingsError) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Ошибка настроек: ${state.message}'),
backgroundColor: Theme.of(context).colorScheme.error,
),
);
}
},
child: BlocBuilder<SettingsCubit, SettingsState>(
builder: (context, state) {
if (state is SettingsLoaded) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SwitchListTile(
title: Text(localizations.darkModeSetting),
subtitle: Text(localizations.darkModeDescription),
value: state.isDarkMode,
// Комментарий: При изменении положения переключателя (onChanged) мы вызываем метод `toggleDarkMode` у Cubit.
// `context.read<SettingsCubit>()` используется для доступа к Cubit без подписки на его изменения.
// Это хорошо для вызова методов. Также передаем userId из UserService.
onChanged: (value) {
// Получаем ID текущего пользователя из UserCubit
final userState = context.read<UserCubit>().state;
if (userState is UserLoaded && userState.user != null) {
final userId = userState.user!.id;
context.read<SettingsCubit>().toggleDarkMode(value, userId);
}
},
),
const Divider(),
ListTile(
title: Text(localizations.languageSetting),
subtitle: Text(localizations.languageDescription),
trailing: DropdownButton<String>(
value: state.languageCode,
// Комментарий: При выборе нового языка вызываем метод `changeLanguage` у Cubit.
// Также передаем userId из UserService.
onChanged: (String? newValue) {
if (newValue != null) {
// Получаем ID текущего пользователя из UserCubit
final userState = context.read<UserCubit>().state;
if (userState is UserLoaded && userState.user != null) {
final userId = userState.user!.id;
context.read<SettingsCubit>().changeLanguage(newValue, userId);
}
}
},
// Комментарий: Формируем список доступных языков.
items: <String>['en', 'ru']
.map<DropdownMenuItem<String>>((String value) {
@@ -75,20 +88,24 @@ class SettingsPage extends StatelessWidget {
}).toList(),
),
),
const Divider(),
// Комментарий: ListTile для выбора валюты по умолчанию.
ListTile(
title: Text(localizations.currencySetting),
subtitle: Text(localizations.currencyDescription),
trailing: DropdownButton<String>(
// Комментарий: Текущее значение валюты берется из `state.defaultCurrency`.
value: state.defaultCurrency,
// Комментарий: При выборе новой валюты вызываем метод `setDefaultCurrency` у Cubit.
onChanged: (String? newValue) {
if (newValue != null) {
context.read<SettingsCubit>().setDefaultCurrency(newValue);
}
},
const Divider(),
ListTile(
title: Text(localizations.currencySetting),
subtitle: Text(localizations.currencyDescription),
trailing: DropdownButton<String>(
value: state.defaultCurrency,
// Комментарий: При выборе новой валюты вызываем метод `changeCurrency` у Cubit.
// Также передаем userId из UserService.
onChanged: (String? newValue) {
if (newValue != null) {
// Получаем ID текущего пользователя из UserCubit
final userState = context.read<UserCubit>().state;
if (userState is UserLoaded && userState.user != null) {
final userId = userState.user!.id;
context.read<SettingsCubit>().changeCurrency(newValue, userId);
}
}
},
// Комментарий: Формируем список доступных валют. Можно расширить этот список.
items: <String>['RUB', 'USD', 'EUR']
.map<DropdownMenuItem<String>>((String value) {
@@ -138,10 +155,14 @@ class SettingsPage extends StatelessWidget {
},
),
const Divider(),
],
),
);
},
],
),
);
} else {
return const Center(child: CircularProgressIndicator());
}
},
),
),
);
}
+11 -2
View File
@@ -2,8 +2,9 @@ import 'package:budget_app/l10n/app_localizations.dart';
import 'package:budget_app/logic/tag/tag_cubit.dart';
import 'package:budget_app/models/tag.dart';
import 'package:budget_app/pages/tag/tag_edit_page.dart';
import 'package:budget_app/pages/tag/widgets/tag_list_item.dart';
import 'package:budget_app/pages/tag/widgets/add_tag_button.dart';
import 'package:budget_app/pages/tag/widgets/tag_list_item.dart';
import 'package:budget_app/logic/user/user_cubit.dart'; // Импортируем UserCubit
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:get_it/get_it.dart';
@@ -63,9 +64,17 @@ class _TagListPageState extends State<TagListPage> {
MaterialPageRoute(
builder: (_) => TagEditPage(
onSave: (name) {
// Получаем ID текущего пользователя из UserCubit
final userState = context.read<UserCubit>().state;
if (userState is! UserLoaded || userState.user == null) {
// Обработка случая, когда пользователь не аутентифицирован
// Возможно, показать сообщение об ошибке или перенаправить на страницу входа
return;
}
final currentUserId = userState.user!.id;
final newTag = Tag(
name: name,
userId: 'default_user',
userId: currentUserId,
);
context.read<TagCubit>().addTag(newTag);
_listKey.currentState?.insertItem(0);
-93
View File
@@ -1,93 +0,0 @@
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 '/data/repositories/interfaces/iuser_repository.dart';
import '/models/user.dart';
/// Сервис для управления текущим пользователем приложения
/// Использует ChangeNotifier для уведомления UI об изменениях
class UserService extends ChangeNotifier {
// Ключ для сохранения ID текущего пользователя в настройках
static const String _currentUserKey = 'currentUserId';
late final Box _settingsBox; // Box для хранения настроек
final IUserRepository _userRepository;
final ICategoryRepository _categoryRepository;
final ITagRepository _tagRepository;
final ITransactionRepository _transactionRepository;
User? _currentUser; // Текущий активный пользователь
/// Геттер для получения текущего пользователя
User? get currentUser => _currentUser;
/// Проверяем, есть ли активный пользователь
bool get hasCurrentUser => _currentUser != null;
/// Конструктор сервиса
UserService(
this._userRepository,
this._categoryRepository,
this._tagRepository,
this._transactionRepository,
) {
_settingsBox = Hive.box('settings');
_loadCurrentUser(); // Загружаем сохраненного пользователя при запуске
}
/// Загружаем текущего пользователя из настроек
Future<void> _loadCurrentUser() async {
final userId = _settingsBox.get(_currentUserKey);
if (userId != null) {
_currentUser = await _userRepository.getById(userId);
notifyListeners(); // Уведомляем UI об изменении
}
}
/// Устанавливаем текущего пользователя
Future<void> setCurrentUser(User user) async {
_currentUser = user;
// Сохраняем ID пользователя в настройках для следующего запуска
await _settingsBox.put(_currentUserKey, user.id);
notifyListeners(); // Уведомляем UI об изменении
}
/// Выход из аккаунта (сброс текущего пользователя)
Future<void> logout() async {
_currentUser = null;
await _settingsBox.delete(_currentUserKey);
notifyListeners(); // Уведомляем UI об изменении
}
/// Создание нового пользователя и установка его как текущего
Future<void> createAndSetUser(String name, String email) async {
final user = User(
name: name,
email: email,
languageCode: '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);
}
/// Получение всех пользователей (для выбора)
Future<List<User>> getAllUsers() async {
return await _userRepository.getAll();
}
}
+2 -2
View File
@@ -40,7 +40,7 @@ class AppTheme {
// Комментарий: Добавляем тему для выпадающих меню, чтобы цвет соответствовал фону.
dropdownMenuTheme: DropdownMenuThemeData(
menuStyle: MenuStyle(
backgroundColor: MaterialStateProperty.all(Colors.white),
backgroundColor: WidgetStateProperty.all(Colors.white),
),
),
// Изменяем цвета в соответствии с черно-белой палитрой
@@ -94,7 +94,7 @@ class AppTheme {
// Комментарий: Добавляем тему для выпадающих меню, чтобы цвет соответствовал фону.
dropdownMenuTheme: DropdownMenuThemeData(
menuStyle: MenuStyle(
backgroundColor: MaterialStateProperty.all(Colors.grey[800]),
backgroundColor: WidgetStateProperty.all(Colors.grey[800]),
),
),
// Изменяем цвета в соответствии с черно-белой палитрой