Add cubit settings
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
# Project: Budget App
|
||||
|
||||
## General Instructions:
|
||||
|
||||
- Это проект на Flutter используй только его
|
||||
- Комментируй в коде каждое изменение, которое ты делаешь, что бы мне было понятно и я учился на этом.
|
||||
- Комментарии и твои ответы должны быть на русском языке
|
||||
- When generating new Flutter code, please follow the existing coding style.
|
||||
|
||||
## Coding Style:
|
||||
|
||||
- Interface names should be prefixed with `I` (e.g., `IUserService`).
|
||||
- Private class members should be prefixed with an underscore (`_`).
|
||||
- Учитывай, что в проекте используется bloc cubit архитектура
|
||||
|
||||
## Role
|
||||
- You are a Flutter assistant that helps users write more efficient and optimizable Flutter code.
|
||||
- You specialize in identifying patterns that enable Flutter Compiler to automatically apply optimizations, reducing unnecessary re-renders and improving application performance.
|
||||
|
||||
## Follow these guidelines in all code you produce and suggest
|
||||
- Prefer composition and small components: Break down UI into small, reusable components rather than writing large monolithic components. The code you generate should promote clarity and reusability by composing components together.
|
||||
- Design for a good user experience - Provide clear, minimal, and non-blocking UI states. When data is loading, show lightweight placeholders (e.g., skeleton screens) rather than intrusive spinners everywhere. Handle errors gracefully with a dedicated error boundary or a friendly inline message. Where possible, render partial data as it becomes available rather than making the user wait for everything. Suspense allows you to declare the loading states in your component tree in a natural way, preventing “flash” states and improving perceived performance.
|
||||
-
|
||||
@@ -1,12 +1,7 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:hive_ce/hive.dart';
|
||||
import 'package:budget_app/models/category.dart';
|
||||
import 'package:budget_app/models/tag.dart';
|
||||
import 'package:budget_app/models/transaction_record.dart';
|
||||
|
||||
import '../models/user.dart';
|
||||
|
||||
@GenerateAdapters([
|
||||
AdapterSpec<Color>(),
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import 'package:get_it/get_it.dart';
|
||||
|
||||
import 'data/database/hive_service.dart';
|
||||
import 'data/repositories/hive_category_repository.dart';
|
||||
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/itag_repository.dart';
|
||||
import 'data/repositories/interfaces/itransaction_repository.dart';
|
||||
import 'data/repositories/hive_user_repository.dart';
|
||||
import 'data/repositories/interfaces/iuser_repository.dart';
|
||||
import 'logic/auth/auth_bloc.dart';
|
||||
import 'logic/settings/settings_cubit.dart'; // Импортируем SettingsCubit
|
||||
import 'logic/transaction/transaction_bloc.dart';
|
||||
import 'services/settings_service.dart';
|
||||
import 'services/user_service.dart';
|
||||
|
||||
final getIt = GetIt.instance;
|
||||
@@ -20,17 +21,15 @@ Future<void> initDependencies() async {
|
||||
await HiveService.init();
|
||||
|
||||
// Регистрация сервисов
|
||||
getIt.registerSingleton<SettingsService>(SettingsService());
|
||||
getIt.registerSingleton<SettingsCubit>(SettingsCubit()); // Регистрируем Cubit
|
||||
|
||||
// Регистрация репозиториев
|
||||
getIt.registerSingleton<ICategoryRepository>(
|
||||
HiveCategoryRepository(HiveService.categories),
|
||||
);
|
||||
|
||||
getIt.registerSingleton<ITagRepository>(
|
||||
HiveTagRepository(HiveService.tags),
|
||||
);
|
||||
|
||||
|
||||
getIt.registerSingleton<ITagRepository>(HiveTagRepository(HiveService.tags));
|
||||
|
||||
getIt.registerSingleton<ITransactionRepository>(
|
||||
HiveTransactionRepository(HiveService.transactions),
|
||||
);
|
||||
@@ -40,9 +39,13 @@ Future<void> initDependencies() async {
|
||||
);
|
||||
|
||||
// Services
|
||||
getIt.registerSingleton<UserService>(UserService(getIt(), getIt(), getIt(), getIt()));
|
||||
getIt.registerSingleton<UserService>(
|
||||
UserService(getIt(), getIt(), getIt(), getIt()),
|
||||
);
|
||||
|
||||
// Blocs
|
||||
getIt.registerFactory<AuthBloc>(() => AuthBloc(userService: getIt()));
|
||||
getIt.registerFactory<TransactionBloc>(() => TransactionBloc(transactionRepository: getIt()));
|
||||
getIt.registerFactory<TransactionBloc>(
|
||||
() => TransactionBloc(transactionRepository: getIt()),
|
||||
);
|
||||
}
|
||||
|
||||
+3
-1
@@ -20,5 +20,7 @@
|
||||
"languageDescription": "Change application language",
|
||||
"russianLanguage": "Russian",
|
||||
"englishLanguage": "English",
|
||||
"defaultUser": "Default User"
|
||||
"defaultUser": "Default User",
|
||||
"currencySetting": "Default Currency",
|
||||
"currencyDescription": "Set the default currency for transactions"
|
||||
}
|
||||
@@ -223,6 +223,18 @@ abstract class AppLocalizations {
|
||||
/// In en, this message translates to:
|
||||
/// **'Default User'**
|
||||
String get defaultUser;
|
||||
|
||||
/// No description provided for @currencySetting.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Default Currency'**
|
||||
String get currencySetting;
|
||||
|
||||
/// No description provided for @currencyDescription.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Set the default currency for transactions'**
|
||||
String get currencyDescription;
|
||||
}
|
||||
|
||||
class _AppLocalizationsDelegate
|
||||
|
||||
@@ -72,4 +72,10 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get defaultUser => 'Default User';
|
||||
|
||||
@override
|
||||
String get currencySetting => 'Default Currency';
|
||||
|
||||
@override
|
||||
String get currencyDescription => 'Set the default currency for transactions';
|
||||
}
|
||||
|
||||
@@ -72,4 +72,11 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get defaultUser => 'Пользователь по умолчанию';
|
||||
|
||||
@override
|
||||
String get currencySetting => 'Валюта по умолчанию';
|
||||
|
||||
@override
|
||||
String get currencyDescription =>
|
||||
'Установить валюту по умолчанию для транзакций';
|
||||
}
|
||||
|
||||
+3
-1
@@ -20,5 +20,7 @@
|
||||
"languageDescription": "Изменить язык приложения",
|
||||
"russianLanguage": "Русский",
|
||||
"englishLanguage": "Английский",
|
||||
"defaultUser": "Пользователь по умолчанию"
|
||||
"defaultUser": "Пользователь по умолчанию",
|
||||
"currencySetting": "Валюта по умолчанию",
|
||||
"currencyDescription": "Установить валюту по умолчанию для транзакций"
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:hive_ce/hive.dart';
|
||||
|
||||
part 'settings_state.dart';
|
||||
|
||||
class SettingsCubit extends Cubit<SettingsState> {
|
||||
static const String _darkModeKey = 'darkMode';
|
||||
static const String _languageCodeKey = 'languageCode';
|
||||
static const String _defaultCurrencyKey = 'defaultCurrency'; // Комментарий: Новый ключ для хранения валюты по умолчанию
|
||||
late final Box _settingsBox;
|
||||
|
||||
SettingsCubit() : super(const SettingsInitial()) {
|
||||
_settingsBox = Hive.box('settings');
|
||||
_loadSettings();
|
||||
}
|
||||
|
||||
void _loadSettings() {
|
||||
final isDarkMode = _settingsBox.get(_darkModeKey, defaultValue: false);
|
||||
final languageCode = _settingsBox.get(_languageCodeKey, defaultValue: 'ru');
|
||||
// Комментарий: Загружаем валюту по умолчанию из Hive. Если ее нет, используем 'RUB'.
|
||||
final defaultCurrency = _settingsBox.get(_defaultCurrencyKey, defaultValue: 'RUB');
|
||||
emit(state.copyWith(isDarkMode: isDarkMode, languageCode: languageCode, defaultCurrency: defaultCurrency));
|
||||
}
|
||||
|
||||
Future<void> setDarkMode(bool value) async {
|
||||
await _settingsBox.put(_darkModeKey, value);
|
||||
emit(state.copyWith(isDarkMode: value));
|
||||
}
|
||||
|
||||
Future<void> setLanguageCode(String code) async {
|
||||
await _settingsBox.put(_languageCodeKey, code);
|
||||
emit(state.copyWith(languageCode: code));
|
||||
}
|
||||
|
||||
// Комментарий: Новый метод для установки валюты по умолчанию.
|
||||
Future<void> setDefaultCurrency(String currencyCode) async {
|
||||
await _settingsBox.put(_defaultCurrencyKey, currencyCode);
|
||||
emit(state.copyWith(defaultCurrency: currencyCode));
|
||||
}
|
||||
|
||||
void toggleTheme() {
|
||||
setDarkMode(!state.isDarkMode);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
part of 'settings_cubit.dart';
|
||||
|
||||
class SettingsState extends Equatable {
|
||||
final bool isDarkMode;
|
||||
final String languageCode;
|
||||
final String defaultCurrency; // Новое поле для валюты по умолчанию
|
||||
|
||||
const SettingsState({
|
||||
required this.isDarkMode,
|
||||
required this.languageCode,
|
||||
required this.defaultCurrency, // Теперь обязательный параметр
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object> get props => [isDarkMode, languageCode, defaultCurrency];
|
||||
|
||||
SettingsState copyWith({
|
||||
bool? isDarkMode,
|
||||
String? languageCode,
|
||||
String? defaultCurrency, // Добавляем в copyWith
|
||||
}) {
|
||||
return SettingsState(
|
||||
isDarkMode: isDarkMode ?? this.isDarkMode,
|
||||
languageCode: languageCode ?? this.languageCode,
|
||||
defaultCurrency: defaultCurrency ?? this.defaultCurrency, // Обновляем значение
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SettingsInitial extends SettingsState {
|
||||
const SettingsInitial() : super(isDarkMode: false, languageCode: 'ru', defaultCurrency: 'RUB'); // Инициализируем валюту по умолчанию
|
||||
}
|
||||
+36
-49
@@ -1,16 +1,14 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart'; // Добавляем импортсса
|
||||
import 'l10n/app_localizations.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart'; // Добавляем импорт
|
||||
import '/l10n/app_localizations.dart';
|
||||
import 'logic/auth/auth_bloc.dart';
|
||||
import 'pages/login/login_page.dart';
|
||||
import 'package:budget_app/pages/home_page.dart';
|
||||
import 'theme/app_theme.dart';
|
||||
import 'package:hive_ce_flutter/hive_flutter.dart';
|
||||
import 'injection_container.dart' as di;
|
||||
import 'services/settings_service.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'logic/settings/settings_cubit.dart'; // Импортируем SettingsCubit
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
@@ -26,54 +24,43 @@ class MyApp extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _MyAppState extends State<MyApp> {
|
||||
late final SettingsService _settingsService;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_settingsService = GetIt.instance<SettingsService>();
|
||||
_settingsService.addListener(_onSettingsChanged);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_settingsService.removeListener(_onSettingsChanged);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onSettingsChanged() {
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => GetIt.instance<AuthBloc>()..add(AuthStarted()),
|
||||
child: MaterialApp(
|
||||
title: AppLocalizations.of(context)?.appTitle ?? 'Budget App', // Используем локализованный заголовок
|
||||
theme: AppTheme.lightTheme(),
|
||||
darkTheme: AppTheme.darkTheme(),
|
||||
themeMode: _settingsService.isDarkMode ? ThemeMode.dark : ThemeMode.light,
|
||||
// Добавляем локализацию
|
||||
localizationsDelegates: const [
|
||||
AppLocalizations.delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
],
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
locale: Locale(_settingsService.languageCode), // Устанавливаем текущий язык из настроек
|
||||
home: BlocBuilder<AuthBloc, AuthState>(
|
||||
builder: (context, state) {
|
||||
if (state is AuthAuthenticated) {
|
||||
return const HomePage();
|
||||
} else {
|
||||
return const LoginPage();
|
||||
}
|
||||
},
|
||||
),
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider(create: (context) => GetIt.instance<AuthBloc>()..add(AuthStarted())),
|
||||
BlocProvider(create: (context) => GetIt.instance<SettingsCubit>()),
|
||||
],
|
||||
child: BlocBuilder<SettingsCubit, SettingsState>(
|
||||
builder: (context, settingsState) {
|
||||
return MaterialApp(
|
||||
title: AppLocalizations.of(context)?.appTitle ?? 'Budget App', // Используем локализованный заголовок
|
||||
theme: AppTheme.lightTheme(),
|
||||
darkTheme: AppTheme.darkTheme(),
|
||||
themeMode: settingsState.isDarkMode ? ThemeMode.dark : ThemeMode.light,
|
||||
// Добавляем локализацию
|
||||
localizationsDelegates: const [
|
||||
AppLocalizations.delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
],
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
locale: Locale(settingsState.languageCode), // Устанавливаем текущий язык из настроек
|
||||
home: BlocBuilder<AuthBloc, AuthState>(
|
||||
builder: (context, authState) {
|
||||
if (authState is AuthAuthenticated) {
|
||||
return const HomePage();
|
||||
} else {
|
||||
return const LoginPage();
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -36,6 +36,10 @@ class Category {
|
||||
/// Это позволяет разделять категории между разными пользователями
|
||||
final String userId;
|
||||
|
||||
@HiveField(6)
|
||||
/// Дата и время последнего обновления объекта
|
||||
final DateTime updatedAt;
|
||||
|
||||
/// Конструктор с обязательными параметрами
|
||||
Category({
|
||||
String? id,
|
||||
@@ -44,7 +48,9 @@ class Category {
|
||||
required this.icon,
|
||||
required this.isIncome,
|
||||
required this.userId, // Теперь userId обязательный параметр
|
||||
}) : id = id ?? IdGenerator.generateId();
|
||||
DateTime? updatedAt, // Добавлено поле updatedAt, теперь необязательное
|
||||
}) : id = id ?? IdGenerator.generateId(),
|
||||
updatedAt = updatedAt ?? DateTime.now(); // Устанавливаем текущее время по умолчанию
|
||||
|
||||
/// Метод для преобразования объекта в Map (полезно для работы с БД)
|
||||
Map<String, dynamic> toMap() {
|
||||
@@ -55,6 +61,7 @@ class Category {
|
||||
'icon': icon.codePoint,
|
||||
'isIncome': isIncome,
|
||||
'userId': userId, // Добавляем userId в Map
|
||||
'updatedAt': updatedAt.toIso8601String(), // Добавлено updatedAt в Map
|
||||
};
|
||||
}
|
||||
|
||||
@@ -67,6 +74,27 @@ class Category {
|
||||
icon: IconData(map['icon'], fontFamily: 'MaterialIcons'),
|
||||
isIncome: map['isIncome'],
|
||||
userId: map['userId'], // Добавляем userId при создании из Map
|
||||
updatedAt: DateTime.parse(map['updatedAt']), // Добавлено updatedAt при создании из Map
|
||||
);
|
||||
}
|
||||
|
||||
/// Метод для создания копии объекта с возможностью изменения полей
|
||||
Category copyWith({
|
||||
String? id,
|
||||
String? name,
|
||||
Color? color,
|
||||
IconData? icon,
|
||||
bool? isIncome,
|
||||
String? userId,
|
||||
}) {
|
||||
return Category(
|
||||
id: id ?? this.id,
|
||||
name: name ?? this.name,
|
||||
color: color ?? this.color,
|
||||
icon: icon ?? this.icon,
|
||||
isIncome: isIncome ?? this.isIncome,
|
||||
userId: userId ?? this.userId,
|
||||
updatedAt: DateTime.now(), // Обновляем updatedAt при каждом копировании
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,13 +23,14 @@ class CategoryAdapter extends TypeAdapter<Category> {
|
||||
icon: fields[3] as IconData,
|
||||
isIncome: fields[4] as bool,
|
||||
userId: fields[5] as String,
|
||||
updatedAt: fields[6] as DateTime?,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, Category obj) {
|
||||
writer
|
||||
..writeByte(6)
|
||||
..writeByte(7)
|
||||
..writeByte(0)
|
||||
..write(obj.id)
|
||||
..writeByte(1)
|
||||
@@ -41,7 +42,9 @@ class CategoryAdapter extends TypeAdapter<Category> {
|
||||
..writeByte(4)
|
||||
..write(obj.isIncome)
|
||||
..writeByte(5)
|
||||
..write(obj.userId);
|
||||
..write(obj.userId)
|
||||
..writeByte(6)
|
||||
..write(obj.updatedAt);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
+24
-2
@@ -19,6 +19,10 @@ class Tag {
|
||||
@HiveField(2)
|
||||
final String userId;
|
||||
|
||||
@HiveField(3)
|
||||
/// Дата и время последнего обновления объекта
|
||||
final DateTime updatedAt;
|
||||
|
||||
/// Конструктор с обязательными параметрами
|
||||
Tag({
|
||||
String? id,
|
||||
@@ -26,7 +30,9 @@ class Tag {
|
||||
// Комментарий: Добавляем userId в конструктор как обязательный параметр.
|
||||
// Теперь при создании тега необходимо будет указать, какому пользователю он принадлежит.
|
||||
required this.userId,
|
||||
}) : id = id ?? IdGenerator.generateId();
|
||||
DateTime? updatedAt, // Добавлено поле updatedAt, теперь необязательное
|
||||
}) : id = id ?? IdGenerator.generateId(),
|
||||
updatedAt = updatedAt ?? DateTime.now(); // Устанавливаем текущее время по умолчанию
|
||||
|
||||
/// Преобразование объекта в Map
|
||||
Map<String, dynamic> toMap() {
|
||||
@@ -36,6 +42,7 @@ class Tag {
|
||||
// Комментарий: Добавляем userId в Map. Это нужно для сохранения
|
||||
// данных в форматах, которые не работают напрямую с объектами Dart (например, при отправке на сервер).
|
||||
'userId': userId,
|
||||
'updatedAt': updatedAt.toIso8601String(), // Добавлено updatedAt в Map
|
||||
};
|
||||
}
|
||||
|
||||
@@ -45,8 +52,23 @@ class Tag {
|
||||
id: map['id'],
|
||||
name: map['name'],
|
||||
// Комментарий: Извлекаем userId из Map при создании объекта.
|
||||
// Это позволяет восстановить полный объект Tag из данных, например, из базы данных.
|
||||
// Это позволит восстановить полный объект Tag из данных, например, из базы данных.
|
||||
userId: map['userId'],
|
||||
updatedAt: DateTime.parse(map['updatedAt']), // Добавлено updatedAt при создании из Map
|
||||
);
|
||||
}
|
||||
|
||||
/// Метод для создания копии объекта с возможностью изменения полей
|
||||
Tag copyWith({
|
||||
String? id,
|
||||
String? name,
|
||||
String? userId,
|
||||
}) {
|
||||
return Tag(
|
||||
id: id ?? this.id,
|
||||
name: name ?? this.name,
|
||||
userId: userId ?? this.userId,
|
||||
updatedAt: DateTime.now(), // Обновляем updatedAt при каждом копировании
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,19 +20,22 @@ class TagAdapter extends TypeAdapter<Tag> {
|
||||
id: fields[0] as String?,
|
||||
name: fields[1] as String,
|
||||
userId: fields[2] as String,
|
||||
updatedAt: fields[3] as DateTime?,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, Tag obj) {
|
||||
writer
|
||||
..writeByte(3)
|
||||
..writeByte(4)
|
||||
..writeByte(0)
|
||||
..write(obj.id)
|
||||
..writeByte(1)
|
||||
..write(obj.name)
|
||||
..writeByte(2)
|
||||
..write(obj.userId);
|
||||
..write(obj.userId)
|
||||
..writeByte(3)
|
||||
..write(obj.updatedAt);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hive_ce/hive.dart';
|
||||
|
||||
import '../../utils/id_generator.dart';
|
||||
import 'category.dart';
|
||||
import 'tag.dart';
|
||||
@@ -10,7 +10,6 @@ part 'transaction_record.g.dart';
|
||||
/// Модель записи о транзакции - основной элемент учета бюджета
|
||||
/// Содержит все детали финансовой операции
|
||||
class TransactionRecord {
|
||||
|
||||
/// Уникальный идентификатор транзакции
|
||||
@HiveField(0)
|
||||
final String id;
|
||||
@@ -43,6 +42,10 @@ class TransactionRecord {
|
||||
@HiveField(7) // Используем следующий доступный номер поля Hive
|
||||
final String userId;
|
||||
|
||||
@HiveField(8)
|
||||
/// Дата и время последнего обновления объекта
|
||||
final DateTime updatedAt;
|
||||
|
||||
/// Конструктор с обязательными параметрами
|
||||
TransactionRecord({
|
||||
String? id,
|
||||
@@ -53,7 +56,11 @@ class TransactionRecord {
|
||||
required this.vendor,
|
||||
required this.currency,
|
||||
required this.userId, // Добавляем userId в конструктор
|
||||
}) : id = id ?? IdGenerator.generateId();
|
||||
DateTime? updatedAt, // Добавлено поле updatedAt, теперь необязательное
|
||||
}) : id = id ?? IdGenerator.generateId(),
|
||||
updatedAt =
|
||||
updatedAt ??
|
||||
DateTime.now(); // Устанавливаем текущее время по умолчанию
|
||||
|
||||
/// Преобразование объекта в Map
|
||||
Map<String, dynamic> toMap() {
|
||||
@@ -66,6 +73,7 @@ class TransactionRecord {
|
||||
'vendor': vendor,
|
||||
'currency': currency,
|
||||
'userId': userId, // Добавляем userId в Map
|
||||
'updatedAt': updatedAt.toIso8601String(), // Добавлено updatedAt в Map
|
||||
};
|
||||
}
|
||||
|
||||
@@ -80,10 +88,37 @@ class TransactionRecord {
|
||||
vendor: map['vendor'],
|
||||
currency: map['currency'],
|
||||
userId: map['userId'], // Извлекаем userId из Map
|
||||
updatedAt: DateTime.parse(
|
||||
map['updatedAt'],
|
||||
), // Добавлено updatedAt при создании из Map
|
||||
);
|
||||
}
|
||||
|
||||
/// Вспомогательный геттер для определения типа операции
|
||||
/// (доход/расход) на основе категории
|
||||
bool get isIncome => category.isIncome;
|
||||
|
||||
/// Метод для создания копии объекта с возможностью изменения полей
|
||||
TransactionRecord copyWith({
|
||||
String? id,
|
||||
Category? category,
|
||||
Tag? tag,
|
||||
double? amount,
|
||||
DateTime? dateTime,
|
||||
String? vendor,
|
||||
String? currency,
|
||||
String? userId,
|
||||
}) {
|
||||
return TransactionRecord(
|
||||
id: id ?? this.id,
|
||||
category: category ?? this.category,
|
||||
tag: tag ?? this.tag,
|
||||
amount: amount ?? this.amount,
|
||||
dateTime: dateTime ?? this.dateTime,
|
||||
vendor: vendor ?? this.vendor,
|
||||
currency: currency ?? this.currency,
|
||||
userId: userId ?? this.userId,
|
||||
updatedAt: DateTime.now(), // Обновляем updatedAt при каждом копировании
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,13 +25,14 @@ class TransactionRecordAdapter extends TypeAdapter<TransactionRecord> {
|
||||
vendor: fields[5] as String,
|
||||
currency: fields[6] as String,
|
||||
userId: fields[7] as String,
|
||||
updatedAt: fields[8] as DateTime?,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, TransactionRecord obj) {
|
||||
writer
|
||||
..writeByte(8)
|
||||
..writeByte(9)
|
||||
..writeByte(0)
|
||||
..write(obj.id)
|
||||
..writeByte(1)
|
||||
@@ -47,7 +48,9 @@ class TransactionRecordAdapter extends TypeAdapter<TransactionRecord> {
|
||||
..writeByte(6)
|
||||
..write(obj.currency)
|
||||
..writeByte(7)
|
||||
..write(obj.userId);
|
||||
..write(obj.userId)
|
||||
..writeByte(8)
|
||||
..write(obj.updatedAt);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
+25
-1
@@ -24,6 +24,10 @@ class User {
|
||||
@HiveField(3) // Поле 3 в Hive - язык пользователя
|
||||
final String language;
|
||||
|
||||
@HiveField(4)
|
||||
/// Дата и время последнего обновления объекта
|
||||
final DateTime updatedAt;
|
||||
|
||||
/// Конструктор пользователя
|
||||
/// id генерируется автоматически, если не передан
|
||||
User({
|
||||
@@ -31,7 +35,9 @@ class User {
|
||||
required this.name, // Обязательный параметр
|
||||
required this.email, // Обязательный параметр
|
||||
this.language = 'ru', // Язык по умолчанию - русский
|
||||
}) : id = id ?? IdGenerator.generateId(); // Если id не передан, генерируем новый
|
||||
DateTime? updatedAt, // Добавлено поле updatedAt, теперь необязательное
|
||||
}) : id = id ?? IdGenerator.generateId(), // Если id не передан, генерируем новый
|
||||
updatedAt = updatedAt ?? DateTime.now(); // Устанавливаем текущее время по умолчанию
|
||||
|
||||
/// Преобразование объекта в Map для сохранения в JSON или передачи по сети
|
||||
Map<String, dynamic> toMap() {
|
||||
@@ -40,6 +46,7 @@ class User {
|
||||
'name': name,
|
||||
'email': email,
|
||||
'language': language,
|
||||
'updatedAt': updatedAt.toIso8601String(), // Добавлено updatedAt в Map
|
||||
};
|
||||
}
|
||||
|
||||
@@ -50,6 +57,7 @@ class User {
|
||||
name: map['name'],
|
||||
email: map['email'],
|
||||
language: map['language'] ?? 'ru', // Устанавливаем русский по умолчанию, если язык не указан
|
||||
updatedAt: DateTime.parse(map['updatedAt']), // Добавлено updatedAt при создании из Map
|
||||
);
|
||||
}
|
||||
|
||||
@@ -58,4 +66,20 @@ class User {
|
||||
String toString() {
|
||||
return 'User(id: $id, name: $name, email: $email)';
|
||||
}
|
||||
|
||||
/// Метод для создания копии объекта с возможностью изменения полей
|
||||
User copyWith({
|
||||
String? id,
|
||||
String? name,
|
||||
String? email,
|
||||
String? language,
|
||||
}) {
|
||||
return User(
|
||||
id: id ?? this.id,
|
||||
name: name ?? this.name,
|
||||
email: email ?? this.email,
|
||||
language: language ?? this.language,
|
||||
updatedAt: DateTime.now(), // Обновляем updatedAt при каждом копировании
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,13 +21,14 @@ class UserAdapter extends TypeAdapter<User> {
|
||||
name: fields[1] as String,
|
||||
email: fields[2] as String,
|
||||
language: fields[3] == null ? 'ru' : fields[3] as String,
|
||||
updatedAt: fields[4] as DateTime?,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, User obj) {
|
||||
writer
|
||||
..writeByte(4)
|
||||
..writeByte(5)
|
||||
..writeByte(0)
|
||||
..write(obj.id)
|
||||
..writeByte(1)
|
||||
@@ -35,7 +36,9 @@ class UserAdapter extends TypeAdapter<User> {
|
||||
..writeByte(2)
|
||||
..write(obj.email)
|
||||
..writeByte(3)
|
||||
..write(obj.language);
|
||||
..write(obj.language)
|
||||
..writeByte(4)
|
||||
..write(obj.updatedAt);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
+49
-25
@@ -1,12 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
|
||||
import '/l10n/app_localizations.dart';
|
||||
import '../logic/auth/auth_bloc.dart';
|
||||
import '../logic/transaction/transaction_bloc.dart';
|
||||
import '../models/transaction_record.dart';
|
||||
import 'settings_page.dart';
|
||||
import 'reports_page.dart'; // Импортируем новую страницу отчетов
|
||||
import 'settings_page.dart';
|
||||
|
||||
class HomePage extends StatefulWidget {
|
||||
const HomePage({super.key});
|
||||
@@ -49,11 +49,14 @@ class _HomePageState extends State<HomePage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final localizations = AppLocalizations.of(context)!; // Получаем экземпляр локализации
|
||||
final localizations = AppLocalizations.of(
|
||||
context,
|
||||
)!; // Получаем экземпляр локализации
|
||||
|
||||
return BlocProvider(
|
||||
create: (context) {
|
||||
return GetIt.instance<TransactionBloc>()..add(LoadTransactions(userId: _currentUserId));
|
||||
return GetIt.instance<TransactionBloc>()
|
||||
..add(LoadTransactions(userId: _currentUserId));
|
||||
},
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
@@ -67,7 +70,9 @@ class _HomePageState extends State<HomePage> {
|
||||
onPressed: () {
|
||||
// TODO: Добавить логику для добавления новой транзакции
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(localizations.addTransactionButton)), // Локализованный текст
|
||||
SnackBar(
|
||||
content: Text(localizations.addTransactionButton),
|
||||
), // Локализованный текст
|
||||
);
|
||||
},
|
||||
child: const Icon(Icons.add),
|
||||
@@ -102,7 +107,9 @@ class TransactionsPage extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final localizations = AppLocalizations.of(context)!; // Получаем экземпляр локализации
|
||||
final localizations = AppLocalizations.of(
|
||||
context,
|
||||
)!; // Получаем экземпляр локализации
|
||||
|
||||
return BlocBuilder<TransactionBloc, TransactionState>(
|
||||
builder: (context, state) {
|
||||
@@ -110,14 +117,19 @@ class TransactionsPage extends StatelessWidget {
|
||||
return Center(child: CircularProgressIndicator());
|
||||
} else if (state is TransactionLoaded) {
|
||||
if (state.transactions.isEmpty) {
|
||||
return Center(child: Text(localizations.noTransactionsText)); // Локализованный текст
|
||||
return Center(
|
||||
child: Text(localizations.noTransactionsText),
|
||||
); // Локализованный текст
|
||||
}
|
||||
return ListView.builder(
|
||||
itemCount: state.transactions.length,
|
||||
itemBuilder: (context, index) {
|
||||
final transaction = state.transactions[index];
|
||||
return ListTile(
|
||||
leading: Icon(transaction.category.icon, color: transaction.category.color),
|
||||
leading: Icon(
|
||||
transaction.category.icon,
|
||||
color: transaction.category.color,
|
||||
),
|
||||
title: Text(transaction.vendor),
|
||||
subtitle: Text(transaction.category.name),
|
||||
trailing: Text(
|
||||
@@ -130,9 +142,13 @@ class TransactionsPage extends StatelessWidget {
|
||||
},
|
||||
);
|
||||
} else if (state is TransactionError) {
|
||||
return Center(child: Text(localizations.transactionErrorText(state.message))); // Локализованный текст
|
||||
return Center(
|
||||
child: Text(localizations.transactionErrorText(state.message)),
|
||||
); // Локализованный текст
|
||||
} else {
|
||||
return Center(child: Text(localizations.loadingTransactionsText)); // Локализованный текст
|
||||
return Center(
|
||||
child: Text(localizations.loadingTransactionsText),
|
||||
); // Локализованный текст
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -142,25 +158,26 @@ class TransactionsPage extends StatelessWidget {
|
||||
class HomeView extends StatelessWidget {
|
||||
const HomeView({super.key});
|
||||
|
||||
|
||||
/**
|
||||
* Основной метод построения интерфейса виджета.
|
||||
*
|
||||
* Возвращает Scaffold - базовую структуру страницы Material Design,
|
||||
* которая включает:
|
||||
* 1. AppBar (верхнюю панель)
|
||||
* 2. Body (основное содержимое)
|
||||
*/
|
||||
// //
|
||||
// Основной метод построения интерфейса виджета.
|
||||
//
|
||||
// Возвращает Scaffold - базовую структуру страницы Material Design,
|
||||
// которая включает:
|
||||
// 1. AppBar (верхнюю панель)
|
||||
// 2. Body (основное содержимое)
|
||||
// //
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final localizations = AppLocalizations.of(context)!; // Получаем экземпляр локализации
|
||||
final localizations = AppLocalizations.of(
|
||||
context,
|
||||
)!; // Получаем экземпляр локализации
|
||||
|
||||
return Scaffold(
|
||||
// Верхняя панель приложения
|
||||
appBar: AppBar(
|
||||
// Заголовок приложения
|
||||
title: const Text('Budget App'),
|
||||
|
||||
|
||||
// Кнопки в правой части AppBar
|
||||
actions: [
|
||||
// Кнопка настроек
|
||||
@@ -176,7 +193,7 @@ class HomeView extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
|
||||
body: BlocBuilder<TransactionBloc, TransactionState>(
|
||||
builder: (context, state) {
|
||||
if (state is TransactionLoading) {
|
||||
@@ -190,7 +207,10 @@ class HomeView extends StatelessWidget {
|
||||
itemBuilder: (context, index) {
|
||||
final transaction = state.transactions[index];
|
||||
return ListTile(
|
||||
leading: Icon(transaction.category.icon, color: transaction.category.color),
|
||||
leading: Icon(
|
||||
transaction.category.icon,
|
||||
color: transaction.category.color,
|
||||
),
|
||||
title: Text(transaction.vendor),
|
||||
subtitle: Text(transaction.category.name),
|
||||
trailing: Text(
|
||||
@@ -203,9 +223,13 @@ class HomeView extends StatelessWidget {
|
||||
},
|
||||
);
|
||||
} else if (state is TransactionError) {
|
||||
return Center(child: Text(localizations.transactionErrorText(state.message))); // Локализованный текст
|
||||
return Center(
|
||||
child: Text(localizations.transactionErrorText(state.message)),
|
||||
); // Локализованный текст
|
||||
} else {
|
||||
return Center(child: Text(localizations.loadingTransactionsText)); // Локализованный текст
|
||||
return Center(
|
||||
child: Text(localizations.loadingTransactionsText),
|
||||
); // Локализованный текст
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
@@ -1,81 +1,107 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '/l10n/app_localizations.dart';
|
||||
import '../services/settings_service.dart';
|
||||
import '../logic/settings/settings_cubit.dart';
|
||||
|
||||
class SettingsPage extends StatefulWidget {
|
||||
// Комментарий: Мы преобразуем SettingsPage из StatefulWidget в StatelessWidget.
|
||||
// Это возможно, потому что теперь состояние управляется SettingsCubit,
|
||||
// и виджету не нужно хранить собственное состояние, что делает код проще и эффективнее.
|
||||
class SettingsPage extends StatelessWidget {
|
||||
const SettingsPage({super.key});
|
||||
|
||||
@override
|
||||
State<SettingsPage> createState() => _SettingsPageState();
|
||||
}
|
||||
|
||||
class _SettingsPageState extends State<SettingsPage> {
|
||||
late final SettingsService _settingsService;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_settingsService = GetIt.instance<SettingsService>();
|
||||
_settingsService.addListener(_onSettingsChanged);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_settingsService.removeListener(_onSettingsChanged);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onSettingsChanged() {
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final localizations = AppLocalizations.of(context)!; // Получаем экземпляр локализации
|
||||
// Комментарий: Получаем экземпляр локализации для использования в тексте.
|
||||
// Это позволяет нам отображать текст на языке, выбранном пользователем.
|
||||
final localizations = AppLocalizations.of(context)!;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(localizations.settingsPageTitle), // Локализованный заголовок
|
||||
// Комментарий: Используем локализованную строку для заголовка страницы.
|
||||
title: Text(localizations.settingsPageTitle),
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SwitchListTile(
|
||||
title: Text(localizations.darkModeSetting), // Локализованный текст
|
||||
subtitle: Text(localizations.darkModeDescription), // Локализованный текст
|
||||
value: _settingsService.isDarkMode,
|
||||
onChanged: (value) async {
|
||||
await _settingsService.setDarkMode(value);
|
||||
},
|
||||
// Комментарий: Используем 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);
|
||||
}
|
||||
},
|
||||
// Комментарий: Формируем список доступных языков.
|
||||
items: <String>['en', 'ru']
|
||||
.map<DropdownMenuItem<String>>((String value) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: value,
|
||||
// Комментарий: Отображаем локализованное название языка.
|
||||
child: Text(value == 'en'
|
||||
? localizations.englishLanguage
|
||||
: localizations.russianLanguage),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
// Комментарий: 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);
|
||||
}
|
||||
},
|
||||
// Комментарий: Формируем список доступных валют. Можно расширить этот список.
|
||||
items: <String>['RUB', 'USD', 'EUR']
|
||||
.map<DropdownMenuItem<String>>((String value) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: value,
|
||||
child: Text(value),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
],
|
||||
),
|
||||
const Divider(),
|
||||
ListTile(
|
||||
title: Text(localizations.languageSetting), // Локализованный текст
|
||||
subtitle: Text(localizations.languageDescription), // Локализованный текст
|
||||
trailing: DropdownButton<String>(
|
||||
value: _settingsService.languageCode, // Текущий выбранный язык
|
||||
onChanged: (String? newValue) async {
|
||||
if (newValue != null) {
|
||||
await _settingsService.setLanguageCode(newValue);
|
||||
}
|
||||
},
|
||||
items: <String>['en', 'ru'] // Доступные языки
|
||||
.map<DropdownMenuItem<String>>((String value) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: value,
|
||||
child: Text(value == 'en' ? localizations.englishLanguage : localizations.russianLanguage), // Локализованные названия языков
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
// Здесь можно добавить другие настройки
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../models/category.dart';
|
||||
import '../services/user_service.dart';
|
||||
|
||||
/// Утилиты для работы с категориями
|
||||
class CategoryUtils {
|
||||
|
||||
@@ -16,6 +16,7 @@ class TagUtils {
|
||||
),
|
||||
Tag(
|
||||
id: 'tag_work',
|
||||
|
||||
name: 'Работа',
|
||||
userId: userId,
|
||||
),
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import '../models/transaction_record.dart';
|
||||
import '../models/category.dart';
|
||||
import '../models/tag.dart';
|
||||
import 'category_utils.dart';
|
||||
import 'tag_utils.dart';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user