dev_bloc #1
@@ -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);
|
||||
}
|
||||
@@ -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());
|
||||
|
||||
@@ -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
|
||||
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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)';
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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,
|
||||
), // Локализованный текст
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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]),
|
||||
),
|
||||
),
|
||||
// Изменяем цвета в соответствии с черно-белой палитрой
|
||||
|
||||
Reference in New Issue
Block a user