Add cubit settings

This commit is contained in:
2025-06-27 22:28:12 +03:00
parent 5856b59b54
commit 0d64d4ca0e
24 changed files with 459 additions and 175 deletions
+23
View File
@@ -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.
-
-5
View File
@@ -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>(),
+13 -10
View File
@@ -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
View File
@@ -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"
}
+12
View File
@@ -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
+6
View File
@@ -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';
}
+7
View File
@@ -72,4 +72,11 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get defaultUser => 'Пользователь по умолчанию';
@override
String get currencySetting => 'Валюта по умолчанию';
@override
String get currencyDescription =>
'Установить валюту по умолчанию для транзакций';
}
+3 -1
View File
@@ -20,5 +20,7 @@
"languageDescription": "Изменить язык приложения",
"russianLanguage": "Русский",
"englishLanguage": "Английский",
"defaultUser": "Пользователь по умолчанию"
"defaultUser": "Пользователь по умолчанию",
"currencySetting": "Валюта по умолчанию",
"currencyDescription": "Установить валюту по умолчанию для транзакций"
}
+45
View File
@@ -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);
}
}
+32
View File
@@ -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
View File
@@ -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();
}
},
),
);
},
),
);
}
}
+29 -1
View File
@@ -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 при каждом копировании
);
}
}
+5 -2
View File
@@ -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
View File
@@ -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 при каждом копировании
);
}
}
+5 -2
View File
@@ -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
+38 -3
View File
@@ -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 при каждом копировании
);
}
}
+5 -2
View File
@@ -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
View File
@@ -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 при каждом копировании
);
}
}
+5 -2
View File
@@ -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
View File
@@ -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),
); // Локализованный текст
}
},
),
+92 -66
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import '../models/category.dart';
import '../services/user_service.dart';
/// Утилиты для работы с категориями
class CategoryUtils {
+1
View File
@@ -16,6 +16,7 @@ class TagUtils {
),
Tag(
id: 'tag_work',
name: 'Работа',
userId: userId,
),
-2
View File
@@ -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';