From 9bae137d085db04e8873fd5bdeda7f592a971af7 Mon Sep 17 00:00:00 2001 From: Sanders Date: Mon, 7 Jul 2025 00:29:15 +0300 Subject: [PATCH] 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. --- lib/data/database/hive_service.dart | 63 +++---- .../hive_global_settings_repository.dart | 33 ++++ .../hive_settings_repository.dart | 40 ++++ .../iglobal_settings_repository.dart | 9 + .../interfaces/isettings_repository.dart | 16 ++ lib/hive/hive_registrar.g.dart | 6 + lib/injection_container.dart | 49 +++-- lib/logic/auth/auth_bloc.dart | 16 +- lib/logic/category/category_cubit.dart | 18 +- lib/logic/settings/settings_cubit.dart | 109 ++++++----- lib/logic/settings/settings_state.dart | 39 +++- lib/logic/sms/sms_cubit.dart | 11 +- lib/logic/tag/tag_cubit.dart | 18 +- lib/logic/user/user_cubit.dart | 171 ++++++++++++++++++ lib/logic/user/user_state.dart | 31 ++++ lib/main.dart | 17 +- lib/models/app_settings.dart | 88 +++++++++ lib/models/app_settings.g.dart | 53 ++++++ lib/models/global_settings.dart | 13 ++ lib/models/global_settings.g.dart | 39 ++++ lib/models/user.dart | 65 ++----- lib/models/user.g.dart | 17 +- lib/pages/category/category_list_page.dart | 13 +- lib/pages/home/home_page.dart | 41 ++--- .../home/widgets/add_transaction_dialog.dart | 49 +++-- lib/pages/login/login_page.dart | 131 +++++++++----- lib/pages/settings_page.dart | 137 ++++++++------ lib/pages/tag/tag_list_page.dart | 13 +- lib/services/user_service.dart | 93 ---------- lib/theme/app_theme.dart | 4 +- 30 files changed, 958 insertions(+), 444 deletions(-) create mode 100644 lib/data/repositories/hive_global_settings_repository.dart create mode 100644 lib/data/repositories/hive_settings_repository.dart create mode 100644 lib/data/repositories/interfaces/iglobal_settings_repository.dart create mode 100644 lib/data/repositories/interfaces/isettings_repository.dart create mode 100644 lib/logic/user/user_cubit.dart create mode 100644 lib/logic/user/user_state.dart create mode 100644 lib/models/app_settings.dart create mode 100644 lib/models/app_settings.g.dart create mode 100644 lib/models/global_settings.dart create mode 100644 lib/models/global_settings.g.dart delete mode 100644 lib/services/user_service.dart diff --git a/lib/data/database/hive_service.dart b/lib/data/database/hive_service.dart index 56c30ea..b7e9dda 100644 --- a/lib/data/database/hive_service.dart +++ b/lib/data/database/hive_service.dart @@ -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 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( + _appSettingsBox, + ), // Открываем Box для AppSettings Hive.openBox(_categoryBox), Hive.openBox(_tagBox), Hive.openBox(_transactionBox), Hive.openBox(_userBox), Hive.openBox(_smsMessageBox), + Hive.openBox(_globalSettings), ]); // Проверка и заполнение начальными данными - await _checkAndFillInitialData(); + _checkAndFillInitialData(); } + static Box get appSettings => + Hive.box(_appSettingsBox); // Геттер для настроек static Box get categories => Hive.box(_categoryBox); static Box get tags => Hive.box(_tagBox); static Box get transactions => @@ -52,44 +54,23 @@ class HiveService { static Box get users => Hive.box(_userBox); static Box get smsMessages => Hive.box(_smsMessageBox); + static Box get globalSettings => + Hive.box(_globalSettings); /// Проверяет и заполняет боксы начальными данными при первом запуске - static Future _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(); } } } diff --git a/lib/data/repositories/hive_global_settings_repository.dart b/lib/data/repositories/hive_global_settings_repository.dart new file mode 100644 index 0000000..2a054d5 --- /dev/null +++ b/lib/data/repositories/hive_global_settings_repository.dart @@ -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 _box; + + HiveGlobalSettingsRepository(this._box); + + @override + Future getCurrentUserId() async { + try { + final settings = _box.get(_settingsKey); + return settings?.currentUserId; + } catch (e) { + throw Exception('Ошибка получения ID текущего пользователя: $e'); + } + } + + @override + Future 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'); + } + } +} diff --git a/lib/data/repositories/hive_settings_repository.dart b/lib/data/repositories/hive_settings_repository.dart new file mode 100644 index 0000000..8a34d65 --- /dev/null +++ b/lib/data/repositories/hive_settings_repository.dart @@ -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 _box; + + HiveSettingsRepository(this._box); + + @override + Future getSettings(String userId) async { + try { + // Возвращаем настройки пользователя или создаем новые по умолчанию + final settings = _box.get(userId, defaultValue: AppSettings(userId: userId)); + return settings!; // Гарантируем возврат не-null значения + } catch (e) { + throw Exception('Ошибка получения настроек: $e'); + } + } + + @override + Future saveSettings(AppSettings settings) async { + try { + // Сохраняем настройки с ключом = userId + await _box.put(settings.userId, settings); + } catch (e) { + throw Exception('Ошибка сохранения настроек: $e'); + } + } + + @override + Future deleteSettings(String userId) async { + try { + await _box.delete(userId); + } catch (e) { + throw Exception('Ошибка удаления настроек: $e'); + } + } +} diff --git a/lib/data/repositories/interfaces/iglobal_settings_repository.dart b/lib/data/repositories/interfaces/iglobal_settings_repository.dart new file mode 100644 index 0000000..f88eba6 --- /dev/null +++ b/lib/data/repositories/interfaces/iglobal_settings_repository.dart @@ -0,0 +1,9 @@ + +/// Интерфейс для глобальных настроек, не связанных с конкретным пользователем +abstract class IGlobalSettingsRepository { + /// Получает ID текущего пользователя + Future getCurrentUserId(); + + /// Устанавливает ID текущего пользователя + Future setCurrentUserId(String? userId); +} diff --git a/lib/data/repositories/interfaces/isettings_repository.dart b/lib/data/repositories/interfaces/isettings_repository.dart new file mode 100644 index 0000000..a9cf609 --- /dev/null +++ b/lib/data/repositories/interfaces/isettings_repository.dart @@ -0,0 +1,16 @@ +import 'package:budget_app/models/app_settings.dart'; + +/// Интерфейс репозитория для работы с настройками приложения +abstract class ISettingsRepository { + /// Получает настройки для указанного пользователя + /// [userId] - идентификатор пользователя + Future getSettings(String userId); + + /// Сохраняет настройки + /// [settings] - объект настроек для сохранения + Future saveSettings(AppSettings settings); + + /// Удаляет настройки для указанного пользователя + /// [userId] - идентификатор пользователя + Future deleteSettings(String userId); +} diff --git a/lib/hive/hive_registrar.g.dart b/lib/hive/hive_registrar.g.dart index f62746b..b655859 100644 --- a/lib/hive/hive_registrar.g.dart +++ b/lib/hive/hive_registrar.g.dart @@ -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()); diff --git a/lib/injection_container.dart b/lib/injection_container.dart index 59be842..4b573da 100644 --- a/lib/injection_container.dart +++ b/lib/injection_container.dart @@ -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 initDependencies() async { HiveCategoryRepository(HiveService.categories), ); - getIt.registerSingleton( - HiveSmsMessageRepository(HiveService.smsMessages), - ); getIt.registerSingleton(HiveTagRepository(HiveService.tags)); getIt.registerSingleton( @@ -44,20 +45,31 @@ Future initDependencies() async { HiveUserRepository(HiveService.users), ); - // Services - getIt.registerSingleton( - UserService(getIt(), getIt(), getIt(), getIt()), + // Регистрируем ISettingsRepository с реализацией HiveSettingsRepository + getIt.registerSingleton( + HiveSettingsRepository(HiveService.appSettings), ); + getIt.registerSingleton( + HiveGlobalSettingsRepository(HiveService.globalSettings), + ); + + // Services getIt.registerSingleton(SmsService()); - // Регистрация сервисов - getIt.registerSingleton( - SettingsCubit(getIt()), - ); // Регистрируем Cubit и передаем IUserRepository + // Регистрируем UserCubit с передачей всех необходимых репозиториев + getIt.registerSingleton( + UserCubit( + settingsRepository: getIt(), + userRepository: getIt(), + categoryRepository: getIt(), + tagRepository: getIt(), + transactionRepository: getIt(), + ), + ); // Blocs - getIt.registerFactory(() => AuthBloc(userService: getIt())); + getIt.registerFactory(() => AuthBloc(userCubit: getIt())); getIt.registerFactory( () => TransactionBloc(transactionRepository: getIt()), ); @@ -65,7 +77,18 @@ Future initDependencies() async { getIt.registerFactory(() => SmsCubit(getIt(), getIt(), getIt())); getIt.registerFactory( () => CategoryCubit(getIt()), - ); // Регистрируем CategoryCubit с зависимостью от UserService + ); // Регистрируем CategoryCubit с зависимостью от UserCubit getIt.registerFactory(() => TagCubit(getIt())); + + + getIt.registerSingleton( + HiveSmsMessageRepository(HiveService.smsMessages), + ); + + // Регистрация сервисов + getIt.registerSingleton( + SettingsCubit(getIt()), + ); // Регистрируем Cubit и передаем IUserRepository и UserService + } diff --git a/lib/logic/auth/auth_bloc.dart b/lib/logic/auth/auth_bloc.dart index bc5ecd1..77d3366 100644 --- a/lib/logic/auth/auth_bloc.dart +++ b/lib/logic/auth/auth_bloc.dart @@ -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 { - final UserService _userService; + final UserCubit _userCubit; - AuthBloc({required UserService userService}) : _userService = userService, super(AuthInitial()) { + AuthBloc({required UserCubit userCubit}) : _userCubit = userCubit, super(AuthInitial()) { on(_onAuthStarted); on(_onAuthLoggedIn); on(_onAuthLoggedOut); } void _onAuthStarted(AuthStarted event, Emitter 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 { } void _onAuthLoggedOut(AuthLoggedOut event, Emitter emit) { - _userService.logout(); + // Вызываем logout у UserCubit + _userCubit.logout(); emit(AuthUnauthenticated()); } } diff --git a/lib/logic/category/category_cubit.dart b/lib/logic/category/category_cubit.dart index dc3b9c9..782df2d 100644 --- a/lib/logic/category/category_cubit.dart +++ b/lib/logic/category/category_cubit.dart @@ -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> { - final UserService _userService; // Добавляем зависимость от UserService + final UserCubit _userCubit; // Добавляем зависимость от UserCubit final Box _categoryBox; // Используем Box для типизации - CategoryCubit(this._userService) // Принимаем UserService через конструктор + CategoryCubit(this._userCubit) // Принимаем UserCubit через конструктор : _categoryBox = Hive.box('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> { // Добавляет новую категорию, присваивая ей 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(); diff --git a/lib/logic/settings/settings_cubit.dart b/lib/logic/settings/settings_cubit.dart index ef516eb..797f588 100644 --- a/lib/logic/settings/settings_cubit.dart +++ b/lib/logic/settings/settings_cubit.dart @@ -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 { - 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 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 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 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 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 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 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 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 _saveSettings(SettingsLoaded settings, String userId) async { + final appSettings = AppSettings( + userId: userId, + isDarkMode: settings.isDarkMode, + languageCode: settings.languageCode, + defaultCurrency: settings.defaultCurrency, + ); + await _settingsRepository.saveSettings(appSettings); + } +} \ No newline at end of file diff --git a/lib/logic/settings/settings_state.dart b/lib/logic/settings/settings_state.dart index 16f98ff..fffc314 100644 --- a/lib/logic/settings/settings_state.dart +++ b/lib/logic/settings/settings_state.dart @@ -1,32 +1,51 @@ part of 'settings_cubit.dart'; -class SettingsState extends Equatable { +@immutable +abstract class SettingsState extends Equatable { + const SettingsState(); + + @override + List 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 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 get props => [message]; } diff --git a/lib/logic/sms/sms_cubit.dart b/lib/logic/sms/sms_cubit.dart index d09a7cc..e4f1aea 100644 --- a/lib/logic/sms/sms_cubit.dart +++ b/lib/logic/sms/sms_cubit.dart @@ -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 { 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 { // Комментарий: Запрашиваем разрешение на чтение 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()); diff --git a/lib/logic/tag/tag_cubit.dart b/lib/logic/tag/tag_cubit.dart index 78c4d9f..fe896f6 100644 --- a/lib/logic/tag/tag_cubit.dart +++ b/lib/logic/tag/tag_cubit.dart @@ -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> { - final UserService _userService; + final UserCubit _userCubit; final Box _tagBox; - TagCubit(this._userService) + TagCubit(this._userCubit) : _tagBox = Hive.box('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> { 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(); diff --git a/lib/logic/user/user_cubit.dart b/lib/logic/user/user_cubit.dart new file mode 100644 index 0000000..8c5a7ef --- /dev/null +++ b/lib/logic/user/user_cubit.dart @@ -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 { + 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 _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 _createDefaultUser() async { + final user = User( + name: 'Пользователь по умолчанию', + email: 'default@example.com', + ); + await _userRepository.add(user); + + // Создаем начальные данные для нового пользователя + await _createInitialData(user.id); + + await _setCurrentUser(user); + return user; + } + + /// Создает начальные данные для нового пользователя (категории, теги, транзакции) + Future _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 _setCurrentUser(User user) async { + await _settingsRepository.setCurrentUserId(user.id); + emit(UserLoaded(user)); + } + + /// Устанавливает текущего пользователя + Future 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 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 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> getAllUsers() async { + try { + return await _userRepository.getAll(); + } catch (e, stack) { + _logger.e('Error getting all users', error: e, stackTrace: stack); + return []; + } + } +} + diff --git a/lib/logic/user/user_state.dart b/lib/logic/user/user_state.dart new file mode 100644 index 0000000..bf45d51 --- /dev/null +++ b/lib/logic/user/user_state.dart @@ -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); +} diff --git a/lib/main.dart b/lib/main.dart index fd3581b..a2dac33 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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 { Widget build(BuildContext context) { return MultiBlocProvider( providers: [ + BlocProvider(create: (context) => GetIt.instance()), // Добавляем UserCubit для управления пользователями BlocProvider(create: (context) => GetIt.instance()..add(AuthStarted())), BlocProvider(create: (context) => GetIt.instance()), BlocProvider(create: (context) => GetIt.instance()), @@ -37,12 +39,19 @@ class _MyAppState extends State { ], child: BlocBuilder( 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 { GlobalCupertinoLocalizations.delegate, ], supportedLocales: AppLocalizations.supportedLocales, - locale: Locale(settingsState.languageCode), // Устанавливаем текущий язык из настроек + locale: Locale(languageCode), home: BlocBuilder( builder: (context, authState) { if (authState is AuthAuthenticated) { diff --git a/lib/models/app_settings.dart b/lib/models/app_settings.dart new file mode 100644 index 0000000..9f5a69b --- /dev/null +++ b/lib/models/app_settings.dart @@ -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 toMap() { + return { + 'userId': userId, + 'languageCode': languageCode, + 'isDarkMode': isDarkMode, + 'defaultCurrency': defaultCurrency, + 'updatedAt': updatedAt.toIso8601String(), + }; + } + + /// Создание из Map + factory AppSettings.fromMap(Map 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 get props => [ + userId, + languageCode, + isDarkMode, + defaultCurrency, + updatedAt, + ]; + + @override + String toString() { + return 'AppSettings(userId: $userId, languageCode: $languageCode, ' + 'isDarkMode: $isDarkMode, defaultCurrency: $defaultCurrency)'; + } +} diff --git a/lib/models/app_settings.g.dart b/lib/models/app_settings.g.dart new file mode 100644 index 0000000..7069a2c --- /dev/null +++ b/lib/models/app_settings.g.dart @@ -0,0 +1,53 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'app_settings.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class AppSettingsAdapter extends TypeAdapter { + @override + final typeId = 1005; + + @override + AppSettings read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + 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; +} diff --git a/lib/models/global_settings.dart b/lib/models/global_settings.dart new file mode 100644 index 0000000..b97601a --- /dev/null +++ b/lib/models/global_settings.dart @@ -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, + }); +} diff --git a/lib/models/global_settings.g.dart b/lib/models/global_settings.g.dart new file mode 100644 index 0000000..99b067d --- /dev/null +++ b/lib/models/global_settings.g.dart @@ -0,0 +1,39 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'global_settings.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class GlobalSettingsAdapter extends TypeAdapter { + @override + final typeId = 1006; + + @override + GlobalSettings read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + 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; +} diff --git a/lib/models/user.dart b/lib/models/user.dart index 7556f29..66024fb 100644 --- a/lib/models/user.dart +++ b/lib/models/user.dart @@ -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 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 get props => [id]; } diff --git a/lib/models/user.g.dart b/lib/models/user.g.dart index 0e7ed7b..9ac4782 100644 --- a/lib/models/user.g.dart +++ b/lib/models/user.g.dart @@ -20,18 +20,15 @@ class UserAdapter extends TypeAdapter { 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 { ..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); } diff --git a/lib/pages/category/category_list_page.dart b/lib/pages/category/category_list_page.dart index e708e68..510ca7c 100644 --- a/lib/pages/category/category_list_page.dart +++ b/lib/pages/category/category_list_page.dart @@ -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 { MaterialPageRoute( builder: (_) => CategoryEditPage( onSave: (name, color, icon, isIncome) { + // Получаем ID текущего пользователя из UserCubit + final userState = context.read().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().addCategory(newCategory); _listKey.currentState?.insertItem(0); diff --git a/lib/pages/home/home_page.dart b/lib/pages/home/home_page.dart index 7629910..e67a27b 100644 --- a/lib/pages/home/home_page.dart +++ b/lib/pages/home/home_page.dart @@ -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 { @override void initState() { super.initState(); - // Получаем ID текущего пользователя из AuthBloc - final authState = context.read().state; - if (authState is AuthAuthenticated) { - _currentUserId = authState.user.id; + // Получаем ID текущего пользователя из UserCubit + final userState = context.read().state; + if (userState is UserLoaded && userState.user != null) { + _currentUserId = userState.user!.id; // Загружаем транзакции через глобальный TransactionBloc context.read().add(LoadTransactions(userId: _currentUserId)); - } else { - // Если пользователь не аутентифицирован, можно перенаправить на страницу входа - // или использовать ID по умолчанию, если это применимо. - // В данном случае, для простоты, мы предполагаем, что пользователь всегда аутентифицирован. - _currentUserId = 'default_user'; // Заглушка, если что-то пошло не так } } @@ -153,16 +147,21 @@ class _TransactionsPageState extends State { children: [ // Изменение: SummaryWidget теперь получает все транзакции, // выбранный месяц и колбэк для его изменения. - Builder( - builder: (context) { - final userService = GetIt.instance(); - final currencySymbol = userService.currentUser?.defaultCurrency ?? '₽'; - + BlocBuilder( + 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', ); }, ), diff --git a/lib/pages/home/widgets/add_transaction_dialog.dart b/lib/pages/home/widgets/add_transaction_dialog.dart index fbe8d48..c8cf9d9 100644 --- a/lib/pages/home/widgets/add_transaction_dialog.dart +++ b/lib/pages/home/widgets/add_transaction_dialog.dart @@ -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 { return; } + // Получаем валюту из настроек + final settingsState = context.read().state; + final currency = (settingsState is SettingsLoaded) + ? settingsState.defaultCurrency + : 'RUB'; + + // Получаем ID текущего пользователя из UserCubit + final userState = context.read().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().currentUser?.defaultCurrency ?? 'USD', - userId: authState.user.id, + currency: currency, + userId: userState.user!.id, ); context.read().add( @@ -137,9 +154,15 @@ class _AddTransactionDialogState extends State { @override Widget build(BuildContext context) { final localizations = AppLocalizations.of(context)!; - final categories = CategoryUtils.getDefaultCategories( - (context.read().state as AuthAuthenticated).user.id, - ).where((c) => c.isIncome == _isIncome).toList(); + + // Получаем ID текущего пользователя из UserCubit + final userState = context.read().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 { // Комментарий: Добавляем выпадающий список для выбора тега. // Он будет загружать теги асинхронно для текущего пользователя. FutureBuilder>( - future: GetIt.instance().getAllByUser( - (context.read().state as AuthAuthenticated).user.id, - ), + future: GetIt.instance().getAllByUser(userId), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); diff --git a/lib/pages/login/login_page.dart b/lib/pages/login/login_page.dart index 7dcb879..05a2b42 100644 --- a/lib/pages/login/login_page.dart +++ b/lib/pages/login/login_page.dart @@ -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 { @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(); - final authBloc = context.read(); // Комментарий: Получаем 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 + listener: (context, state) { + if (state is UserLoaded && state.user != null) { + // Комментарий: При успешном создании пользователя отправляем событие в AuthBloc + context.read().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 + builder: (context, state) { + final isLoading = state is UserLoading; + + return ElevatedButton( + onPressed: isLoading ? null : () { + // Комментарий: Блокируем кнопку во время загрузки + if (_formKey.currentState!.validate()) { + // Комментарий: Используем UserCubit для создания пользователя + context.read().createAndSetUser( + _nameController.text, + _emailController.text, + ); + } + }, + child: isLoading + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) // Показываем индикатор загрузки + : Text( + localizations.loginButtonText, + ), // Локализованный текст + ); + }, + ), + ], + ), ), ), ), diff --git a/lib/pages/settings_page.dart b/lib/pages/settings_page.dart index 0504ba1..d76e625 100644 --- a/lib/pages/settings_page.dart +++ b/lib/pages/settings_page.dart @@ -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( - 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()` используется для доступа к Cubit без подписки на его изменения. - // Это хорошо для вызова методов. - onChanged: (value) { - context.read().setDarkMode(value); - }, - ), - const Divider(), - // Комментарий: ListTile для смены языка. - ListTile( - title: Text(localizations.languageSetting), - subtitle: Text(localizations.languageDescription), - trailing: DropdownButton( - // Комментарий: Текущее значение языка для выпадающего списка берется из `state.languageCode`. - value: state.languageCode, - // Комментарий: При выборе нового языка вызываем метод `setLanguageCode` у Cubit. - onChanged: (String? newValue) { - if (newValue != null) { - context.read().setLanguageCode(newValue); + body: BlocListener( + 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( + 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()` используется для доступа к Cubit без подписки на его изменения. + // Это хорошо для вызова методов. Также передаем userId из UserService. + onChanged: (value) { + // Получаем ID текущего пользователя из UserCubit + final userState = context.read().state; + if (userState is UserLoaded && userState.user != null) { + final userId = userState.user!.id; + context.read().toggleDarkMode(value, userId); } }, + ), + const Divider(), + ListTile( + title: Text(localizations.languageSetting), + subtitle: Text(localizations.languageDescription), + trailing: DropdownButton( + value: state.languageCode, + // Комментарий: При выборе нового языка вызываем метод `changeLanguage` у Cubit. + // Также передаем userId из UserService. + onChanged: (String? newValue) { + if (newValue != null) { + // Получаем ID текущего пользователя из UserCubit + final userState = context.read().state; + if (userState is UserLoaded && userState.user != null) { + final userId = userState.user!.id; + context.read().changeLanguage(newValue, userId); + } + } + }, // Комментарий: Формируем список доступных языков. items: ['en', 'ru'] .map>((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( - // Комментарий: Текущее значение валюты берется из `state.defaultCurrency`. - value: state.defaultCurrency, - // Комментарий: При выборе новой валюты вызываем метод `setDefaultCurrency` у Cubit. - onChanged: (String? newValue) { - if (newValue != null) { - context.read().setDefaultCurrency(newValue); - } - }, + const Divider(), + ListTile( + title: Text(localizations.currencySetting), + subtitle: Text(localizations.currencyDescription), + trailing: DropdownButton( + value: state.defaultCurrency, + // Комментарий: При выборе новой валюты вызываем метод `changeCurrency` у Cubit. + // Также передаем userId из UserService. + onChanged: (String? newValue) { + if (newValue != null) { + // Получаем ID текущего пользователя из UserCubit + final userState = context.read().state; + if (userState is UserLoaded && userState.user != null) { + final userId = userState.user!.id; + context.read().changeCurrency(newValue, userId); + } + } + }, // Комментарий: Формируем список доступных валют. Можно расширить этот список. items: ['RUB', 'USD', 'EUR'] .map>((String value) { @@ -138,10 +155,14 @@ class SettingsPage extends StatelessWidget { }, ), const Divider(), - ], - ), - ); - }, + ], + ), + ); + } else { + return const Center(child: CircularProgressIndicator()); + } + }, + ), ), ); } diff --git a/lib/pages/tag/tag_list_page.dart b/lib/pages/tag/tag_list_page.dart index 0296050..532c690 100644 --- a/lib/pages/tag/tag_list_page.dart +++ b/lib/pages/tag/tag_list_page.dart @@ -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 { MaterialPageRoute( builder: (_) => TagEditPage( onSave: (name) { + // Получаем ID текущего пользователя из UserCubit + final userState = context.read().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().addTag(newTag); _listKey.currentState?.insertItem(0); diff --git a/lib/services/user_service.dart b/lib/services/user_service.dart deleted file mode 100644 index d06f725..0000000 --- a/lib/services/user_service.dart +++ /dev/null @@ -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 _loadCurrentUser() async { - final userId = _settingsBox.get(_currentUserKey); - if (userId != null) { - _currentUser = await _userRepository.getById(userId); - notifyListeners(); // Уведомляем UI об изменении - } - } - - /// Устанавливаем текущего пользователя - Future setCurrentUser(User user) async { - _currentUser = user; - // Сохраняем ID пользователя в настройках для следующего запуска - await _settingsBox.put(_currentUserKey, user.id); - notifyListeners(); // Уведомляем UI об изменении - } - - /// Выход из аккаунта (сброс текущего пользователя) - Future logout() async { - _currentUser = null; - await _settingsBox.delete(_currentUserKey); - notifyListeners(); // Уведомляем UI об изменении - } - - /// Создание нового пользователя и установка его как текущего - Future 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> getAllUsers() async { - return await _userRepository.getAll(); - } -} diff --git a/lib/theme/app_theme.dart b/lib/theme/app_theme.dart index e60c95b..6a547bc 100644 --- a/lib/theme/app_theme.dart +++ b/lib/theme/app_theme.dart @@ -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]), ), ), // Изменяем цвета в соответствии с черно-белой палитрой