Merge pull request 'dev_bloc' (#1) from dev_bloc into master
Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
# Project: Budget App
|
||||
|
||||
## General Instructions:
|
||||
|
||||
- Это проект на Flutter используй только его
|
||||
- Комментируй в коде каждое изменение, которое ты делаешь, что бы мне было понятно и я учился на этом.
|
||||
- Комментарии и твои ответы должны быть на русском языке
|
||||
- When generating new Flutter code, please follow the existing coding style.
|
||||
- Цветовая палитра черно-белая
|
||||
- Все настройки цветов выноси в тему
|
||||
- Все элементы должны разбиваться на мелкие и иметь логичную структуру по попкам
|
||||
- Весь текст должен иметь локализацию чрезе flutter_localizations. Смотри папку l10n
|
||||
- Разработка ведется под windows, ты можешь исопльзовать его консольные команды
|
||||
|
||||
## 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.
|
||||
-
|
||||
Vendored
+31
@@ -0,0 +1,31 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Flutter",
|
||||
"type": "dart",
|
||||
"request": "launch",
|
||||
"program": "lib/main.dart"
|
||||
},
|
||||
{
|
||||
"name": "budget_app",
|
||||
"request": "launch",
|
||||
"type": "dart"
|
||||
},
|
||||
{
|
||||
"name": "budget_app (profile mode)",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"flutterMode": "profile"
|
||||
},
|
||||
{
|
||||
"name": "budget_app (release mode)",
|
||||
"request": "launch",
|
||||
"type": "dart",
|
||||
"flutterMode": "release"
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"dart.flutterSdkPath": "C:\\Sanders\\Flutter\\flutter_windows_3.32.2-stable\\flutter"
|
||||
}
|
||||
@@ -24,7 +24,7 @@ android {
|
||||
applicationId = "ru.sanderrs.budget_app"
|
||||
// You can update the following values to match your application needs.
|
||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||
minSdk = flutter.minSdkVersion
|
||||
minSdk = 23
|
||||
targetSdk = flutter.targetSdkVersion
|
||||
versionCode = flutter.versionCode
|
||||
versionName = flutter.versionName
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<!-- Разрешения для чтения SMS -->
|
||||
<uses-feature
|
||||
android:name="android.hardware.telephony"
|
||||
android:required="false" />
|
||||
|
||||
<uses-permission android:name="android.permission.READ_SMS"/>
|
||||
<uses-permission android:name="android.permission.SEND_SMS"/>
|
||||
<uses-permission android:name="android.permission.RECEIVE_SMS"/>
|
||||
|
||||
<application
|
||||
android:label="budget_app"
|
||||
android:name="${applicationName}"
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,3 @@
|
||||
description: This file stores settings for Dart & Flutter DevTools.
|
||||
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
|
||||
extensions:
|
||||
@@ -0,0 +1,3 @@
|
||||
arb-dir: lib/l10n
|
||||
template-arb-file: app_en.arb
|
||||
output-localization-file: app_localizations.dart
|
||||
@@ -1,87 +1,70 @@
|
||||
import 'package:budget_app/hive/hive_registrar.g.dart';
|
||||
import 'package:hive_ce_flutter/hive_flutter.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
import '/models/category.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();
|
||||
import '../../models/app_settings.dart';
|
||||
import '../../models/category.dart';
|
||||
import '../../models/global_settings.dart';
|
||||
import '../../models/sms_handler_settings.dart';
|
||||
import '../../models/sms_message.dart';
|
||||
import '../../models/tag.dart';
|
||||
import '../../models/transaction_record.dart';
|
||||
import '../../models/user.dart';
|
||||
import 'package:budget_app/hive/hive_registrar.g.dart';
|
||||
|
||||
/// Сервис для управления Hive Box.
|
||||
/// Отвечает за инициализацию, открытие и закрытие глобальных и пользовательских хранилищ.
|
||||
class HiveService {
|
||||
static const String _settingsBox = 'settings';
|
||||
static const String _categoryBox = 'categories';
|
||||
static const String _tagBox = 'tags';
|
||||
static const String _transactionBox = 'transactions';
|
||||
static const String _userBox = 'users';
|
||||
// --- Глобальные Box ---
|
||||
// Эти хранилища не зависят от пользователя и инициализируются один раз.
|
||||
static late final Box<User> users;
|
||||
static late final Box<GlobalSettings> globalSettings;
|
||||
|
||||
// ID системного пользователя по умолчанию
|
||||
static const String _defaultUserId = 'default_user';
|
||||
|
||||
static Future<void> init() async {
|
||||
_logger.i('Initializing Hive database');
|
||||
/// Инициализация глобальных Hive Box.
|
||||
/// Должен вызываться при старте приложения.
|
||||
static Future<void> initGlobalBoxes() async {
|
||||
await Hive.initFlutter();
|
||||
|
||||
//zfinal path = Directory.current.path;
|
||||
// Регистрация адаптеров Hive...
|
||||
Hive.registerAdapters();
|
||||
|
||||
// Открытие всех Box'ов
|
||||
await Future.wait([
|
||||
Hive.openBox(_settingsBox),
|
||||
Hive.openBox<Category>(_categoryBox),
|
||||
Hive.openBox<Tag>(_tagBox),
|
||||
Hive.openBox<TransactionRecord>(_transactionBox),
|
||||
Hive.openBox<User>(_userBox),
|
||||
]);
|
||||
|
||||
// Проверка и заполнение начальными данными
|
||||
await _checkAndFillInitialData();
|
||||
// Открытие глобальных хранилищ
|
||||
users = await Hive.openBox<User>('users');
|
||||
globalSettings = await Hive.openBox<GlobalSettings>('global_settings');
|
||||
}
|
||||
|
||||
static Box<Category> get categories => Hive.box<Category>(_categoryBox);
|
||||
static Box<Tag> get tags => Hive.box<Tag>(_tagBox);
|
||||
static Box<TransactionRecord> get transactions =>
|
||||
Hive.box<TransactionRecord>(_transactionBox);
|
||||
static Box<User> get users => Hive.box<User>(_userBox);
|
||||
// --- Пользовательские Box ---
|
||||
// Эти хранилища привязаны к конкретному пользователю.
|
||||
// Они открываются после входа пользователя в систему.
|
||||
static late Box<Category> categories;
|
||||
static late Box<Tag> tags;
|
||||
static late Box<TransactionRecord> transactions;
|
||||
static late Box<AppSettings> appSettings;
|
||||
static late Box<SmsHandlerSettings> smsHandlerSettings;
|
||||
static late Box<SmsMessage> smsMessages;
|
||||
|
||||
/// Проверяет и заполняет боксы начальными данными при первом запуске
|
||||
static Future<void> _checkAndFillInitialData() async {
|
||||
// Создаем пользователя по умолчанию, если его нет
|
||||
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();
|
||||
}
|
||||
/// Инициализация Hive Box для конкретного пользователя.
|
||||
/// [userId] - Уникальный идентификатор пользователя.
|
||||
static Future<void> initUserBoxes(String userId) async {
|
||||
// Открываем Box'ы с именами, включающими userId для изоляции данных.
|
||||
// Например: 'categories_user123'
|
||||
categories = await Hive.openBox<Category>('categories_\$userId');
|
||||
tags = await Hive.openBox<Tag>('tags_\$userId');
|
||||
transactions = await Hive.openBox<TransactionRecord>(
|
||||
'transactions_\$userId',
|
||||
);
|
||||
appSettings = await Hive.openBox<AppSettings>('app_settings_\$userId');
|
||||
smsHandlerSettings = await Hive.openBox<SmsHandlerSettings>(
|
||||
'sms_handler_settings_\$userId',
|
||||
);
|
||||
smsMessages = await Hive.openBox<SmsMessage>('sms_messages_\$userId');
|
||||
}
|
||||
|
||||
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));
|
||||
await transactionBox.flush();
|
||||
}
|
||||
/// Закрытие пользовательских Hive Box.
|
||||
/// Должен вызываться при выходе пользователя из системы.
|
||||
static Future<void> closeUserBoxes() async {
|
||||
await categories.close();
|
||||
await tags.close();
|
||||
await transactions.close();
|
||||
await appSettings.close();
|
||||
await smsHandlerSettings.close();
|
||||
await smsMessages.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'dart:async'; // Добавлено для Future
|
||||
import 'package:hive_ce/hive.dart';
|
||||
import '/data/repositories/interfaces/icategory_repository.dart';
|
||||
import '/models/category.dart';
|
||||
@@ -9,60 +10,76 @@ class HiveCategoryRepository implements ICategoryRepository {
|
||||
|
||||
@override
|
||||
Future<List<Category>> getAll() async {
|
||||
return _box.values.toList();
|
||||
try {
|
||||
return _box.values.toList();
|
||||
} catch (e) {
|
||||
throw Exception('Ошибка получения категорий: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Category?> getById(String id) async {
|
||||
return _box.get(id);
|
||||
try {
|
||||
return _box.get(id);
|
||||
} catch (e) {
|
||||
throw Exception('Ошибка получения категории: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> add(Category category) async {
|
||||
await _box.put(category.id, category);
|
||||
try {
|
||||
await _box.put(category.id, category);
|
||||
} catch (e) {
|
||||
throw Exception('Ошибка добавления категории: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> update(Category category) async {
|
||||
await add(category);
|
||||
try {
|
||||
await add(category);
|
||||
} catch (e) {
|
||||
throw Exception('Ошибка обновления категории: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> delete(String id) async {
|
||||
await _box.delete(id);
|
||||
try {
|
||||
await _box.delete(id);
|
||||
} catch (e) {
|
||||
throw Exception('Ошибка удаления категории: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Category>> getIncomeCategories() async {
|
||||
return _box.values.where((c) => c.isIncome).toList();
|
||||
try {
|
||||
return _box.values.where((c) => c.isIncome).toList();
|
||||
} catch (e) {
|
||||
throw Exception('Ошибка получения категорий доходов: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Category>> getExpenseCategories() async {
|
||||
return _box.values.where((c) => !c.isIncome).toList();
|
||||
}
|
||||
|
||||
// Новые методы для работы с пользователями
|
||||
|
||||
@override
|
||||
Future<List<Category>> getAllByUser(String userId) async {
|
||||
// Фильтруем все категории по userId
|
||||
return _box.values.where((c) => c.userId == userId).toList();
|
||||
try {
|
||||
return _box.values.where((c) => !c.isIncome).toList();
|
||||
} catch (e) {
|
||||
throw Exception('Ошибка получения категорий расходов: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Category>> getIncomeCategoriesByUser(String userId) async {
|
||||
// Получаем только категории доходов конкретного пользователя
|
||||
return _box.values
|
||||
.where((c) => c.userId == userId && c.isIncome)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Category>> getExpenseCategoriesByUser(String userId) async {
|
||||
// Получаем только категории расходов конкретного пользователя
|
||||
return _box.values
|
||||
.where((c) => c.userId == userId && !c.isIncome)
|
||||
.toList();
|
||||
Future<void> addAll(List<Category> categories) async {
|
||||
try {
|
||||
final Map<String, Category> categoryMap = {
|
||||
for (var cat in categories) cat.id: cat
|
||||
};
|
||||
await _box.putAll(categoryMap);
|
||||
} catch (e) {
|
||||
throw Exception('Ошибка пакетного добавления категорий: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,52 @@
|
||||
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;
|
||||
|
||||
// Фиксированный ключ для хранения настроек приложения
|
||||
static const String _settingsKey = 'user_settings';
|
||||
|
||||
HiveSettingsRepository(this._box);
|
||||
|
||||
@override
|
||||
Future<AppSettings> getSettings() async {
|
||||
try {
|
||||
// Получаем настройки по ключу вместо индекса
|
||||
final settings = _box.get(_settingsKey);
|
||||
|
||||
// Если настройки не существуют, создаем новые с значениями по умолчанию
|
||||
if (settings == null) {
|
||||
final defaultSettings = AppSettings();
|
||||
await _box.put(_settingsKey, defaultSettings);
|
||||
return defaultSettings;
|
||||
}
|
||||
|
||||
return settings;
|
||||
} catch (e) {
|
||||
throw Exception('Ошибка получения настроек: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> saveSettings(AppSettings settings) async {
|
||||
try {
|
||||
// Сохраняем настройки с фиксированным ключом
|
||||
await _box.put(_settingsKey, settings);
|
||||
} catch (e) {
|
||||
throw Exception('Ошибка сохранения настроек: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteSettings() async {
|
||||
try {
|
||||
// Удаляем настройки по фиксированному ключу
|
||||
await _box.delete(_settingsKey);
|
||||
} catch (e) {
|
||||
throw Exception('Ошибка удаления настроек: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import 'package:hive_ce/hive.dart';
|
||||
|
||||
import '../../models/sms_handler_settings.dart';
|
||||
import 'interfaces/isms_handler_repository.dart';
|
||||
|
||||
/// Реализация репозитория для настроек обработки СМС с использованием Hive.
|
||||
class HiveSmsHandlerRepository implements ISmsHandlerRepository {
|
||||
final Box<SmsHandlerSettings> _smsHandlerBox;
|
||||
|
||||
HiveSmsHandlerRepository(this._smsHandlerBox);
|
||||
|
||||
@override
|
||||
Future<SmsHandlerSettings?> getSmsHandlerSettings() async {
|
||||
// В Hive мы будем использовать ID пользователя как ключ для его настроек.
|
||||
return _smsHandlerBox.getAt(1);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> saveSmsHandlerSettings(SmsHandlerSettings settings) async {
|
||||
// Сохраняем объект настроек по ключу, равному ID пользователя.
|
||||
await _smsHandlerBox.put(settings.id, settings);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> saveRuleForSender(String sender, SmsProcessingRule rule) async {
|
||||
// 1. Получаем текущие настройки.
|
||||
SmsHandlerSettings? settings = await getSmsHandlerSettings();
|
||||
|
||||
if (settings == null) {
|
||||
// 2. Если настроек нет, создаем новый объект.
|
||||
settings = SmsHandlerSettings(
|
||||
rulesBySender: {sender: rule}, // Создаем карту с первым правилом
|
||||
);
|
||||
} else {
|
||||
// 3. Если настройки есть, обновляем или добавляем правило.
|
||||
settings.rulesBySender[sender] = rule;
|
||||
}
|
||||
|
||||
// 4. Сохраняем обновленный объект настроек.
|
||||
await saveSmsHandlerSettings(settings);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteRuleForSender(String sender) async {
|
||||
// 1. Получаем текущие настройки.
|
||||
final settings = await getSmsHandlerSettings();
|
||||
|
||||
if (settings != null) {
|
||||
// 2. Если настройки существуют, удаляем правило для отправителя.
|
||||
settings.rulesBySender.remove(sender);
|
||||
// 3. Сохраняем измененный объект.
|
||||
await saveSmsHandlerSettings(settings);
|
||||
}
|
||||
// Если настроек нет, ничего не делаем.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import 'package:hive_ce/hive.dart';
|
||||
import '/data/repositories/interfaces/isms_message_repository.dart';
|
||||
import '/models/sms_message.dart';
|
||||
|
||||
/// Hive-реализация репозитория для работы с SMS сообщениями
|
||||
class HiveSmsMessageRepository implements ISmsMessageRepository {
|
||||
final Box<SmsMessage> _box;
|
||||
|
||||
HiveSmsMessageRepository(this._box);
|
||||
|
||||
@override
|
||||
Future<List<SmsMessage>> getAll() async {
|
||||
return _box.values.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<SmsMessage?> getById(String id) async {
|
||||
return _box.get(id);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> add(SmsMessage message) async {
|
||||
await _box.put(message.id, message);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> update(SmsMessage message) async {
|
||||
await add(message);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> delete(String id) async {
|
||||
await _box.delete(id);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> addAll(List<SmsMessage> messages) async {
|
||||
final Map<String, SmsMessage> messageMap = {
|
||||
for (var msg in messages) msg.id: msg
|
||||
};
|
||||
await _box.putAll(messageMap);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<SmsMessage>> getByTransactionId(String transactionId) async {
|
||||
return _box.values
|
||||
.where((msg) => msg.transactionId == transactionId)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,19 @@
|
||||
import 'package:budget_app/data/database/hive_service.dart';
|
||||
import 'package:budget_app/data/repositories/interfaces/itag_repository.dart';
|
||||
import 'package:budget_app/models/tag.dart';
|
||||
import 'package:hive_ce/hive.dart';
|
||||
import '/data/repositories/interfaces/itag_repository.dart';
|
||||
import '../../models/tag.dart';
|
||||
|
||||
class HiveTagRepository implements ITagRepository {
|
||||
final Box<Tag> _box;
|
||||
HiveTagRepository(); // Конструктор без параметров
|
||||
|
||||
HiveTagRepository(this._box);
|
||||
// Получаем бокс тегов из HiveService
|
||||
Box<Tag> get _box {
|
||||
if (HiveService.tags.isOpen) {
|
||||
return HiveService.tags;
|
||||
} else {
|
||||
throw Exception('Tags box is not initialized');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Tag>> getAll() async {
|
||||
@@ -32,12 +40,11 @@ class HiveTagRepository implements ITagRepository {
|
||||
await _box.delete(id);
|
||||
}
|
||||
|
||||
// Комментарий: Реализуем новый метод, объявленный в интерфейсе ITagRepository.
|
||||
@override
|
||||
Future<List<Tag>> getAllByUser(String userId) async {
|
||||
// Комментарий: Мы используем метод `where` для фильтрации всех записей в хранилище Hive.
|
||||
// Он перебирает все теги (`_box.values`) и возвращает только те,
|
||||
// у которых поле `userId` совпадает с идентификатором, переданным в метод.
|
||||
return _box.values.where((tag) => tag.userId == userId).toList();
|
||||
Future<void> addAll(List<Tag> tags) async {
|
||||
final Map<String, Tag> tagMap = {
|
||||
for (var tag in tags) tag.id: tag
|
||||
};
|
||||
await _box.putAll(tagMap);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,37 +60,11 @@ class HiveTransactionRepository implements ITransactionRepository {
|
||||
.toList();
|
||||
}
|
||||
|
||||
// Добавляем новый метод для получения всех транзакций конкретного пользователя
|
||||
@override
|
||||
Future<List<TransactionRecord>> getAllByUser(String userId) async {
|
||||
// Фильтруем все транзакции в Box по userId
|
||||
return _box.values.where((t) => t.userId == userId).toList();
|
||||
}
|
||||
|
||||
// Добавляем новый метод для получения транзакций пользователя по диапазону дат
|
||||
@override
|
||||
Future<List<TransactionRecord>> getByUserAndDateRange(String userId, DateTime from, DateTime to) async {
|
||||
// Фильтруем транзакции сначала по userId, затем по диапазону дат
|
||||
return _box.values
|
||||
.where((t) => t.userId == userId && t.dateTime.isAfter(from) && t.dateTime.isBefore(to))
|
||||
.toList();
|
||||
}
|
||||
|
||||
// Добавляем новый метод для получения транзакций пользователя по категории
|
||||
@override
|
||||
Future<List<TransactionRecord>> getByUserAndCategory(String userId, String categoryId) async {
|
||||
// Фильтруем транзакции сначала по userId, затем по категории
|
||||
return _box.values
|
||||
.where((t) => t.userId == userId && t.category.id == categoryId)
|
||||
.toList();
|
||||
}
|
||||
|
||||
// Добавляем новый метод для получения транзакций пользователя по тегу
|
||||
@override
|
||||
Future<List<TransactionRecord>> getByUserAndTag(String userId, String tagId) async {
|
||||
// Фильтруем транзакции сначала по userId, затем по тегу
|
||||
return _box.values
|
||||
.where((t) => t.userId == userId && t.tag?.id == tagId)
|
||||
.toList();
|
||||
Future<void> addAll(List<TransactionRecord> transactions) async {
|
||||
final Map<String, TransactionRecord> transactionMap = {
|
||||
for (var tr in transactions) tr.id: tr
|
||||
};
|
||||
await _box.putAll(transactionMap);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,13 +10,6 @@ abstract class ICategoryRepository {
|
||||
Future<List<Category>> getIncomeCategories();
|
||||
Future<List<Category>> getExpenseCategories();
|
||||
|
||||
// Новые методы для работы с пользователями
|
||||
/// Получить все категории конкретного пользователя
|
||||
Future<List<Category>> getAllByUser(String userId);
|
||||
|
||||
/// Получить категории доходов конкретного пользователя
|
||||
Future<List<Category>> getIncomeCategoriesByUser(String userId);
|
||||
|
||||
/// Получить категории расходов конкретного пользователя
|
||||
Future<List<Category>> getExpenseCategoriesByUser(String userId);
|
||||
/// Добавить список категорий
|
||||
Future<void> addAll(List<Category> categories);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
/// Сохраняет настройки
|
||||
/// [settings] - объект настроек для сохранения
|
||||
Future<void> saveSettings(AppSettings settings);
|
||||
|
||||
/// Удаляет настройки для указанного пользователя
|
||||
/// [userId] - идентификатор пользователя
|
||||
Future<void> deleteSettings();
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import '../../../models/sms_handler_settings.dart';
|
||||
|
||||
/// Абстрактный класс (интерфейс) для репозитория настроек обработки СМС.
|
||||
/// Определяет контракт, по которому UI и бизнес-логика будут взаимодействовать
|
||||
/// с данными о настройках, не зная деталей их реализации (Hive, Firebase, etc).
|
||||
abstract class ISmsHandlerRepository {
|
||||
/// Получает настройки обработки СМС для указанного пользователя.
|
||||
///
|
||||
Future<SmsHandlerSettings?> getSmsHandlerSettings();
|
||||
|
||||
/// Сохраняет или обновляет настройки обработки СМС для пользователя.
|
||||
///
|
||||
/// [settings] - Объект с настройками, который нужно сохранить.
|
||||
Future<void> saveSmsHandlerSettings(SmsHandlerSettings settings);
|
||||
|
||||
/// Добавляет или обновляет правило для конкретного отправителя.
|
||||
///
|
||||
/// [userId] - ID пользователя.
|
||||
/// [sender] - Идентификатор отправителя (например, 'SBERBANK').
|
||||
/// [rule] - Правило обработки.
|
||||
Future<void> saveRuleForSender(String sender, SmsProcessingRule rule);
|
||||
|
||||
/// Удаляет правило для конкретного отправителя.
|
||||
///
|
||||
/// [userId] - ID пользователя.
|
||||
/// [sender] - Идентификатор отправителя, чье правило нужно удалить.
|
||||
Future<void> deleteRuleForSender(String sender);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import '/models/sms_message.dart';
|
||||
|
||||
// Интерфейс репозитория для работы с SMS сообщениями
|
||||
abstract class ISmsMessageRepository {
|
||||
/// Получает все SMS сообщения
|
||||
Future<List<SmsMessage>> getAll();
|
||||
|
||||
/// Получает SMS сообщение по ID
|
||||
Future<SmsMessage?> getById(String id);
|
||||
|
||||
/// Добавляет новое SMS сообщение
|
||||
Future<void> add(SmsMessage message);
|
||||
|
||||
/// Обновляет существующее SMS сообщение
|
||||
Future<void> update(SmsMessage message);
|
||||
|
||||
/// Удаляет SMS сообщение по ID
|
||||
Future<void> delete(String id);
|
||||
|
||||
/// Добавляет несколько SMS сообщений
|
||||
Future<void> addAll(List<SmsMessage> messages);
|
||||
|
||||
/// Получает SMS сообщения, связанные с транзакцией
|
||||
Future<List<SmsMessage>> getByTransactionId(String transactionId);
|
||||
}
|
||||
@@ -7,9 +7,6 @@ abstract class ITagRepository {
|
||||
Future<void> update(Tag tag);
|
||||
Future<void> delete(String id);
|
||||
|
||||
// Комментарий: Добавляем новый абстрактный метод в интерфейс.
|
||||
// Все классы, которые реализуют этот интерфейс, должны будут предоставить
|
||||
// реализацию этого метода. Это гарантирует, что наш репозиторий
|
||||
// сможет получать теги для конкретного пользователя.
|
||||
Future<List<Tag>> getAllByUser(String userId);
|
||||
/// Добавить список тегов
|
||||
Future<void> addAll(List<Tag> tags);
|
||||
}
|
||||
|
||||
@@ -11,10 +11,6 @@ abstract class ITransactionRepository {
|
||||
Future<List<TransactionRecord>> getByCategory(String categoryId);
|
||||
Future<List<TransactionRecord>> getByTag(String tagId);
|
||||
|
||||
// Добавляем новые методы для работы с транзакциями конкретного пользователя
|
||||
// Это важно для многопользовательской архитектуры
|
||||
Future<List<TransactionRecord>> getAllByUser(String userId);
|
||||
Future<List<TransactionRecord>> getByUserAndDateRange(String userId, DateTime from, DateTime to);
|
||||
Future<List<TransactionRecord>> getByUserAndCategory(String userId, String categoryId);
|
||||
Future<List<TransactionRecord>> getByUserAndTag(String userId, String tagId);
|
||||
/// Добавить список транзакций
|
||||
Future<void> addAll(List<TransactionRecord> transactions);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
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>(),
|
||||
AdapterSpec<IconData>(),
|
||||
])
|
||||
part 'hive_adapters.g.dart';
|
||||
|
||||
@@ -6,38 +6,6 @@ part of 'hive_adapters.dart';
|
||||
// AdaptersGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class ColorAdapter extends TypeAdapter<Color> {
|
||||
@override
|
||||
final typeId = 0;
|
||||
|
||||
@override
|
||||
Color read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return Color((fields[0] as num).toInt());
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, Color obj) {
|
||||
writer
|
||||
..writeByte(1)
|
||||
..writeByte(0)
|
||||
..write(obj.value);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is ColorAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
|
||||
class IconDataAdapter extends TypeAdapter<IconData> {
|
||||
@override
|
||||
final typeId = 1;
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
# Generated by Hive CE
|
||||
# Manual modifications may be necessary for certain migrations
|
||||
# Check in to version control
|
||||
nextTypeId: 2
|
||||
nextTypeId: 3
|
||||
types:
|
||||
Color:
|
||||
typeId: 0
|
||||
nextIndex: 1
|
||||
fields:
|
||||
value:
|
||||
index: 0
|
||||
IconData:
|
||||
typeId: 1
|
||||
nextIndex: 5
|
||||
|
||||
@@ -4,16 +4,25 @@
|
||||
|
||||
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_handler_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';
|
||||
import 'package:budget_app/models/user.dart';
|
||||
|
||||
extension HiveRegistrar on HiveInterface {
|
||||
void registerAdapters() {
|
||||
registerAdapter(AppSettingsAdapter());
|
||||
registerAdapter(CategoryAdapter());
|
||||
registerAdapter(ColorAdapter());
|
||||
registerAdapter(GlobalSettingsAdapter());
|
||||
registerAdapter(IconDataAdapter());
|
||||
registerAdapter(SmsHandlerSettingsAdapter());
|
||||
registerAdapter(SmsMessageAdapter());
|
||||
registerAdapter(SmsProcessingRuleAdapter());
|
||||
registerAdapter(SmsProcessingTypeAdapter());
|
||||
registerAdapter(TagAdapter());
|
||||
registerAdapter(TransactionRecordAdapter());
|
||||
registerAdapter(UserAdapter());
|
||||
@@ -22,9 +31,14 @@ extension HiveRegistrar on HiveInterface {
|
||||
|
||||
extension IsolatedHiveRegistrar on IsolatedHiveInterface {
|
||||
void registerAdapters() {
|
||||
registerAdapter(AppSettingsAdapter());
|
||||
registerAdapter(CategoryAdapter());
|
||||
registerAdapter(ColorAdapter());
|
||||
registerAdapter(GlobalSettingsAdapter());
|
||||
registerAdapter(IconDataAdapter());
|
||||
registerAdapter(SmsHandlerSettingsAdapter());
|
||||
registerAdapter(SmsMessageAdapter());
|
||||
registerAdapter(SmsProcessingRuleAdapter());
|
||||
registerAdapter(SmsProcessingTypeAdapter());
|
||||
registerAdapter(TagAdapter());
|
||||
registerAdapter(TransactionRecordAdapter());
|
||||
registerAdapter(UserAdapter());
|
||||
|
||||
+171
-14
@@ -1,32 +1,189 @@
|
||||
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';
|
||||
import 'data/repositories/hive_sms_handler_repository.dart';
|
||||
import 'data/repositories/hive_sms_message_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/iglobal_settings_repository.dart';
|
||||
import 'data/repositories/interfaces/isettings_repository.dart';
|
||||
import 'data/repositories/interfaces/isms_handler_repository.dart';
|
||||
import 'data/repositories/interfaces/isms_message_repository.dart';
|
||||
import 'data/repositories/interfaces/itag_repository.dart';
|
||||
import 'data/repositories/interfaces/itransaction_repository.dart';
|
||||
import 'services/settings_service.dart';
|
||||
import 'data/repositories/interfaces/iuser_repository.dart';
|
||||
import 'logic/auth/auth_bloc.dart';
|
||||
import 'logic/category/category_cubit.dart';
|
||||
import 'logic/settings/settings_cubit.dart';
|
||||
import 'logic/sms/sms_cubit.dart';
|
||||
import 'logic/tag/tag_cubit.dart';
|
||||
import 'logic/transaction/transaction_bloc.dart';
|
||||
import 'logic/user/user_cubit.dart';
|
||||
import 'services/sms_service.dart';
|
||||
|
||||
final getIt = GetIt.instance;
|
||||
|
||||
Future<void> initDependencies() async {
|
||||
/// Инициализация глобальных зависимостей, которые не зависят от пользователя.
|
||||
/// Вызывается один раз при старте приложения.
|
||||
Future<void> initGlobalDependencies() async {
|
||||
// Инициализация Hive
|
||||
await HiveService.init();
|
||||
await HiveService.initGlobalBoxes();
|
||||
|
||||
// Регистрация сервисов
|
||||
getIt.registerSingleton<SettingsService>(SettingsService());
|
||||
|
||||
// Регистрация репозиториев
|
||||
getIt.registerSingleton<ICategoryRepository>(
|
||||
HiveCategoryRepository(HiveService.categories),
|
||||
// Глобальные репозитории
|
||||
getIt.registerSingleton<IUserRepository>(
|
||||
HiveUserRepository(HiveService.users),
|
||||
);
|
||||
getIt.registerSingleton<IGlobalSettingsRepository>(
|
||||
HiveGlobalSettingsRepository(HiveService.globalSettings),
|
||||
);
|
||||
|
||||
getIt.registerSingleton<ITagRepository>(
|
||||
HiveTagRepository(HiveService.tags),
|
||||
);
|
||||
// Сервисы
|
||||
getIt.registerSingleton<SmsService>(SmsService());
|
||||
|
||||
getIt.registerSingleton<ITransactionRepository>(
|
||||
HiveTransactionRepository(HiveService.transactions),
|
||||
// Cubits & Blocs, которые нужны до входа пользователя
|
||||
getIt.registerSingleton<UserCubit>(
|
||||
UserCubit(
|
||||
settingsRepository: getIt<IGlobalSettingsRepository>(),
|
||||
userRepository: getIt<IUserRepository>(),
|
||||
// Эти репозитории будут заменены после входа пользователя
|
||||
categoryRepository:
|
||||
null, // Временно null, будет заменен в initUserSpecificDependencies
|
||||
tagRepository:
|
||||
null, // Временно null, будет заменен в initUserSpecificDependencies
|
||||
transactionRepository:
|
||||
null, // Временно null, будет заменен в initUserSpecificDependencies
|
||||
),
|
||||
);
|
||||
getIt.registerFactory<AuthBloc>(() => AuthBloc(
|
||||
userCubit: getIt(),
|
||||
settingsRepository: getIt(),
|
||||
userRepository: getIt(),
|
||||
));
|
||||
}
|
||||
|
||||
/// Инициализация зависимостей, специфичных для пользователя.
|
||||
/// Вызывается после успешной аутентификации.
|
||||
Future<void> initUserSpecificDependencies(String userId) async {
|
||||
// Открываем пользовательские Hive Box'ы
|
||||
await HiveService.initUserBoxes(userId);
|
||||
|
||||
// --- Регистрация пользовательских репозиториев ---
|
||||
// Используем lazy singletons, чтобы их можно было легко сбросить и создать заново при смене пользователя.
|
||||
|
||||
// Категории
|
||||
if (getIt.isRegistered<ICategoryRepository>()) {
|
||||
await getIt.unregister<ICategoryRepository>();
|
||||
}
|
||||
getIt.registerLazySingleton<ICategoryRepository>(
|
||||
() => HiveCategoryRepository(HiveService.categories),
|
||||
);
|
||||
|
||||
// Теги
|
||||
if (getIt.isRegistered<ITagRepository>()) {
|
||||
await getIt.unregister<ITagRepository>();
|
||||
}
|
||||
getIt.registerLazySingleton<ITagRepository>(
|
||||
() => HiveTagRepository(),
|
||||
);
|
||||
|
||||
// Транзакции
|
||||
if (getIt.isRegistered<ITransactionRepository>()) {
|
||||
await getIt.unregister<ITransactionRepository>();
|
||||
}
|
||||
getIt.registerLazySingleton<ITransactionRepository>(
|
||||
() => HiveTransactionRepository(HiveService.transactions),
|
||||
);
|
||||
|
||||
// Настройки приложения
|
||||
if (getIt.isRegistered<ISettingsRepository>()) {
|
||||
await getIt.unregister<ISettingsRepository>();
|
||||
}
|
||||
getIt.registerLazySingleton<ISettingsRepository>(
|
||||
() => HiveSettingsRepository(HiveService.appSettings),
|
||||
);
|
||||
|
||||
// Настройки обработчика SMS
|
||||
if (getIt.isRegistered<ISmsHandlerRepository>()) {
|
||||
await getIt.unregister<ISmsHandlerRepository>();
|
||||
}
|
||||
getIt.registerLazySingleton<ISmsHandlerRepository>(
|
||||
() => HiveSmsHandlerRepository(HiveService.smsHandlerSettings),
|
||||
);
|
||||
|
||||
// Сообщения SMS
|
||||
if (getIt.isRegistered<ISmsMessageRepository>()) {
|
||||
await getIt.unregister<ISmsMessageRepository>();
|
||||
}
|
||||
getIt.registerLazySingleton<ISmsMessageRepository>(
|
||||
() => HiveSmsMessageRepository(HiveService.smsMessages),
|
||||
);
|
||||
|
||||
// --- Обновление UserCubit новыми репозиториями ---
|
||||
// Мы не пересоздаем UserCubit, а просто обновляем его зависимости.
|
||||
final userCubit = getIt<UserCubit>();
|
||||
userCubit.categoryRepository = getIt<ICategoryRepository>();
|
||||
userCubit.tagRepository = getIt<ITagRepository>();
|
||||
userCubit.transactionRepository = getIt<ITransactionRepository>();
|
||||
|
||||
// --- Регистрация Cubits & Blocs, которые зависят от пользовательских данных ---
|
||||
|
||||
// Настройки
|
||||
if (getIt.isRegistered<SettingsCubit>()) {
|
||||
await getIt.unregister<SettingsCubit>();
|
||||
}
|
||||
getIt.registerSingleton<SettingsCubit>(
|
||||
SettingsCubit(getIt()),
|
||||
);
|
||||
|
||||
// Транзакции
|
||||
if (getIt.isRegistered<TransactionBloc>()) {
|
||||
await getIt.unregister<TransactionBloc>();
|
||||
}
|
||||
getIt.registerFactory<TransactionBloc>(
|
||||
() => TransactionBloc(transactionRepository: getIt()),
|
||||
);
|
||||
|
||||
// SMS
|
||||
if (getIt.isRegistered<SmsCubit>()) {
|
||||
await getIt.unregister<SmsCubit>();
|
||||
}
|
||||
getIt.registerFactory<SmsCubit>(() => SmsCubit(getIt(), getIt(), getIt()));
|
||||
|
||||
// Категории
|
||||
if (getIt.isRegistered<CategoryCubit>()) {
|
||||
await getIt.unregister<CategoryCubit>();
|
||||
}
|
||||
getIt.registerFactory<CategoryCubit>(
|
||||
() => CategoryCubit(getIt()), // Добавляем userId
|
||||
);
|
||||
|
||||
// Теги
|
||||
if (getIt.isRegistered<TagCubit>()) {
|
||||
await getIt.unregister<TagCubit>();
|
||||
}
|
||||
getIt.registerFactory<TagCubit>(() => TagCubit(getIt()));
|
||||
}
|
||||
|
||||
/// Сброс пользовательских зависимостей при выходе из системы.
|
||||
Future<void> resetUserSpecificDependencies() async {
|
||||
// Закрываем пользовательские Hive Box'ы
|
||||
await HiveService.closeUserBoxes();
|
||||
|
||||
// Разрегистрация всех пользовательских зависимостей
|
||||
await getIt.unregister<ICategoryRepository>();
|
||||
await getIt.unregister<ITagRepository>();
|
||||
await getIt.unregister<ITransactionRepository>();
|
||||
await getIt.unregister<ISettingsRepository>();
|
||||
await getIt.unregister<ISmsHandlerRepository>();
|
||||
await getIt.unregister<ISmsMessageRepository>();
|
||||
await getIt.unregister<SettingsCubit>();
|
||||
await getIt.unregister<TransactionBloc>();
|
||||
await getIt.unregister<SmsCubit>();
|
||||
await getIt.unregister<CategoryCubit>();
|
||||
await getIt.unregister<TagCubit>();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"@@locale": "en",
|
||||
"appTitle": "Budget App",
|
||||
"homePageTitle": "Home",
|
||||
"reportsPageTitle": "Reports",
|
||||
"settingsPageTitle": "Settings",
|
||||
"addTransactionButton": "Add new transaction",
|
||||
"noTransactionsText": "No transactions yet",
|
||||
"loadingTransactionsText": "Loading transactions...",
|
||||
"transactionErrorText": "Error: {message}",
|
||||
"loginPageTitle": "Login",
|
||||
"nameFieldLabel": "Name",
|
||||
"emailFieldLabel": "Email",
|
||||
"nameFieldEmptyError": "Please enter your name",
|
||||
"emailFieldEmptyError": "Please enter your email",
|
||||
"loginButtonText": "Login / Register",
|
||||
"darkModeSetting": "Dark Theme",
|
||||
"darkModeDescription": "Toggle between light and dark theme",
|
||||
"languageSetting": "Language",
|
||||
"languageDescription": "Change application language",
|
||||
"russianLanguage": "Russian",
|
||||
"englishLanguage": "English",
|
||||
"defaultUser": "Default User",
|
||||
"currencySetting": "Default Currency",
|
||||
"currencyDescription": "Set the default currency for transactions",
|
||||
"transactionsHistoryTitle": "Transactions History",
|
||||
"balance": "Balance",
|
||||
"income": "Income",
|
||||
"expense": "Expense",
|
||||
"amount": "Amount",
|
||||
"vendor": "Vendor",
|
||||
"category": "Category",
|
||||
"date": "Date",
|
||||
"requiredField": "Required field",
|
||||
"invalidNumber": "Invalid number",
|
||||
"cancel": "Cancel",
|
||||
"save": "Save",
|
||||
"tag": "Tag",
|
||||
"icon": "Icon",
|
||||
"smsPageTitle": "SMS Messages",
|
||||
"smsPermissionDenied": "SMS permission is required",
|
||||
"editCategories": "Edit Categories",
|
||||
"editCategoriesDescription": "Add, edit, or delete categories",
|
||||
"color": "Color",
|
||||
"chooseIcon": "Pick icon",
|
||||
"chooseIconHint": "Search",
|
||||
"categoryType": "Category type",
|
||||
"addCategory": "Add category",
|
||||
"editTags": "Edit Tags",
|
||||
"editTagsDescription": "Add, edit, or delete tags",
|
||||
"addTag": "Add tag",
|
||||
"editTag": "Edit tag",
|
||||
"loadSmsMessages": "Load SMS Messages",
|
||||
"loadSmsMessagesDescription": "Load and process SMS messages to automatically create transactions",
|
||||
"smsSettings": "Processing settings",
|
||||
"createTransaction": "Create transaction",
|
||||
"unknownSender": "Unknown sender",
|
||||
"smsProcessed": "Processed",
|
||||
"smsNotProcessed": "Not processed"
|
||||
}
|
||||
@@ -0,0 +1,476 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:intl/intl.dart' as intl;
|
||||
|
||||
import 'app_localizations_en.dart';
|
||||
import 'app_localizations_ru.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
|
||||
/// Callers can lookup localized strings with an instance of AppLocalizations
|
||||
/// returned by `AppLocalizations.of(context)`.
|
||||
///
|
||||
/// Applications need to include `AppLocalizations.delegate()` in their app's
|
||||
/// `localizationDelegates` list, and the locales they support in the app's
|
||||
/// `supportedLocales` list. For example:
|
||||
///
|
||||
/// ```dart
|
||||
/// import 'l10n/app_localizations.dart';
|
||||
///
|
||||
/// return MaterialApp(
|
||||
/// localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
/// supportedLocales: AppLocalizations.supportedLocales,
|
||||
/// home: MyApplicationHome(),
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
/// ## Update pubspec.yaml
|
||||
///
|
||||
/// Please make sure to update your pubspec.yaml to include the following
|
||||
/// packages:
|
||||
///
|
||||
/// ```yaml
|
||||
/// dependencies:
|
||||
/// # Internationalization support.
|
||||
/// flutter_localizations:
|
||||
/// sdk: flutter
|
||||
/// intl: any # Use the pinned version from flutter_localizations
|
||||
///
|
||||
/// # Rest of dependencies
|
||||
/// ```
|
||||
///
|
||||
/// ## iOS Applications
|
||||
///
|
||||
/// iOS applications define key application metadata, including supported
|
||||
/// locales, in an Info.plist file that is built into the application bundle.
|
||||
/// To configure the locales supported by your app, you’ll need to edit this
|
||||
/// file.
|
||||
///
|
||||
/// First, open your project’s ios/Runner.xcworkspace Xcode workspace file.
|
||||
/// Then, in the Project Navigator, open the Info.plist file under the Runner
|
||||
/// project’s Runner folder.
|
||||
///
|
||||
/// Next, select the Information Property List item, select Add Item from the
|
||||
/// Editor menu, then select Localizations from the pop-up menu.
|
||||
///
|
||||
/// Select and expand the newly-created Localizations item then, for each
|
||||
/// locale your application supports, add a new item and select the locale
|
||||
/// you wish to add from the pop-up menu in the Value field. This list should
|
||||
/// be consistent with the languages listed in the AppLocalizations.supportedLocales
|
||||
/// property.
|
||||
abstract class AppLocalizations {
|
||||
AppLocalizations(String locale)
|
||||
: localeName = intl.Intl.canonicalizedLocale(locale.toString());
|
||||
|
||||
final String localeName;
|
||||
|
||||
static AppLocalizations? of(BuildContext context) {
|
||||
return Localizations.of<AppLocalizations>(context, AppLocalizations);
|
||||
}
|
||||
|
||||
static const LocalizationsDelegate<AppLocalizations> delegate =
|
||||
_AppLocalizationsDelegate();
|
||||
|
||||
/// A list of this localizations delegate along with the default localizations
|
||||
/// delegates.
|
||||
///
|
||||
/// Returns a list of localizations delegates containing this delegate along with
|
||||
/// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate,
|
||||
/// and GlobalWidgetsLocalizations.delegate.
|
||||
///
|
||||
/// Additional delegates can be added by appending to this list in
|
||||
/// MaterialApp. This list does not have to be used at all if a custom list
|
||||
/// of delegates is preferred or required.
|
||||
static const List<LocalizationsDelegate<dynamic>> localizationsDelegates =
|
||||
<LocalizationsDelegate<dynamic>>[
|
||||
delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
];
|
||||
|
||||
/// A list of this localizations delegate's supported locales.
|
||||
static const List<Locale> supportedLocales = <Locale>[
|
||||
Locale('en'),
|
||||
Locale('ru'),
|
||||
];
|
||||
|
||||
/// No description provided for @appTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Budget App'**
|
||||
String get appTitle;
|
||||
|
||||
/// No description provided for @homePageTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Home'**
|
||||
String get homePageTitle;
|
||||
|
||||
/// No description provided for @reportsPageTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Reports'**
|
||||
String get reportsPageTitle;
|
||||
|
||||
/// No description provided for @settingsPageTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Settings'**
|
||||
String get settingsPageTitle;
|
||||
|
||||
/// No description provided for @addTransactionButton.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Add new transaction'**
|
||||
String get addTransactionButton;
|
||||
|
||||
/// No description provided for @noTransactionsText.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No transactions yet'**
|
||||
String get noTransactionsText;
|
||||
|
||||
/// No description provided for @loadingTransactionsText.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Loading transactions...'**
|
||||
String get loadingTransactionsText;
|
||||
|
||||
/// No description provided for @transactionErrorText.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Error: {message}'**
|
||||
String transactionErrorText(Object message);
|
||||
|
||||
/// No description provided for @loginPageTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Login'**
|
||||
String get loginPageTitle;
|
||||
|
||||
/// No description provided for @nameFieldLabel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Name'**
|
||||
String get nameFieldLabel;
|
||||
|
||||
/// No description provided for @emailFieldLabel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Email'**
|
||||
String get emailFieldLabel;
|
||||
|
||||
/// No description provided for @nameFieldEmptyError.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Please enter your name'**
|
||||
String get nameFieldEmptyError;
|
||||
|
||||
/// No description provided for @emailFieldEmptyError.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Please enter your email'**
|
||||
String get emailFieldEmptyError;
|
||||
|
||||
/// No description provided for @loginButtonText.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Login / Register'**
|
||||
String get loginButtonText;
|
||||
|
||||
/// No description provided for @darkModeSetting.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Dark Theme'**
|
||||
String get darkModeSetting;
|
||||
|
||||
/// No description provided for @darkModeDescription.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Toggle between light and dark theme'**
|
||||
String get darkModeDescription;
|
||||
|
||||
/// No description provided for @languageSetting.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Language'**
|
||||
String get languageSetting;
|
||||
|
||||
/// No description provided for @languageDescription.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Change application language'**
|
||||
String get languageDescription;
|
||||
|
||||
/// No description provided for @russianLanguage.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Russian'**
|
||||
String get russianLanguage;
|
||||
|
||||
/// No description provided for @englishLanguage.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'English'**
|
||||
String get englishLanguage;
|
||||
|
||||
/// No description provided for @defaultUser.
|
||||
///
|
||||
/// 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;
|
||||
|
||||
/// No description provided for @transactionsHistoryTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Transactions History'**
|
||||
String get transactionsHistoryTitle;
|
||||
|
||||
/// No description provided for @balance.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Balance'**
|
||||
String get balance;
|
||||
|
||||
/// No description provided for @income.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Income'**
|
||||
String get income;
|
||||
|
||||
/// No description provided for @expense.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Expense'**
|
||||
String get expense;
|
||||
|
||||
/// No description provided for @amount.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Amount'**
|
||||
String get amount;
|
||||
|
||||
/// No description provided for @vendor.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Vendor'**
|
||||
String get vendor;
|
||||
|
||||
/// No description provided for @category.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Category'**
|
||||
String get category;
|
||||
|
||||
/// No description provided for @date.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Date'**
|
||||
String get date;
|
||||
|
||||
/// No description provided for @requiredField.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Required field'**
|
||||
String get requiredField;
|
||||
|
||||
/// No description provided for @invalidNumber.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Invalid number'**
|
||||
String get invalidNumber;
|
||||
|
||||
/// No description provided for @cancel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Cancel'**
|
||||
String get cancel;
|
||||
|
||||
/// No description provided for @save.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Save'**
|
||||
String get save;
|
||||
|
||||
/// No description provided for @tag.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Tag'**
|
||||
String get tag;
|
||||
|
||||
/// No description provided for @icon.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Icon'**
|
||||
String get icon;
|
||||
|
||||
/// No description provided for @smsPageTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'SMS Messages'**
|
||||
String get smsPageTitle;
|
||||
|
||||
/// No description provided for @smsPermissionDenied.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'SMS permission is required'**
|
||||
String get smsPermissionDenied;
|
||||
|
||||
/// No description provided for @editCategories.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Edit Categories'**
|
||||
String get editCategories;
|
||||
|
||||
/// No description provided for @editCategoriesDescription.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Add, edit, or delete categories'**
|
||||
String get editCategoriesDescription;
|
||||
|
||||
/// No description provided for @color.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Color'**
|
||||
String get color;
|
||||
|
||||
/// No description provided for @chooseIcon.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Pick icon'**
|
||||
String get chooseIcon;
|
||||
|
||||
/// No description provided for @chooseIconHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Search'**
|
||||
String get chooseIconHint;
|
||||
|
||||
/// No description provided for @categoryType.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Category type'**
|
||||
String get categoryType;
|
||||
|
||||
/// No description provided for @addCategory.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Add category'**
|
||||
String get addCategory;
|
||||
|
||||
/// No description provided for @editTags.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Edit Tags'**
|
||||
String get editTags;
|
||||
|
||||
/// No description provided for @editTagsDescription.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Add, edit, or delete tags'**
|
||||
String get editTagsDescription;
|
||||
|
||||
/// No description provided for @addTag.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Add tag'**
|
||||
String get addTag;
|
||||
|
||||
/// No description provided for @editTag.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Edit tag'**
|
||||
String get editTag;
|
||||
|
||||
/// No description provided for @loadSmsMessages.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Load SMS Messages'**
|
||||
String get loadSmsMessages;
|
||||
|
||||
/// No description provided for @loadSmsMessagesDescription.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Load and process SMS messages to automatically create transactions'**
|
||||
String get loadSmsMessagesDescription;
|
||||
|
||||
/// No description provided for @smsSettings.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Processing settings'**
|
||||
String get smsSettings;
|
||||
|
||||
/// No description provided for @createTransaction.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Create transaction'**
|
||||
String get createTransaction;
|
||||
|
||||
/// No description provided for @unknownSender.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Unknown sender'**
|
||||
String get unknownSender;
|
||||
|
||||
/// No description provided for @smsProcessed.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Processed'**
|
||||
String get smsProcessed;
|
||||
|
||||
/// No description provided for @smsNotProcessed.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Not processed'**
|
||||
String get smsNotProcessed;
|
||||
}
|
||||
|
||||
class _AppLocalizationsDelegate
|
||||
extends LocalizationsDelegate<AppLocalizations> {
|
||||
const _AppLocalizationsDelegate();
|
||||
|
||||
@override
|
||||
Future<AppLocalizations> load(Locale locale) {
|
||||
return SynchronousFuture<AppLocalizations>(lookupAppLocalizations(locale));
|
||||
}
|
||||
|
||||
@override
|
||||
bool isSupported(Locale locale) =>
|
||||
<String>['en', 'ru'].contains(locale.languageCode);
|
||||
|
||||
@override
|
||||
bool shouldReload(_AppLocalizationsDelegate old) => false;
|
||||
}
|
||||
|
||||
AppLocalizations lookupAppLocalizations(Locale locale) {
|
||||
// Lookup logic when only language code is specified.
|
||||
switch (locale.languageCode) {
|
||||
case 'en':
|
||||
return AppLocalizationsEn();
|
||||
case 'ru':
|
||||
return AppLocalizationsRu();
|
||||
}
|
||||
|
||||
throw FlutterError(
|
||||
'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely '
|
||||
'an issue with the localizations generation tool. Please file an issue '
|
||||
'on GitHub with a reproducible sample app and the gen-l10n configuration '
|
||||
'that was used.',
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
// ignore: unused_import
|
||||
import 'package:intl/intl.dart' as intl;
|
||||
import 'app_localizations.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
|
||||
/// The translations for English (`en`).
|
||||
class AppLocalizationsEn extends AppLocalizations {
|
||||
AppLocalizationsEn([String locale = 'en']) : super(locale);
|
||||
|
||||
@override
|
||||
String get appTitle => 'Budget App';
|
||||
|
||||
@override
|
||||
String get homePageTitle => 'Home';
|
||||
|
||||
@override
|
||||
String get reportsPageTitle => 'Reports';
|
||||
|
||||
@override
|
||||
String get settingsPageTitle => 'Settings';
|
||||
|
||||
@override
|
||||
String get addTransactionButton => 'Add new transaction';
|
||||
|
||||
@override
|
||||
String get noTransactionsText => 'No transactions yet';
|
||||
|
||||
@override
|
||||
String get loadingTransactionsText => 'Loading transactions...';
|
||||
|
||||
@override
|
||||
String transactionErrorText(Object message) {
|
||||
return 'Error: $message';
|
||||
}
|
||||
|
||||
@override
|
||||
String get loginPageTitle => 'Login';
|
||||
|
||||
@override
|
||||
String get nameFieldLabel => 'Name';
|
||||
|
||||
@override
|
||||
String get emailFieldLabel => 'Email';
|
||||
|
||||
@override
|
||||
String get nameFieldEmptyError => 'Please enter your name';
|
||||
|
||||
@override
|
||||
String get emailFieldEmptyError => 'Please enter your email';
|
||||
|
||||
@override
|
||||
String get loginButtonText => 'Login / Register';
|
||||
|
||||
@override
|
||||
String get darkModeSetting => 'Dark Theme';
|
||||
|
||||
@override
|
||||
String get darkModeDescription => 'Toggle between light and dark theme';
|
||||
|
||||
@override
|
||||
String get languageSetting => 'Language';
|
||||
|
||||
@override
|
||||
String get languageDescription => 'Change application language';
|
||||
|
||||
@override
|
||||
String get russianLanguage => 'Russian';
|
||||
|
||||
@override
|
||||
String get englishLanguage => 'English';
|
||||
|
||||
@override
|
||||
String get defaultUser => 'Default User';
|
||||
|
||||
@override
|
||||
String get currencySetting => 'Default Currency';
|
||||
|
||||
@override
|
||||
String get currencyDescription => 'Set the default currency for transactions';
|
||||
|
||||
@override
|
||||
String get transactionsHistoryTitle => 'Transactions History';
|
||||
|
||||
@override
|
||||
String get balance => 'Balance';
|
||||
|
||||
@override
|
||||
String get income => 'Income';
|
||||
|
||||
@override
|
||||
String get expense => 'Expense';
|
||||
|
||||
@override
|
||||
String get amount => 'Amount';
|
||||
|
||||
@override
|
||||
String get vendor => 'Vendor';
|
||||
|
||||
@override
|
||||
String get category => 'Category';
|
||||
|
||||
@override
|
||||
String get date => 'Date';
|
||||
|
||||
@override
|
||||
String get requiredField => 'Required field';
|
||||
|
||||
@override
|
||||
String get invalidNumber => 'Invalid number';
|
||||
|
||||
@override
|
||||
String get cancel => 'Cancel';
|
||||
|
||||
@override
|
||||
String get save => 'Save';
|
||||
|
||||
@override
|
||||
String get tag => 'Tag';
|
||||
|
||||
@override
|
||||
String get icon => 'Icon';
|
||||
|
||||
@override
|
||||
String get smsPageTitle => 'SMS Messages';
|
||||
|
||||
@override
|
||||
String get smsPermissionDenied => 'SMS permission is required';
|
||||
|
||||
@override
|
||||
String get editCategories => 'Edit Categories';
|
||||
|
||||
@override
|
||||
String get editCategoriesDescription => 'Add, edit, or delete categories';
|
||||
|
||||
@override
|
||||
String get color => 'Color';
|
||||
|
||||
@override
|
||||
String get chooseIcon => 'Pick icon';
|
||||
|
||||
@override
|
||||
String get chooseIconHint => 'Search';
|
||||
|
||||
@override
|
||||
String get categoryType => 'Category type';
|
||||
|
||||
@override
|
||||
String get addCategory => 'Add category';
|
||||
|
||||
@override
|
||||
String get editTags => 'Edit Tags';
|
||||
|
||||
@override
|
||||
String get editTagsDescription => 'Add, edit, or delete tags';
|
||||
|
||||
@override
|
||||
String get addTag => 'Add tag';
|
||||
|
||||
@override
|
||||
String get editTag => 'Edit tag';
|
||||
|
||||
@override
|
||||
String get loadSmsMessages => 'Load SMS Messages';
|
||||
|
||||
@override
|
||||
String get loadSmsMessagesDescription =>
|
||||
'Load and process SMS messages to automatically create transactions';
|
||||
|
||||
@override
|
||||
String get smsSettings => 'Processing settings';
|
||||
|
||||
@override
|
||||
String get createTransaction => 'Create transaction';
|
||||
|
||||
@override
|
||||
String get unknownSender => 'Unknown sender';
|
||||
|
||||
@override
|
||||
String get smsProcessed => 'Processed';
|
||||
|
||||
@override
|
||||
String get smsNotProcessed => 'Not processed';
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
// ignore: unused_import
|
||||
import 'package:intl/intl.dart' as intl;
|
||||
import 'app_localizations.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
|
||||
/// The translations for Russian (`ru`).
|
||||
class AppLocalizationsRu extends AppLocalizations {
|
||||
AppLocalizationsRu([String locale = 'ru']) : super(locale);
|
||||
|
||||
@override
|
||||
String get appTitle => 'Бюджетное приложение';
|
||||
|
||||
@override
|
||||
String get homePageTitle => 'Главная';
|
||||
|
||||
@override
|
||||
String get reportsPageTitle => 'Отчеты';
|
||||
|
||||
@override
|
||||
String get settingsPageTitle => 'Настройки';
|
||||
|
||||
@override
|
||||
String get addTransactionButton => 'Добавить новую транзакцию';
|
||||
|
||||
@override
|
||||
String get noTransactionsText => 'Нет транзакций';
|
||||
|
||||
@override
|
||||
String get loadingTransactionsText => 'Загрузка транзакций...';
|
||||
|
||||
@override
|
||||
String transactionErrorText(Object message) {
|
||||
return 'Ошибка: $message';
|
||||
}
|
||||
|
||||
@override
|
||||
String get loginPageTitle => 'Вход';
|
||||
|
||||
@override
|
||||
String get nameFieldLabel => 'Имя';
|
||||
|
||||
@override
|
||||
String get emailFieldLabel => 'Email';
|
||||
|
||||
@override
|
||||
String get nameFieldEmptyError => 'Пожалуйста, введите имя';
|
||||
|
||||
@override
|
||||
String get emailFieldEmptyError => 'Пожалуйста, введите email';
|
||||
|
||||
@override
|
||||
String get loginButtonText => 'Войти / Зарегистрироваться';
|
||||
|
||||
@override
|
||||
String get darkModeSetting => 'Темная тема';
|
||||
|
||||
@override
|
||||
String get darkModeDescription => 'Переключить между светлой и темной темой';
|
||||
|
||||
@override
|
||||
String get languageSetting => 'Язык';
|
||||
|
||||
@override
|
||||
String get languageDescription => 'Изменить язык приложения';
|
||||
|
||||
@override
|
||||
String get russianLanguage => 'Русский';
|
||||
|
||||
@override
|
||||
String get englishLanguage => 'Английский';
|
||||
|
||||
@override
|
||||
String get defaultUser => 'Пользователь по умолчанию';
|
||||
|
||||
@override
|
||||
String get currencySetting => 'Валюта по умолчанию';
|
||||
|
||||
@override
|
||||
String get currencyDescription =>
|
||||
'Установить валюту по умолчанию для транзакций';
|
||||
|
||||
@override
|
||||
String get transactionsHistoryTitle => 'История транзакций';
|
||||
|
||||
@override
|
||||
String get balance => 'Баланс';
|
||||
|
||||
@override
|
||||
String get income => 'Доходы';
|
||||
|
||||
@override
|
||||
String get expense => 'Расходы';
|
||||
|
||||
@override
|
||||
String get amount => 'Сумма';
|
||||
|
||||
@override
|
||||
String get vendor => 'Название';
|
||||
|
||||
@override
|
||||
String get category => 'Категория';
|
||||
|
||||
@override
|
||||
String get date => 'Дата';
|
||||
|
||||
@override
|
||||
String get requiredField => 'Обязательное поле';
|
||||
|
||||
@override
|
||||
String get invalidNumber => 'Неверный формат числа';
|
||||
|
||||
@override
|
||||
String get cancel => 'Отмена';
|
||||
|
||||
@override
|
||||
String get save => 'Сохранить';
|
||||
|
||||
@override
|
||||
String get tag => 'Тег';
|
||||
|
||||
@override
|
||||
String get icon => 'Иконка';
|
||||
|
||||
@override
|
||||
String get smsPageTitle => 'SMS Сообщения';
|
||||
|
||||
@override
|
||||
String get smsPermissionDenied => 'Необходимо разрешение на чтение SMS';
|
||||
|
||||
@override
|
||||
String get editCategories => 'Редактировать категории';
|
||||
|
||||
@override
|
||||
String get editCategoriesDescription =>
|
||||
'Добавляйте, редактируйте или удаляйте категории';
|
||||
|
||||
@override
|
||||
String get color => 'Цвет';
|
||||
|
||||
@override
|
||||
String get chooseIcon => 'Выберите иконку';
|
||||
|
||||
@override
|
||||
String get chooseIconHint => 'Поиск по анлгийскому наименованию';
|
||||
|
||||
@override
|
||||
String get categoryType => 'Тип категории';
|
||||
|
||||
@override
|
||||
String get addCategory => 'Добавить категорию';
|
||||
|
||||
@override
|
||||
String get editTags => 'Редактировать теги';
|
||||
|
||||
@override
|
||||
String get editTagsDescription =>
|
||||
'Добавляйте, редактируйте или удаляйте теги';
|
||||
|
||||
@override
|
||||
String get addTag => 'Добавить тег';
|
||||
|
||||
@override
|
||||
String get editTag => 'Редактировать тег';
|
||||
|
||||
@override
|
||||
String get loadSmsMessages => 'Загрузить SMS';
|
||||
|
||||
@override
|
||||
String get loadSmsMessagesDescription =>
|
||||
'Загрузить и обработать SMS-сообщения для автоматического создания транзакций';
|
||||
|
||||
@override
|
||||
String get smsSettings => 'Настройка обработки';
|
||||
|
||||
@override
|
||||
String get createTransaction => 'Создать транзакцию';
|
||||
|
||||
@override
|
||||
String get unknownSender => 'Неизвестный отправитель';
|
||||
|
||||
@override
|
||||
String get smsProcessed => 'Обработано';
|
||||
|
||||
@override
|
||||
String get smsNotProcessed => 'Не обработано';
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"@@locale": "ru",
|
||||
"appTitle": "Бюджетное приложение",
|
||||
"homePageTitle": "Главная",
|
||||
"reportsPageTitle": "Отчеты",
|
||||
"settingsPageTitle": "Настройки",
|
||||
"addTransactionButton": "Добавить новую транзакцию",
|
||||
"noTransactionsText": "Нет транзакций",
|
||||
"loadingTransactionsText": "Загрузка транзакций...",
|
||||
"transactionErrorText": "Ошибка: {message}",
|
||||
"loginPageTitle": "Вход",
|
||||
"nameFieldLabel": "Имя",
|
||||
"emailFieldLabel": "Email",
|
||||
"nameFieldEmptyError": "Пожалуйста, введите имя",
|
||||
"emailFieldEmptyError": "Пожалуйста, введите email",
|
||||
"loginButtonText": "Войти / Зарегистрироваться",
|
||||
"darkModeSetting": "Темная тема",
|
||||
"darkModeDescription": "Переключить между светлой и темной темой",
|
||||
"languageSetting": "Язык",
|
||||
"languageDescription": "Изменить язык приложения",
|
||||
"russianLanguage": "Русский",
|
||||
"englishLanguage": "Английский",
|
||||
"defaultUser": "Пользователь по умолчанию",
|
||||
"currencySetting": "Валюта по умолчанию",
|
||||
"currencyDescription": "Установить валюту по умолчанию для транзакций",
|
||||
"transactionsHistoryTitle": "История транзакций",
|
||||
"balance": "Баланс",
|
||||
"income": "Доходы",
|
||||
"expense": "Расходы",
|
||||
"amount": "Сумма",
|
||||
"vendor": "Название",
|
||||
"category": "Категория",
|
||||
"date": "Дата",
|
||||
"requiredField": "Обязательное поле",
|
||||
"invalidNumber": "Неверный формат числа",
|
||||
"cancel": "Отмена",
|
||||
"save": "Сохранить",
|
||||
"tag": "Тег",
|
||||
"icon": "Иконка",
|
||||
"smsPageTitle": "SMS Сообщения",
|
||||
"smsPermissionDenied": "Необходимо разрешение на чтение SMS",
|
||||
"editCategories": "Редактировать категории",
|
||||
"editCategoriesDescription": "Добавляйте, редактируйте или удаляйте категории",
|
||||
"color": "Цвет",
|
||||
"chooseIcon": "Выберите иконку",
|
||||
"chooseIconHint": "Поиск по анлгийскому наименованию",
|
||||
"categoryType": "Тип категории",
|
||||
"addCategory": "Добавить категорию",
|
||||
"editTags": "Редактировать теги",
|
||||
"editTagsDescription": "Добавляйте, редактируйте или удаляйте теги",
|
||||
"addTag": "Добавить тег",
|
||||
"editTag": "Редактировать тег",
|
||||
"loadSmsMessages": "Загрузить SMS",
|
||||
"loadSmsMessagesDescription": "Загрузить и обработать SMS-сообщения для автоматического создания транзакций",
|
||||
"smsSettings": "Настройка обработки",
|
||||
"createTransaction": "Создать транзакцию",
|
||||
"unknownSender": "Неизвестный отправитель",
|
||||
"smsProcessed": "Обработано",
|
||||
"smsNotProcessed": "Не обработано"
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:budget_app/models/user.dart';
|
||||
import 'package:budget_app/logic/user/user_cubit.dart';
|
||||
import 'package:budget_app/injection_container.dart' as di;
|
||||
import 'package:budget_app/data/repositories/interfaces/iglobal_settings_repository.dart';
|
||||
import 'package:budget_app/data/repositories/interfaces/iuser_repository.dart';
|
||||
|
||||
part 'auth_event.dart';
|
||||
part 'auth_state.dart';
|
||||
|
||||
class AuthBloc extends Bloc<AuthEvent, AuthState> {
|
||||
final UserCubit _userCubit;
|
||||
final IGlobalSettingsRepository _settingsRepository;
|
||||
final IUserRepository _userRepository;
|
||||
|
||||
AuthBloc({
|
||||
required UserCubit userCubit,
|
||||
required IGlobalSettingsRepository settingsRepository,
|
||||
required IUserRepository userRepository,
|
||||
}) : _userCubit = userCubit,
|
||||
_settingsRepository = settingsRepository,
|
||||
_userRepository = userRepository,
|
||||
super(AuthInitial()) {
|
||||
on<AuthStarted>(_onAuthStarted);
|
||||
on<AuthLoggedIn>(_onAuthLoggedIn);
|
||||
on<AuthLoggedOut>(_onAuthLoggedOut);
|
||||
on<AuthRegisterRequested>(_onAuthRegisterRequested);
|
||||
}
|
||||
|
||||
void _onAuthStarted(AuthStarted event, Emitter<AuthState> emit) async {
|
||||
try {
|
||||
final userId = await _settingsRepository.getCurrentUserId();
|
||||
if (userId != null) {
|
||||
final user = await _userRepository.getById(userId);
|
||||
if (user != null) {
|
||||
// Пользователь найден, инициализируем зависимости и аутентифицируем
|
||||
await di.initUserSpecificDependencies(user.id);
|
||||
_userCubit.setUser(user);
|
||||
emit(AuthAuthenticated(user: user));
|
||||
} else {
|
||||
// ID есть, а пользователя нет (ошибка) -> сбрасываем
|
||||
await _settingsRepository.setCurrentUserId(null);
|
||||
emit(AuthUnauthenticated());
|
||||
}
|
||||
} else {
|
||||
// ID не найден, пользователь не аутентифицирован
|
||||
emit(AuthUnauthenticated());
|
||||
}
|
||||
} catch (e) {
|
||||
emit(AuthError(e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
void _onAuthLoggedIn(AuthLoggedIn event, Emitter<AuthState> emit) async {
|
||||
// Инициализируем зависимости для вошедшего пользователя.
|
||||
await di.initUserSpecificDependencies(event.user.id);
|
||||
_userCubit.setUser(event.user);
|
||||
await _settingsRepository.setCurrentUserId(event.user.id);
|
||||
emit(AuthAuthenticated(user: event.user));
|
||||
}
|
||||
|
||||
void _onAuthLoggedOut(AuthLoggedOut event, Emitter<AuthState> emit) async {
|
||||
// Сбрасываем пользовательские зависимости.
|
||||
await di.resetUserSpecificDependencies();
|
||||
await _settingsRepository.setCurrentUserId(null);
|
||||
_userCubit.logout();
|
||||
emit(AuthUnauthenticated());
|
||||
}
|
||||
|
||||
Future<void> _onAuthRegisterRequested(
|
||||
AuthRegisterRequested event, Emitter<AuthState> emit) async {
|
||||
emit(AuthLoading());
|
||||
try {
|
||||
// 1. Создаем пользователя
|
||||
final user = User(name: event.name, email: event.email);
|
||||
await _userRepository.add(user);
|
||||
|
||||
// 2. Инициализируем его зависимости
|
||||
await di.initUserSpecificDependencies(user.id);
|
||||
|
||||
// 3. Создаем начальные данные (теперь это сработает)
|
||||
await _userCubit.createInitialData(user.id);
|
||||
|
||||
// 4. Сохраняем и устанавливаем пользователя
|
||||
await _settingsRepository.setCurrentUserId(user.id);
|
||||
_userCubit.setUser(user);
|
||||
emit(AuthAuthenticated(user: user));
|
||||
} catch (e) {
|
||||
emit(AuthError(e.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
part of 'auth_bloc.dart';
|
||||
|
||||
abstract class AuthEvent extends Equatable {
|
||||
const AuthEvent();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class AuthStarted extends AuthEvent {}
|
||||
|
||||
class AuthLoggedIn extends AuthEvent {
|
||||
final User user;
|
||||
|
||||
const AuthLoggedIn({required this.user});
|
||||
|
||||
@override
|
||||
List<Object> get props => [user];
|
||||
}
|
||||
|
||||
class AuthLoggedOut extends AuthEvent {}
|
||||
|
||||
class AuthRegisterRequested extends AuthEvent {
|
||||
final String name;
|
||||
final String email;
|
||||
|
||||
const AuthRegisterRequested({required this.name, required this.email});
|
||||
|
||||
@override
|
||||
List<Object> get props => [name, email];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
part of 'auth_bloc.dart';
|
||||
|
||||
abstract class AuthState extends Equatable {
|
||||
const AuthState();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class AuthInitial extends AuthState {}
|
||||
|
||||
class AuthLoading extends AuthState {}
|
||||
|
||||
class AuthAuthenticated extends AuthState {
|
||||
final User user;
|
||||
|
||||
const AuthAuthenticated({required this.user});
|
||||
|
||||
@override
|
||||
List<Object> get props => [user];
|
||||
}
|
||||
|
||||
class AuthUnauthenticated extends AuthState {}
|
||||
|
||||
class AuthError extends AuthState {
|
||||
final String message;
|
||||
|
||||
const AuthError(this.message);
|
||||
|
||||
@override
|
||||
List<Object> get props => [message];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
|
||||
import 'package:equatable/equatable.dart'; // equatable для сравнения объектов
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../data/repositories/interfaces/icategory_repository.dart';
|
||||
import '../../models/category.dart';
|
||||
|
||||
part 'category_state.dart'; // Используем part для разделения файла
|
||||
|
||||
/// Cubit для управления категориями
|
||||
/// Изменения:
|
||||
/// - Добавлено хранение userId в cubit
|
||||
/// - Упрощена работа с состояниями по аналогии с TagCubit
|
||||
class CategoryCubit extends Cubit<CategoryState> {
|
||||
final ICategoryRepository repository;
|
||||
|
||||
CategoryCubit(this.repository) : super(CategoryInitial());
|
||||
|
||||
/// Загружает категории для текущего пользователя
|
||||
Future<void> loadCategories() async {
|
||||
emit(CategoryLoading());
|
||||
try {
|
||||
final categories = await repository.getAll();
|
||||
emit(CategoryLoaded(categories));
|
||||
} catch (e) {
|
||||
emit(CategoryError('Ошибка загрузки категорий: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
/// Добавляет новую категорию
|
||||
Future<void> addCategory(Category category) async {
|
||||
try {
|
||||
// Устанавливаем userId для категории из cubit
|
||||
final newCategory = category.copyWith();
|
||||
await repository.add(newCategory);
|
||||
// Перезагружаем список категорий
|
||||
await loadCategories();
|
||||
} catch (e) {
|
||||
emit(CategoryError('Ошибка добавления категории: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
/// Обновляет существующую категорию
|
||||
Future<void> updateCategory(Category category) async {
|
||||
try {
|
||||
await repository.update(category);
|
||||
await loadCategories();
|
||||
} catch (e) {
|
||||
emit(CategoryError('Ошибка обновления категории: $e'));
|
||||
}
|
||||
}
|
||||
|
||||
/// Удаляет категорию по её id
|
||||
Future<void> deleteCategory(String id) async {
|
||||
try {
|
||||
await repository.delete(id);
|
||||
await loadCategories();
|
||||
} catch (e) {
|
||||
emit(CategoryError('Ошибка удаления категории: $e'));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
part of 'category_cubit.dart';
|
||||
|
||||
abstract class CategoryState extends Equatable {
|
||||
const CategoryState();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class CategoryInitial extends CategoryState {}
|
||||
|
||||
class CategoryLoading extends CategoryState {}
|
||||
|
||||
class CategoryLoaded extends CategoryState {
|
||||
final List<Category> categories;
|
||||
|
||||
const CategoryLoaded(this.categories);
|
||||
|
||||
@override
|
||||
List<Object> get props => [categories];
|
||||
}
|
||||
|
||||
class CategoryError extends CategoryState {
|
||||
final String message;
|
||||
|
||||
const CategoryError(this.message);
|
||||
|
||||
@override
|
||||
List<Object> get props => [message];
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import 'dart:async';
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.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 ISettingsRepository _settingsRepository;
|
||||
|
||||
SettingsCubit(this._settingsRepository) : super(SettingsInitial());
|
||||
|
||||
Future<void> loadSettings() async {
|
||||
emit(SettingsLoading());
|
||||
try {
|
||||
final settings = await _settingsRepository.getSettings();
|
||||
emit(SettingsLoaded(
|
||||
isDarkMode: settings.isDarkMode,
|
||||
languageCode: settings.languageCode,
|
||||
defaultCurrency: settings.defaultCurrency,
|
||||
));
|
||||
} catch (e) {
|
||||
emit(SettingsError(e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> toggleDarkMode(bool value) async {
|
||||
if (state is SettingsLoaded) {
|
||||
try {
|
||||
final currentState = state as SettingsLoaded;
|
||||
final newState = currentState.copyWith(isDarkMode: value);
|
||||
emit(newState);
|
||||
await _saveSettings(newState);
|
||||
} catch (e) {
|
||||
emit(SettingsError(e.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> changeLanguage(String languageCode) async {
|
||||
if (state is SettingsLoaded) {
|
||||
try {
|
||||
final currentState = state as SettingsLoaded;
|
||||
final newState = currentState.copyWith(languageCode: languageCode);
|
||||
emit(newState);
|
||||
await _saveSettings(newState);
|
||||
} catch (e) {
|
||||
emit(SettingsError(e.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> changeCurrency(String currency) async {
|
||||
if (state is SettingsLoaded) {
|
||||
try {
|
||||
final currentState = state as SettingsLoaded;
|
||||
final newState = currentState.copyWith(defaultCurrency: currency);
|
||||
emit(newState);
|
||||
await _saveSettings(newState);
|
||||
} catch (e) {
|
||||
emit(SettingsError(e.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _saveSettings(SettingsLoaded settings) async {
|
||||
final appSettings = AppSettings(
|
||||
isDarkMode: settings.isDarkMode,
|
||||
languageCode: settings.languageCode,
|
||||
defaultCurrency: settings.defaultCurrency,
|
||||
);
|
||||
await _settingsRepository.saveSettings(appSettings);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
part of 'settings_cubit.dart';
|
||||
|
||||
@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;
|
||||
|
||||
const SettingsLoaded({
|
||||
required this.isDarkMode,
|
||||
required this.languageCode,
|
||||
required this.defaultCurrency,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object> get props => [isDarkMode, languageCode, defaultCurrency];
|
||||
|
||||
SettingsLoaded copyWith({
|
||||
bool? isDarkMode,
|
||||
String? languageCode,
|
||||
String? defaultCurrency,
|
||||
}) {
|
||||
return SettingsLoaded(
|
||||
isDarkMode: isDarkMode ?? this.isDarkMode,
|
||||
languageCode: languageCode ?? this.languageCode,
|
||||
defaultCurrency: defaultCurrency ?? this.defaultCurrency,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SettingsError extends SettingsState {
|
||||
final String message;
|
||||
|
||||
const SettingsError(this.message);
|
||||
|
||||
@override
|
||||
List<Object> get props => [message];
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
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/logic/user/user_cubit.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:budget_app/data/repositories/interfaces/isms_message_repository.dart';
|
||||
|
||||
/// Cubit для управления состоянием SMS.
|
||||
///
|
||||
/// Отвечает за загрузку SMS сообщений и обработку разрешений.
|
||||
class SmsCubit extends Cubit<SmsState> {
|
||||
final SmsService _smsService;
|
||||
final ISmsMessageRepository _smsRepository;
|
||||
final UserCubit _userCubit;
|
||||
|
||||
SmsCubit(
|
||||
this._smsService,
|
||||
this._smsRepository,
|
||||
this._userCubit
|
||||
) : super(SmsInitial());
|
||||
|
||||
/// Загружает последние 10 SMS сообщений.
|
||||
///
|
||||
/// Перед загрузкой запрашивает необходимые разрешения.
|
||||
/// В случае успеха, переходит в состояние [SmsLoaded].
|
||||
/// В случае отказа в разрешениях, переходит в состояние [SmsPermissionDenied].
|
||||
/// В случае ошибки, переходит в состояние [SmsError].
|
||||
Future<void> loadLastMessages() async {
|
||||
emit(SmsLoading());
|
||||
try {
|
||||
final hasPermissions = await _smsService.requestPermissions();
|
||||
if (hasPermissions) {
|
||||
final messages = await _smsService.getLastSmsMessages(10);
|
||||
emit(SmsLoaded(messages));
|
||||
} else {
|
||||
emit(SmsPermissionDenied());
|
||||
}
|
||||
} catch (e) {
|
||||
emit(SmsError(e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
// Комментарий: Метод для загрузки и сохранения SMS-сообщений.
|
||||
Future<void> loadSmsMessages() async {
|
||||
// Комментарий: Устанавливаем состояние загрузки, чтобы UI мог отобразить индикатор.
|
||||
emit(SmsLoading());
|
||||
try {
|
||||
// Комментарий: Запрашиваем разрешение на чтение SMS.
|
||||
final hasPermissions = await _smsService.requestPermissions();
|
||||
if (hasPermissions) {
|
||||
// Комментарий: Получаем текущего пользователя из состояния UserCubit.
|
||||
final userState = _userCubit.state;
|
||||
if (userState is UserLoaded) {
|
||||
final user = userState.user;
|
||||
|
||||
// Комментарий: Получаем все SMS-сообщения с момента последней синхронизации.
|
||||
final messages = await _smsService.getSmsMessagesSince(user!.lastSmsSyncTime);
|
||||
|
||||
// Комментарий: Сохраняем новые сообщения через репозиторий и создаем транзакции.
|
||||
await _smsRepository.addAll(messages);
|
||||
for (final message in messages) {
|
||||
// Комментарий: Пытаемся создать транзакцию из SMS.
|
||||
_createTransactionFromSms(message);
|
||||
}
|
||||
|
||||
// Комментарий: Устанавливаем состояние успешной загрузки.
|
||||
emit(SmsLoaded(messages));
|
||||
} else {
|
||||
emit(SmsError("User not loaded"));
|
||||
}
|
||||
} else {
|
||||
// Комментарий: Если разрешение не получено, устанавливаем состояние "в доступе отказано".
|
||||
emit(SmsPermissionDenied());
|
||||
}
|
||||
} catch (e) {
|
||||
// Комментарий: В случае ошибки устанавливаем состояние ошибки и передаем сообщение.
|
||||
emit(SmsError(e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
// Комментарий: Метод для создания транзакции из SMS-сообщения.
|
||||
Future<void> _createTransactionFromSms(SmsMessage sms) async {
|
||||
// Комментарий: Здесь будет логика для парсинга SMS и создания транзакции.
|
||||
// Пример простой логики парсинга (нужно будет доработать под реальные SMS).
|
||||
final body = sms.body?.toLowerCase() ?? '';
|
||||
double? amount;
|
||||
TransactionRecord? type;
|
||||
|
||||
// Комментарий: Поиск суммы в сообщении.
|
||||
final amountRegex = RegExp(r'(\d+(\.\d{1,2})?)');
|
||||
final match = amountRegex.firstMatch(body);
|
||||
if (match != null) {
|
||||
amount = double.tryParse(match.group(1)!);
|
||||
}
|
||||
|
||||
// TODO
|
||||
// Комментарий: Определение типа транзакции (доход/расход).
|
||||
// if (body.contains('покупка') || body.contains('списание')) {
|
||||
// type = Transaction.expense;
|
||||
// } else if (body.contains('зачисление') || body.contains('пополнение')) {
|
||||
// type = TransactionType.income;
|
||||
// }
|
||||
|
||||
// if (amount != null && type != null) {
|
||||
// // Комментарий: Создаем новую транзакцию.
|
||||
// final transaction = TransactionRecord(
|
||||
// amount: amount,
|
||||
// type: type,
|
||||
// date: sms.date ?? DateTime.now(),
|
||||
// description: sms.body, // Описание берем из тела SMS
|
||||
// // Комментарий: Здесь можно добавить логику для определения категории и тегов.
|
||||
// );
|
||||
// // Комментарий: Добавляем событие AddTransaction в TransactionBloc.
|
||||
// _transactionBloc.add(AddTransaction(transaction));
|
||||
// // Комментарий: Сохраняем ID транзакции в SMS-сообщении.
|
||||
// await _smsRepository.update(
|
||||
// sms.copyWith(transactionId: transaction.id)
|
||||
// );
|
||||
// }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import 'package:budget_app/models/sms_message.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Абстрактный класс для состояний SMS.
|
||||
abstract class SmsState extends Equatable {
|
||||
const SmsState();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
/// Начальное состояние.
|
||||
class SmsInitial extends SmsState {}
|
||||
|
||||
/// Состояние загрузки SMS.
|
||||
class SmsLoading extends SmsState {}
|
||||
|
||||
/// Состояние, когда SMS успешно загружены.
|
||||
class SmsLoaded extends SmsState {
|
||||
final List<SmsMessage> messages;
|
||||
|
||||
const SmsLoaded(this.messages);
|
||||
|
||||
@override
|
||||
List<Object> get props => [messages];
|
||||
}
|
||||
|
||||
/// Состояние, когда отказано в разрешении на чтение SMS.
|
||||
class SmsPermissionDenied extends SmsState {}
|
||||
|
||||
/// Состояние ошибки при загрузке SMS.
|
||||
class SmsError extends SmsState {
|
||||
final String message;
|
||||
|
||||
const SmsError(this.message);
|
||||
|
||||
@override
|
||||
List<Object> get props => [message];
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:budget_app/data/repositories/interfaces/itag_repository.dart';
|
||||
import 'package:budget_app/logic/tag/tag_state.dart';
|
||||
import 'package:budget_app/models/tag.dart'; // Добавляем импорт модели Tag
|
||||
|
||||
class TagCubit extends Cubit<TagState> {
|
||||
final ITagRepository _repository;
|
||||
|
||||
TagCubit(this._repository) : super(TagInitial());
|
||||
|
||||
// Загрузка тегов
|
||||
Future<void> loadTags() async {
|
||||
emit(TagLoading());
|
||||
try {
|
||||
final tags = await _repository.getAll();
|
||||
emit(TagLoaded(tags));
|
||||
} catch (e) {
|
||||
emit(TagError('Ошибка загрузки тегов: ${e.toString()}'));
|
||||
}
|
||||
}
|
||||
|
||||
// Добавление тега
|
||||
Future<void> addTag(Tag tag) async {
|
||||
try {
|
||||
await _repository.add(tag);
|
||||
await loadTags(); // Перезагружаем список
|
||||
} catch (e) {
|
||||
emit(TagError('Ошибка добавления тега: ${e.toString()}'));
|
||||
}
|
||||
}
|
||||
|
||||
// Обновление тега
|
||||
Future<void> updateTag(Tag tag) async {
|
||||
try {
|
||||
await _repository.update(tag);
|
||||
await loadTags(); // Перезагружаем список
|
||||
} catch (e) {
|
||||
emit(TagError('Ошибка обновления тега: ${e.toString()}'));
|
||||
}
|
||||
}
|
||||
|
||||
// Удаление тега
|
||||
Future<void> deleteTag(String id) async {
|
||||
try {
|
||||
await _repository.delete(id);
|
||||
await loadTags(); // Перезагружаем список
|
||||
} catch (e) {
|
||||
emit(TagError('Ошибка удаления тега: ${e.toString()}'));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import 'package:budget_app/models/tag.dart';
|
||||
|
||||
// Состояния для TagCubit
|
||||
abstract class TagState {}
|
||||
|
||||
class TagInitial extends TagState {}
|
||||
|
||||
class TagLoading extends TagState {}
|
||||
|
||||
class TagLoaded extends TagState {
|
||||
final List<Tag> tags;
|
||||
TagLoaded(this.tags);
|
||||
}
|
||||
|
||||
class TagError extends TagState {
|
||||
final String message;
|
||||
TagError(this.message);
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:budget_app/data/repositories/interfaces/itransaction_repository.dart';
|
||||
import 'package:budget_app/models/transaction_record.dart';
|
||||
|
||||
part 'transaction_event.dart';
|
||||
part 'transaction_state.dart';
|
||||
|
||||
class TransactionBloc extends Bloc<TransactionEvent, TransactionState> {
|
||||
final ITransactionRepository _transactionRepository;
|
||||
|
||||
TransactionBloc({required ITransactionRepository transactionRepository})
|
||||
: _transactionRepository = transactionRepository,
|
||||
super(TransactionInitial()) {
|
||||
on<LoadTransactions>(_onLoadTransactions);
|
||||
on<AddTransaction>(_onAddTransaction);
|
||||
on<UpdateTransaction>(_onUpdateTransaction);
|
||||
on<DeleteTransaction>(_onDeleteTransaction);
|
||||
}
|
||||
|
||||
void _onLoadTransactions(LoadTransactions event, Emitter<TransactionState> emit) async {
|
||||
emit(TransactionLoading());
|
||||
try {
|
||||
final transactions = await _transactionRepository.getAll();
|
||||
emit(TransactionLoaded(transactions: transactions));
|
||||
} catch (e) {
|
||||
emit(TransactionError(message: e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
void _onAddTransaction(AddTransaction event, Emitter<TransactionState> emit) async {
|
||||
try {
|
||||
await _transactionRepository.add(event.transaction);
|
||||
final transactions = await _transactionRepository.getAll();
|
||||
emit(TransactionLoaded(transactions: transactions));
|
||||
} catch (e) {
|
||||
emit(TransactionError(message: e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
void _onUpdateTransaction(UpdateTransaction event, Emitter<TransactionState> emit) async {
|
||||
try {
|
||||
await _transactionRepository.update(event.transaction);
|
||||
final transactions = await _transactionRepository.getAll();
|
||||
emit(TransactionLoaded(transactions: transactions));
|
||||
} catch (e) {
|
||||
emit(TransactionError(message: e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
void _onDeleteTransaction(DeleteTransaction event, Emitter<TransactionState> emit) async {
|
||||
try {
|
||||
// Для удаления нам нужен userId, который мы можем получить из текущего состояния
|
||||
if (state is TransactionLoaded) {
|
||||
final loadedState = state as TransactionLoaded;
|
||||
if (loadedState.transactions.isNotEmpty) {
|
||||
await _transactionRepository.delete(event.transactionId);
|
||||
final transactions = await _transactionRepository.getAll();
|
||||
emit(TransactionLoaded(transactions: transactions));
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
emit(TransactionError(message: e.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
part of 'transaction_bloc.dart';
|
||||
|
||||
abstract class TransactionEvent extends Equatable {
|
||||
const TransactionEvent();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class LoadTransactions extends TransactionEvent {
|
||||
final String userId;
|
||||
|
||||
const LoadTransactions({required this.userId});
|
||||
|
||||
@override
|
||||
List<Object> get props => [userId];
|
||||
}
|
||||
|
||||
class AddTransaction extends TransactionEvent {
|
||||
final TransactionRecord transaction;
|
||||
|
||||
const AddTransaction({required this.transaction});
|
||||
|
||||
@override
|
||||
List<Object> get props => [transaction];
|
||||
}
|
||||
|
||||
class UpdateTransaction extends TransactionEvent {
|
||||
final TransactionRecord transaction;
|
||||
|
||||
const UpdateTransaction({required this.transaction});
|
||||
|
||||
@override
|
||||
List<Object> get props => [transaction];
|
||||
}
|
||||
|
||||
class DeleteTransaction extends TransactionEvent {
|
||||
final String transactionId;
|
||||
|
||||
const DeleteTransaction({required this.transactionId});
|
||||
|
||||
@override
|
||||
List<Object> get props => [transactionId];
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
part of 'transaction_bloc.dart';
|
||||
|
||||
abstract class TransactionState extends Equatable {
|
||||
const TransactionState();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class TransactionInitial extends TransactionState {}
|
||||
|
||||
class TransactionLoading extends TransactionState {}
|
||||
|
||||
class TransactionLoaded extends TransactionState {
|
||||
final List<TransactionRecord> transactions;
|
||||
|
||||
const TransactionLoaded({this.transactions = const []});
|
||||
|
||||
@override
|
||||
List<Object> get props => [transactions];
|
||||
}
|
||||
|
||||
class TransactionError extends TransactionState {
|
||||
final String message;
|
||||
|
||||
const TransactionError({required this.message});
|
||||
|
||||
@override
|
||||
List<Object> get props => [message];
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:logger/logger.dart';
|
||||
|
||||
import '/data/repositories/interfaces/icategory_repository.dart';
|
||||
import '/data/repositories/interfaces/iglobal_settings_repository.dart';
|
||||
import '/data/repositories/interfaces/itag_repository.dart';
|
||||
import '/data/repositories/interfaces/itransaction_repository.dart';
|
||||
import '/data/repositories/interfaces/iuser_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;
|
||||
// Эти репозитории теперь могут быть null и устанавливаются после входа пользователя.
|
||||
ICategoryRepository? _categoryRepository;
|
||||
ITagRepository? _tagRepository;
|
||||
ITransactionRepository? _transactionRepository;
|
||||
final Logger _logger = Logger(
|
||||
printer: PrettyPrinter(
|
||||
methodCount: 2, // Number of method calls to be displayed
|
||||
errorMethodCount: 8, // Number of method calls if stacktrace is provided
|
||||
lineLength: 120, // Width of the output
|
||||
colors: true, // Colorful log messages
|
||||
printEmojis: true, // Print an emoji for each log message
|
||||
// Should each log print contain a timestamp
|
||||
dateTimeFormat: DateTimeFormat.onlyTimeAndSinceStart,
|
||||
),
|
||||
);
|
||||
|
||||
UserCubit({
|
||||
required IGlobalSettingsRepository settingsRepository,
|
||||
required IUserRepository userRepository,
|
||||
// Репозитории сделаны опциональными в конструкторе.
|
||||
ICategoryRepository? categoryRepository,
|
||||
ITagRepository? tagRepository,
|
||||
ITransactionRepository? transactionRepository,
|
||||
}) : _settingsRepository = settingsRepository,
|
||||
_userRepository = userRepository,
|
||||
_categoryRepository = categoryRepository,
|
||||
_tagRepository = tagRepository,
|
||||
_transactionRepository = transactionRepository,
|
||||
super(UserInitial());
|
||||
|
||||
// Сеттеры для внедрения зависимостей после аутентификации.
|
||||
set categoryRepository(ICategoryRepository? repo) => _categoryRepository = repo;
|
||||
set tagRepository(ITagRepository? repo) => _tagRepository = repo;
|
||||
set transactionRepository(ITransactionRepository? repo) =>
|
||||
_transactionRepository = repo;
|
||||
|
||||
Future<void> createInitialData(String userId) async {
|
||||
// Проверяем, что репозитории были установлены, прежде чем их использовать.
|
||||
if (_categoryRepository == null ||
|
||||
_tagRepository == null ||
|
||||
_transactionRepository == null) {
|
||||
_logger.w('User-specific repositories are not initialized. Skipping initial data creation.');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// Разбиваем создание данных на этапы
|
||||
emit(UserLoading(progress: 0.7, message: 'Создание категорий...'));
|
||||
// Используем '!', так как мы уже проверили на null.
|
||||
await _categoryRepository!.addAll(
|
||||
CategoryUtils.getDefaultCategories(),
|
||||
);
|
||||
|
||||
emit(UserLoading(progress: 0.8, message: 'Создание тегов...'));
|
||||
await _tagRepository!.addAll(TagUtils.getDefaultTags());
|
||||
|
||||
emit(UserLoading(progress: 0.9, message: 'Создание транзакций...'));
|
||||
await _transactionRepository!.addAll(
|
||||
TransactionUtils.getSampleTransactions(),
|
||||
);
|
||||
} catch (e, stack) {
|
||||
|
||||
_logger.e(
|
||||
'Error creating initial data for user: $userId',
|
||||
error: e,
|
||||
stackTrace: stack,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Устанавливает текущего пользователя (прямая установка без загрузки)
|
||||
void setUser(User user) {
|
||||
emit(UserLoaded(user));
|
||||
}
|
||||
|
||||
/// Выход из системы
|
||||
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<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,37 @@
|
||||
part of 'user_cubit.dart';
|
||||
|
||||
/// Состояния управления пользователем
|
||||
@immutable
|
||||
abstract class UserState {
|
||||
const UserState();
|
||||
}
|
||||
|
||||
/// Начальное состояние (инициализация)
|
||||
class UserInitial extends UserState {
|
||||
const UserInitial();
|
||||
}
|
||||
|
||||
/// Состояние загрузки данных с прогрессом и сообщением
|
||||
class UserLoading extends UserState {
|
||||
final double progress;
|
||||
final String message;
|
||||
|
||||
const UserLoading({
|
||||
this.progress = 0.0,
|
||||
this.message = '',
|
||||
});
|
||||
}
|
||||
|
||||
/// Состояние успешной загрузки пользователя
|
||||
class UserLoaded extends UserState {
|
||||
final User? user;
|
||||
|
||||
const UserLoaded(this.user);
|
||||
}
|
||||
|
||||
/// Состояние ошибки
|
||||
class UserError extends UserState {
|
||||
final String message;
|
||||
|
||||
const UserError(this.message);
|
||||
}
|
||||
+87
-42
@@ -1,57 +1,102 @@
|
||||
import 'package:budget_app/pages/home/home_page.dart';
|
||||
import 'package:flutter/material.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:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart'; // Добавляем импорт
|
||||
import 'package:get_it/get_it.dart';
|
||||
|
||||
import '/l10n/app_localizations.dart';
|
||||
import '/pages/splash/splash_screen.dart';
|
||||
import 'injection_container.dart' as di;
|
||||
import 'logic/auth/auth_bloc.dart';
|
||||
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
|
||||
import 'pages/login/login_page.dart';
|
||||
import 'theme/app_theme.dart';
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await di.initDependencies();
|
||||
await di.initGlobalDependencies();
|
||||
runApp(const MyApp());
|
||||
}
|
||||
|
||||
/// Главный виджет приложения
|
||||
///
|
||||
/// Управляет:
|
||||
/// - Состоянием темы (темная/светлая)
|
||||
/// - Конфигурацией MaterialApp
|
||||
class MyApp extends StatefulWidget {
|
||||
class MyApp extends StatelessWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
@override
|
||||
State<MyApp> createState() => _MyAppState();
|
||||
}
|
||||
|
||||
class _MyAppState extends State<MyApp> {
|
||||
late final SettingsService _settingsService;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_settingsService = GetIt.instance<SettingsService>();
|
||||
_settingsService.addListener(_onThemeChanged);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_settingsService.removeListener(_onThemeChanged);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onThemeChanged() {
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'Budget App',
|
||||
theme: AppTheme.lightTheme(),
|
||||
darkTheme: AppTheme.darkTheme(),
|
||||
themeMode: _settingsService.isDarkMode ? ThemeMode.dark : ThemeMode.light,
|
||||
home: const HomePage(),
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider(
|
||||
create: (context) => GetIt.instance<UserCubit>(),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => GetIt.instance<AuthBloc>()..add(AuthStarted()),
|
||||
),
|
||||
],
|
||||
child: BlocBuilder<AuthBloc, AuthState>(
|
||||
builder: (context, authState) {
|
||||
if (authState is AuthAuthenticated) {
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider(create: (context) => GetIt.instance<SettingsCubit>()),
|
||||
BlocProvider(create: (context) => GetIt.instance<TransactionBloc>()),
|
||||
BlocProvider(create: (context) => GetIt.instance<SmsCubit>()),
|
||||
],
|
||||
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: 'Budget App',
|
||||
theme: AppTheme.lightTheme(),
|
||||
darkTheme: AppTheme.darkTheme(),
|
||||
themeMode: isDarkMode ? ThemeMode.dark : ThemeMode.light,
|
||||
localizationsDelegates: const [
|
||||
AppLocalizations.delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
],
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
locale: Locale(languageCode),
|
||||
home: const HomePage(),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget homeWidget;
|
||||
if (authState is AuthInitial) {
|
||||
homeWidget = const SplashScreen();
|
||||
} else {
|
||||
homeWidget = const LoginPage();
|
||||
}
|
||||
|
||||
return MaterialApp(
|
||||
title: 'Budget App',
|
||||
theme: AppTheme.lightTheme(),
|
||||
darkTheme: AppTheme.darkTheme(),
|
||||
themeMode: ThemeMode.light, // Тема по умолчанию
|
||||
localizationsDelegates: const [
|
||||
AppLocalizations.delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
],
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
locale: const Locale('ru'), // Язык по умолчанию
|
||||
home: homeWidget,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:hive_ce/hive.dart';
|
||||
import '../utils/id_generator.dart';
|
||||
|
||||
part 'app_settings.g.dart';
|
||||
|
||||
@HiveType(typeId: 1005)
|
||||
class AppSettings extends Equatable {
|
||||
@HiveField(0)
|
||||
final String id;
|
||||
|
||||
/// Код языка интерфейса (например, 'ru', 'en')
|
||||
@HiveField(1)
|
||||
final String languageCode;
|
||||
|
||||
/// Режим темной темы
|
||||
@HiveField(2)
|
||||
final bool isDarkMode;
|
||||
|
||||
/// Валюта по умолчанию
|
||||
@HiveField(3)
|
||||
final String defaultCurrency;
|
||||
|
||||
/// Дата последнего обновления настроек
|
||||
@HiveField(4)
|
||||
final DateTime updatedAt;
|
||||
|
||||
AppSettings({
|
||||
String? id,
|
||||
this.languageCode = 'ru',
|
||||
this.isDarkMode = false,
|
||||
this.defaultCurrency = 'RUB',
|
||||
DateTime? updatedAt,
|
||||
}) : id = id ?? IdGenerator.generateId(),
|
||||
updatedAt = updatedAt ?? DateTime.now();
|
||||
|
||||
/// Преобразование в Map для сохранения
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'languageCode': languageCode,
|
||||
'isDarkMode': isDarkMode,
|
||||
'defaultCurrency': defaultCurrency,
|
||||
'updatedAt': updatedAt.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
/// Создание из Map
|
||||
factory AppSettings.fromMap(Map<String, dynamic> map) {
|
||||
return AppSettings(
|
||||
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(
|
||||
languageCode: languageCode ?? this.languageCode,
|
||||
isDarkMode: isDarkMode ?? this.isDarkMode,
|
||||
defaultCurrency: defaultCurrency ?? this.defaultCurrency,
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object> get props => [id];
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AppSettings(languageCode: $languageCode, '
|
||||
'isDarkMode: $isDarkMode, defaultCurrency: $defaultCurrency)';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// 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(
|
||||
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(4)
|
||||
..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;
|
||||
}
|
||||
+35
-10
@@ -1,3 +1,4 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hive_ce/hive.dart';
|
||||
import '../utils/id_generator.dart';
|
||||
@@ -8,7 +9,7 @@ part 'category.g.dart';
|
||||
|
||||
/// Модель категории для группировки транзакций
|
||||
/// Содержит основные параметры для визуализации и классификации
|
||||
class Category {
|
||||
class Category extends Equatable {
|
||||
/// Уникальный идентификатор категории
|
||||
@HiveField(0)
|
||||
final String id;
|
||||
@@ -31,10 +32,9 @@ class Category {
|
||||
/// false - расход (например покупки)
|
||||
final bool isIncome;
|
||||
|
||||
@HiveField(5)
|
||||
/// Идентификатор пользователя, которому принадлежит эта категория
|
||||
/// Это позволяет разделять категории между разными пользователями
|
||||
final String userId;
|
||||
@HiveField(6)
|
||||
/// Дата и время последнего обновления объекта
|
||||
final DateTime updatedAt;
|
||||
|
||||
/// Конструктор с обязательными параметрами
|
||||
Category({
|
||||
@@ -43,18 +43,19 @@ class Category {
|
||||
required this.color,
|
||||
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() {
|
||||
return {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'color': color.value, // Сохраняем только значение цвета
|
||||
'color': color.toARGB32(), // Сохраняем только значение цвета
|
||||
'icon': icon.codePoint,
|
||||
'isIncome': isIncome,
|
||||
'userId': userId, // Добавляем userId в Map
|
||||
'updatedAt': updatedAt.toIso8601String(), // Добавлено updatedAt в Map
|
||||
};
|
||||
}
|
||||
|
||||
@@ -66,7 +67,31 @@ class Category {
|
||||
color: Color(map['color']),
|
||||
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,
|
||||
updatedAt: DateTime.now(), // Обновляем updatedAt при каждом копировании
|
||||
);
|
||||
}
|
||||
|
||||
// Используем Equatable для сравнения объектов по их свойствам.
|
||||
// В данном случае, мы считаем категории уникальными по их 'id'.
|
||||
@override
|
||||
List<Object?> get props => [id];
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ class CategoryAdapter extends TypeAdapter<Category> {
|
||||
color: fields[2] as Color,
|
||||
icon: fields[3] as IconData,
|
||||
isIncome: fields[4] as bool,
|
||||
userId: fields[5] as String,
|
||||
updatedAt: fields[6] as DateTime?,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -40,8 +40,8 @@ class CategoryAdapter extends TypeAdapter<Category> {
|
||||
..write(obj.icon)
|
||||
..writeByte(4)
|
||||
..write(obj.isIncome)
|
||||
..writeByte(5)
|
||||
..write(obj.userId);
|
||||
..writeByte(6)
|
||||
..write(obj.updatedAt);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
|
||||
import 'package:hive_ce/hive.dart';
|
||||
|
||||
import '/utils/id_generator.dart';
|
||||
part 'sms_handler_settings.g.dart';
|
||||
|
||||
/// Перечисление для определения типа обработки СМС.
|
||||
@HiveType(typeId: 10)
|
||||
enum SmsProcessingType {
|
||||
/// Обработка с использованием регулярного выражения.
|
||||
@HiveField(0)
|
||||
regexp,
|
||||
|
||||
/// Обработка с использованием кастомной функции.
|
||||
@HiveField(1)
|
||||
customFunction,
|
||||
}
|
||||
|
||||
/// Модель для хранения правила обработки СМС от конкретного отправителя.
|
||||
@HiveType(typeId: 11)
|
||||
class SmsProcessingRule extends HiveObject {
|
||||
/// Тип обработки (regexp или кастомная функция).
|
||||
@HiveField(0)
|
||||
final SmsProcessingType type;
|
||||
|
||||
/// Шаблон регулярного выражения (используется, если type == SmsProcessingType.regexp).
|
||||
@HiveField(1)
|
||||
final String? pattern;
|
||||
|
||||
/// Идентификатор кастомной функции (используется, если type == SmsProcessingType.customFunction).
|
||||
/// В коде этот ID будет сопоставляться с реальной функцией.
|
||||
@HiveField(2)
|
||||
final String? customFunctionId;
|
||||
|
||||
@HiveField(3)
|
||||
final String id;
|
||||
|
||||
SmsProcessingRule({
|
||||
String? id,
|
||||
required this.type,
|
||||
this.pattern,
|
||||
this.customFunctionId,
|
||||
}) : assert(
|
||||
(type == SmsProcessingType.regexp && pattern != null) ||
|
||||
(type == SmsProcessingType.customFunction && customFunctionId != null),
|
||||
'Pattern must be provided for regexp type, and customFunctionId for customFunction type.',
|
||||
), id = id ?? IdGenerator.generateId();
|
||||
}
|
||||
|
||||
/// Модель для хранения всех настроек обработки СМС для одного пользователя.
|
||||
@HiveType(typeId: 12)
|
||||
class SmsHandlerSettings extends HiveObject {
|
||||
|
||||
@HiveField(0)
|
||||
final String id;
|
||||
/// Карта правил обработки, где ключ - это идентификатор отправителя (например, 'SBERBANK' или номер телефона),
|
||||
/// а значение - правило обработки для этого отправителя.
|
||||
@HiveField(1)
|
||||
final Map<String, SmsProcessingRule> rulesBySender;
|
||||
|
||||
SmsHandlerSettings({
|
||||
String? id,
|
||||
required this.rulesBySender,
|
||||
}) : id = id ?? IdGenerator.generateId();
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'sms_handler_settings.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class SmsProcessingRuleAdapter extends TypeAdapter<SmsProcessingRule> {
|
||||
@override
|
||||
final typeId = 11;
|
||||
|
||||
@override
|
||||
SmsProcessingRule read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return SmsProcessingRule(
|
||||
type: fields[0] as SmsProcessingType,
|
||||
pattern: fields[1] as String?,
|
||||
customFunctionId: fields[2] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, SmsProcessingRule obj) {
|
||||
writer
|
||||
..writeByte(3)
|
||||
..writeByte(0)
|
||||
..write(obj.type)
|
||||
..writeByte(1)
|
||||
..write(obj.pattern)
|
||||
..writeByte(2)
|
||||
..write(obj.customFunctionId);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is SmsProcessingRuleAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
|
||||
class SmsHandlerSettingsAdapter extends TypeAdapter<SmsHandlerSettings> {
|
||||
@override
|
||||
final typeId = 12;
|
||||
|
||||
@override
|
||||
SmsHandlerSettings read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return SmsHandlerSettings(
|
||||
rulesBySender: (fields[1] as Map).cast<String, SmsProcessingRule>(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, SmsHandlerSettings obj) {
|
||||
writer
|
||||
..writeByte(1)
|
||||
..writeByte(1)
|
||||
..write(obj.rulesBySender);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is SmsHandlerSettingsAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
|
||||
class SmsProcessingTypeAdapter extends TypeAdapter<SmsProcessingType> {
|
||||
@override
|
||||
final typeId = 10;
|
||||
|
||||
@override
|
||||
SmsProcessingType read(BinaryReader reader) {
|
||||
switch (reader.readByte()) {
|
||||
case 0:
|
||||
return SmsProcessingType.regexp;
|
||||
case 1:
|
||||
return SmsProcessingType.customFunction;
|
||||
default:
|
||||
return SmsProcessingType.regexp;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, SmsProcessingType obj) {
|
||||
switch (obj) {
|
||||
case SmsProcessingType.regexp:
|
||||
writer.writeByte(0);
|
||||
case SmsProcessingType.customFunction:
|
||||
writer.writeByte(1);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is SmsProcessingTypeAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import 'package:hive_ce/hive.dart';
|
||||
import '/utils/id_generator.dart';
|
||||
|
||||
part 'sms_message.g.dart';
|
||||
|
||||
@HiveType(typeId: 1004)
|
||||
class SmsMessage extends HiveObject {
|
||||
@HiveField(0)
|
||||
final String id;
|
||||
|
||||
@HiveField(1)
|
||||
final String? body;
|
||||
|
||||
@HiveField(2)
|
||||
final String? sender;
|
||||
|
||||
@HiveField(3)
|
||||
final DateTime? date;
|
||||
|
||||
// Комментарий: Добавлено поле для хранения идентификатора связанной транзакции.
|
||||
@HiveField(4)
|
||||
String? transactionId;
|
||||
|
||||
SmsMessage({
|
||||
String? id,
|
||||
this.body,
|
||||
this.sender,
|
||||
this.date,
|
||||
this.transactionId,
|
||||
}) : id = id ?? IdGenerator.generateId();
|
||||
|
||||
// Комментарий: Добавляем метод для обновления transactionId
|
||||
SmsMessage copyWith({String? transactionId, String? userId}) {
|
||||
return SmsMessage(
|
||||
id: id,
|
||||
body: body,
|
||||
sender: sender,
|
||||
date: date,
|
||||
transactionId: transactionId ?? this.transactionId,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'sms_message.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class SmsMessageAdapter extends TypeAdapter<SmsMessage> {
|
||||
@override
|
||||
final typeId = 1004;
|
||||
|
||||
@override
|
||||
SmsMessage read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return SmsMessage(
|
||||
id: fields[0] as String?,
|
||||
body: fields[1] as String?,
|
||||
sender: fields[2] as String?,
|
||||
date: fields[3] as DateTime?,
|
||||
transactionId: fields[4] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, SmsMessage obj) {
|
||||
writer
|
||||
..writeByte(5)
|
||||
..writeByte(0)
|
||||
..write(obj.id)
|
||||
..writeByte(1)
|
||||
..write(obj.body)
|
||||
..writeByte(2)
|
||||
..write(obj.sender)
|
||||
..writeByte(3)
|
||||
..write(obj.date)
|
||||
..writeByte(4)
|
||||
..write(obj.transactionId);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is SmsMessageAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
+29
-11
@@ -1,10 +1,11 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:hive_ce/hive.dart';
|
||||
import '../../utils/id_generator.dart';
|
||||
|
||||
part 'tag.g.dart';
|
||||
|
||||
@HiveType(typeId: 1001)
|
||||
class Tag {
|
||||
class Tag extends Equatable {
|
||||
@HiveField(0)
|
||||
/// Уникальный идентификатор тега
|
||||
final String id;
|
||||
@@ -13,11 +14,9 @@ class Tag {
|
||||
/// Название тега (например: "Важное", "Работа")
|
||||
final String name;
|
||||
|
||||
// Комментарий: Добавляем новое поле для хранения идентификатора пользователя.
|
||||
// Это позволит нам связать каждый тег с конкретным пользователем.
|
||||
// Мы используем аннотацию @HiveField(2), чтобы указать Hive, как сохранять это поле.
|
||||
@HiveField(2)
|
||||
final String userId;
|
||||
@HiveField(3)
|
||||
/// Дата и время последнего обновления объекта
|
||||
final DateTime updatedAt;
|
||||
|
||||
/// Конструктор с обязательными параметрами
|
||||
Tag({
|
||||
@@ -25,8 +24,9 @@ class Tag {
|
||||
required this.name,
|
||||
// Комментарий: Добавляем userId в конструктор как обязательный параметр.
|
||||
// Теперь при создании тега необходимо будет указать, какому пользователю он принадлежит.
|
||||
required this.userId,
|
||||
}) : id = id ?? IdGenerator.generateId();
|
||||
DateTime? updatedAt, // Добавлено поле updatedAt, теперь необязательное
|
||||
}) : id = id ?? IdGenerator.generateId(),
|
||||
updatedAt = updatedAt ?? DateTime.now(); // Устанавливаем текущее время по умолчанию
|
||||
|
||||
/// Преобразование объекта в Map
|
||||
Map<String, dynamic> toMap() {
|
||||
@@ -35,7 +35,7 @@ class Tag {
|
||||
'name': name,
|
||||
// Комментарий: Добавляем userId в Map. Это нужно для сохранения
|
||||
// данных в форматах, которые не работают напрямую с объектами Dart (например, при отправке на сервер).
|
||||
'userId': userId,
|
||||
'updatedAt': updatedAt.toIso8601String(), // Добавлено updatedAt в Map
|
||||
};
|
||||
}
|
||||
|
||||
@@ -45,8 +45,26 @@ class Tag {
|
||||
id: map['id'],
|
||||
name: map['name'],
|
||||
// Комментарий: Извлекаем userId из Map при создании объекта.
|
||||
// Это позволяет восстановить полный объект Tag из данных, например, из базы данных.
|
||||
userId: map['userId'],
|
||||
// Это позволит восстановить полный объект Tag из данных, например, из базы данных.
|
||||
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,
|
||||
updatedAt: DateTime.now(), // Обновляем updatedAt при каждом копировании
|
||||
);
|
||||
}
|
||||
|
||||
// Используем Equatable для сравнения объектов по их свойствам.
|
||||
// В данном случае, мы считаем теги уникальными по их 'id'.
|
||||
@override
|
||||
List<Object?> get props => [id];
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ class TagAdapter extends TypeAdapter<Tag> {
|
||||
return Tag(
|
||||
id: fields[0] as String?,
|
||||
name: fields[1] as String,
|
||||
userId: fields[2] as String,
|
||||
updatedAt: fields[3] as DateTime?,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -31,8 +31,8 @@ class TagAdapter extends TypeAdapter<Tag> {
|
||||
..write(obj.id)
|
||||
..writeByte(1)
|
||||
..write(obj.name)
|
||||
..writeByte(2)
|
||||
..write(obj.userId);
|
||||
..writeByte(3)
|
||||
..write(obj.updatedAt);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:hive_ce/hive.dart';
|
||||
|
||||
import '../../utils/id_generator.dart';
|
||||
import 'category.dart';
|
||||
import 'tag.dart';
|
||||
@@ -9,8 +10,7 @@ part 'transaction_record.g.dart';
|
||||
@HiveType(typeId: 1002)
|
||||
/// Модель записи о транзакции - основной элемент учета бюджета
|
||||
/// Содержит все детали финансовой операции
|
||||
class TransactionRecord {
|
||||
|
||||
class TransactionRecord extends Equatable {
|
||||
/// Уникальный идентификатор транзакции
|
||||
@HiveField(0)
|
||||
final String id;
|
||||
@@ -39,9 +39,9 @@ class TransactionRecord {
|
||||
/// Валюта операции (код валюты, например "RUB", "USD")
|
||||
final String currency;
|
||||
|
||||
// Добавляем новое поле для хранения идентификатора пользователя
|
||||
@HiveField(7) // Используем следующий доступный номер поля Hive
|
||||
final String userId;
|
||||
@HiveField(8)
|
||||
/// Дата и время последнего обновления объекта
|
||||
final DateTime updatedAt;
|
||||
|
||||
/// Конструктор с обязательными параметрами
|
||||
TransactionRecord({
|
||||
@@ -52,8 +52,11 @@ class TransactionRecord {
|
||||
required this.dateTime,
|
||||
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() {
|
||||
@@ -65,7 +68,7 @@ class TransactionRecord {
|
||||
'dateTime': dateTime.toIso8601String(),
|
||||
'vendor': vendor,
|
||||
'currency': currency,
|
||||
'userId': userId, // Добавляем userId в Map
|
||||
'updatedAt': updatedAt.toIso8601String(), // Добавлено updatedAt в Map
|
||||
};
|
||||
}
|
||||
|
||||
@@ -79,11 +82,41 @@ class TransactionRecord {
|
||||
dateTime: DateTime.parse(map['dateTime']),
|
||||
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,
|
||||
updatedAt: DateTime.now(), // Обновляем updatedAt при каждом копировании
|
||||
);
|
||||
}
|
||||
|
||||
// Используем Equatable для сравнения объектов по их свойствам.
|
||||
// В данном случае, мы считаем записи транзакций уникальными по их 'id'.
|
||||
@override
|
||||
List<Object?> get props => [id];
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ class TransactionRecordAdapter extends TypeAdapter<TransactionRecord> {
|
||||
dateTime: fields[4] as DateTime,
|
||||
vendor: fields[5] as String,
|
||||
currency: fields[6] as String,
|
||||
userId: fields[7] as String,
|
||||
updatedAt: fields[8] as DateTime?,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -46,8 +46,8 @@ class TransactionRecordAdapter extends TypeAdapter<TransactionRecord> {
|
||||
..write(obj.vendor)
|
||||
..writeByte(6)
|
||||
..write(obj.currency)
|
||||
..writeByte(7)
|
||||
..write(obj.userId);
|
||||
..writeByte(8)
|
||||
..write(obj.updatedAt);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
+43
-27
@@ -1,55 +1,71 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
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)
|
||||
/// Модель пользователя приложения
|
||||
/// Содержит основную информацию для идентификации пользователя
|
||||
class User {
|
||||
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)
|
||||
/// Дата и время последнего обновления объекта
|
||||
final DateTime updatedAt;
|
||||
|
||||
// Комментарий: Добавлено поле для хранения времени последней синхронизации SMS.
|
||||
@HiveField(4)
|
||||
final DateTime lastSmsSyncTime;
|
||||
|
||||
/// Конструктор пользователя
|
||||
/// id генерируется автоматически, если не передан
|
||||
User({
|
||||
String? id, // Опциональный параметр - если null, сгенерируется автоматически
|
||||
required this.name, // Обязательный параметр
|
||||
required this.email, // Обязательный параметр
|
||||
}) : id = id ?? IdGenerator.generateId(); // Если id не передан, генерируем новый
|
||||
String? id,
|
||||
required this.name,
|
||||
required this.email,
|
||||
DateTime? updatedAt,
|
||||
DateTime? lastSmsSyncTime,
|
||||
}) : id = id ?? IdGenerator.generateId(),
|
||||
updatedAt = updatedAt ?? DateTime.now(),
|
||||
lastSmsSyncTime = lastSmsSyncTime ??
|
||||
DateTime(DateTime.now().year, DateTime.now().month - 1, 1);
|
||||
|
||||
/// Преобразование объекта в Map для сохранения в JSON или передачи по сети
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'email': email,
|
||||
};
|
||||
}
|
||||
|
||||
/// Создание объекта User из Map (например, при загрузке из JSON)
|
||||
factory User.fromMap(Map<String, dynamic> map) {
|
||||
return User(
|
||||
id: map['id'],
|
||||
name: map['name'],
|
||||
email: map['email'],
|
||||
);
|
||||
}
|
||||
|
||||
/// Переопределяем toString для удобного отображения в логах
|
||||
@override
|
||||
String toString() {
|
||||
return 'User(id: $id, name: $name, email: $email)';
|
||||
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,
|
||||
DateTime? lastSmsSyncTime,
|
||||
}) {
|
||||
return User(
|
||||
id: id ?? this.id,
|
||||
name: name ?? this.name,
|
||||
email: email ?? this.email,
|
||||
updatedAt: DateTime.now(),
|
||||
lastSmsSyncTime: lastSmsSyncTime ?? this.lastSmsSyncTime,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,19 +20,25 @@ class UserAdapter extends TypeAdapter<User> {
|
||||
id: fields[0] as String?,
|
||||
name: fields[1] as String,
|
||||
email: fields[2] as String,
|
||||
updatedAt: fields[3] as DateTime?,
|
||||
lastSmsSyncTime: fields[4] as DateTime?,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, User obj) {
|
||||
writer
|
||||
..writeByte(3)
|
||||
..writeByte(5)
|
||||
..writeByte(0)
|
||||
..write(obj.id)
|
||||
..writeByte(1)
|
||||
..write(obj.name)
|
||||
..writeByte(2)
|
||||
..write(obj.email);
|
||||
..write(obj.email)
|
||||
..writeByte(3)
|
||||
..write(obj.updatedAt)
|
||||
..writeByte(4)
|
||||
..write(obj.lastSmsSyncTime);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
import 'package:budget_app/l10n/app_localizations.dart';
|
||||
import 'package:budget_app/models/category.dart';
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_colorpicker/flutter_colorpicker.dart';
|
||||
import 'package:flutter_iconpicker/Models/configuration.dart';
|
||||
import 'package:flutter_iconpicker/flutter_iconpicker.dart';
|
||||
|
||||
class CategoryEditPage extends StatefulWidget {
|
||||
final Category? category;
|
||||
final Function(String, Color, IconData, bool) onSave;
|
||||
|
||||
const CategoryEditPage({super.key, this.category, required this.onSave});
|
||||
|
||||
@override
|
||||
_CategoryEditPageState createState() => _CategoryEditPageState();
|
||||
}
|
||||
|
||||
class _CategoryEditPageState extends State<CategoryEditPage> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
late String _name;
|
||||
late Color _color;
|
||||
late IconData _icon;
|
||||
late bool _isIncome; // Добавляем состояние для типа категории (доход/расход)
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_name = widget.category?.name ?? '';
|
||||
// Генерация случайного цвета из всей палитры для новой категории
|
||||
_color = widget.category?.color ?? Color((Random().nextDouble() * 0xFFFFFF).toInt()).withOpacity(1.0);
|
||||
// Генерация случайной иконки из предопределённого набора для новой категории
|
||||
_icon = widget.category?.icon ?? [
|
||||
Icons.attach_money,
|
||||
Icons.shopping_cart,
|
||||
Icons.food_bank,
|
||||
Icons.home,
|
||||
Icons.directions_car,
|
||||
Icons.medical_services,
|
||||
Icons.school,
|
||||
Icons.work,
|
||||
Icons.credit_card,
|
||||
Icons.savings,
|
||||
][Random().nextInt(10)];
|
||||
_isIncome = widget.category?.isIncome ?? false; // Инициализируем значение типа категории
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final localizations = AppLocalizations.of(context)!;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
widget.category == null
|
||||
? localizations.addCategory
|
||||
: localizations.editCategories,
|
||||
),
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
initialValue: _name,
|
||||
decoration: InputDecoration(
|
||||
labelText: localizations.nameFieldLabel,
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return localizations.nameFieldEmptyError;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onSaved: (value) {
|
||||
_name = value!;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
children: [
|
||||
Text(localizations.color),
|
||||
const SizedBox(width: 10),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(localizations.color),
|
||||
content: SingleChildScrollView(
|
||||
child: ColorPicker(
|
||||
pickerColor: _color,
|
||||
onColorChanged: (color) {
|
||||
setState(() {
|
||||
_color = color;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(localizations.save),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
child: CircleAvatar(backgroundColor: _color),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
children: [
|
||||
Text(localizations.icon),
|
||||
const SizedBox(width: 10),
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
final icon = await showIconPicker(
|
||||
context,
|
||||
configuration: SinglePickerConfiguration(
|
||||
adaptiveDialog: true,
|
||||
showTooltips: true,
|
||||
showSearchBar: true,
|
||||
preSelected: IconPickerIcon(
|
||||
name: '',
|
||||
data: _icon,
|
||||
pack: IconPack.material,
|
||||
),
|
||||
title: Text(
|
||||
localizations.chooseIcon,
|
||||
textScaler: const TextScaler.linear(1.25),
|
||||
),
|
||||
searchHintText: localizations.chooseIconHint,
|
||||
iconPickerShape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
),
|
||||
//iconPackModes: IconNotifier.starterPacks,
|
||||
searchComparator:
|
||||
(String search, IconPickerIcon icon) =>
|
||||
search.toLowerCase().contains(
|
||||
icon.name
|
||||
.replaceAll('_', ' ')
|
||||
.toLowerCase(),
|
||||
) ||
|
||||
icon.name.toLowerCase().contains(
|
||||
search.toLowerCase(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (icon != null) {
|
||||
setState(() {
|
||||
_icon = icon.data;
|
||||
});
|
||||
}
|
||||
},
|
||||
child: Icon(_icon),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
// Добавляем выбор типа категории (доход/расход)
|
||||
Row(
|
||||
children: [
|
||||
Text(localizations.categoryType),
|
||||
const SizedBox(width: 10),
|
||||
Switch(
|
||||
value: _isIncome,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_isIncome = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(_isIncome ? localizations.income : localizations.expense),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
_formKey.currentState!.save();
|
||||
// Добавляем параметр _isIncome в вызов onSave
|
||||
widget.onSave(_name, _color, _icon, _isIncome);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
child: Text(localizations.save),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
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/add_category_button.dart';
|
||||
import 'package:budget_app/pages/category/widgets/category_list_item.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
|
||||
class CategoryListPage extends StatefulWidget {
|
||||
const CategoryListPage({super.key});
|
||||
|
||||
@override
|
||||
State<CategoryListPage> createState() => _CategoryListPageState();
|
||||
}
|
||||
|
||||
class _CategoryListPageState extends State<CategoryListPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => GetIt.instance<CategoryCubit>()..loadCategories(),
|
||||
child: Builder(builder: (context) {
|
||||
final localizations = AppLocalizations.of(context)!;
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text(localizations.editCategories)),
|
||||
body: BlocBuilder<CategoryCubit, CategoryState>(
|
||||
builder: (context, state) {
|
||||
if (state is CategoryLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
} else if (state is CategoryError) {
|
||||
return Center(child: Text(state.message));
|
||||
} else if (state is CategoryLoaded) {
|
||||
return ListView.builder(
|
||||
itemCount: state.categories.length,
|
||||
itemBuilder: (context, index) {
|
||||
final category = state.categories[index];
|
||||
return CategoryListItem(
|
||||
category: category,
|
||||
onEdit: () => _editCategory(context, category),
|
||||
onDelete: () => _deleteCategory(context, category),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
// CategoryInitial
|
||||
return const Center(child: Text('Нет категорий'));
|
||||
},
|
||||
),
|
||||
floatingActionButton: AddCategoryButton(
|
||||
onPressed: () => _addCategory(context),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
void _addCategory(BuildContext context) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => CategoryEditPage(
|
||||
onSave: (name, color, icon, isIncome) {
|
||||
final newCategory = Category(
|
||||
name: name,
|
||||
color: color,
|
||||
icon: icon,
|
||||
isIncome: isIncome,
|
||||
);
|
||||
|
||||
context.read<CategoryCubit>().addCategory(newCategory);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _editCategory(BuildContext context, Category category) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => CategoryEditPage(
|
||||
category: category,
|
||||
onSave: (name, color, icon, isIncome) {
|
||||
final updatedCategory = category.copyWith(
|
||||
name: name,
|
||||
color: color,
|
||||
icon: icon,
|
||||
isIncome: isIncome,
|
||||
);
|
||||
|
||||
context.read<CategoryCubit>().updateCategory(updatedCategory);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _deleteCategory(BuildContext context, Category category) {
|
||||
context.read<CategoryCubit>().deleteCategory(category.id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Плавающая кнопка добавления категории с анимацией
|
||||
class AddCategoryButton extends StatelessWidget {
|
||||
final VoidCallback onPressed;
|
||||
|
||||
const AddCategoryButton({
|
||||
super.key,
|
||||
required this.onPressed,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FloatingActionButton(
|
||||
onPressed: onPressed,
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
foregroundColor: Theme.of(context).colorScheme.onPrimary,
|
||||
elevation: 4,
|
||||
child: const Icon(Icons.add),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:budget_app/theme/custom_colors.dart';
|
||||
|
||||
/// Виджет кнопок действий для категории (редактировать/удалить)
|
||||
class CategoryActions extends StatelessWidget {
|
||||
final VoidCallback onEdit;
|
||||
final VoidCallback onDelete;
|
||||
|
||||
const CategoryActions({
|
||||
super.key,
|
||||
required this.onEdit,
|
||||
required this.onDelete,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = Theme.of(context).extension<CustomColors>();
|
||||
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.edit, color: colors?.accent),
|
||||
onPressed: onEdit,
|
||||
splashRadius: 20,
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(Icons.delete, color: colors?.accent),
|
||||
onPressed: onDelete,
|
||||
splashRadius: 20,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:budget_app/models/category.dart';
|
||||
import 'package:budget_app/theme/custom_colors.dart';
|
||||
import './category_actions.dart';
|
||||
|
||||
/// Виджет элемента списка категорий с улучшенным дизайном
|
||||
class CategoryListItem extends StatelessWidget {
|
||||
final Category category;
|
||||
final VoidCallback onEdit;
|
||||
final VoidCallback onDelete;
|
||||
|
||||
const CategoryListItem({
|
||||
super.key,
|
||||
required this.category,
|
||||
required this.onEdit,
|
||||
required this.onDelete,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colors = theme.extension<CustomColors>();
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Row(
|
||||
children: [
|
||||
// Аватар категории
|
||||
CircleAvatar(
|
||||
backgroundColor: category.color.withOpacity(0.2),
|
||||
radius: 24,
|
||||
child: Icon(
|
||||
category.icon,
|
||||
color: colors?.accent,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// Информация о категории
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
category.name,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
category.isIncome ? 'Доход' : 'Расход',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: colors?.unselectedIcon,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Кнопки действий
|
||||
CategoryActions(
|
||||
onEdit: onEdit,
|
||||
onDelete: onDelete,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../theme/custom_colors.dart';
|
||||
|
||||
import '/l10n/app_localizations.dart';
|
||||
import '../../logic/settings/settings_cubit.dart';
|
||||
import '../../logic/transaction/transaction_bloc.dart';
|
||||
import '../../logic/user/user_cubit.dart';
|
||||
import '../home/widgets/summary_widget.dart'; // Импортируем новый виджет сводки
|
||||
import '../reports_page.dart'; // Импортируем новую страницу отчетов
|
||||
import '../settings_page.dart';
|
||||
import '../sms/sms_page.dart';
|
||||
import 'widgets/add_transaction_dialog.dart';
|
||||
import 'widgets/transaction_item.dart'; // Импортируем новый виджет для элемента транзакции
|
||||
|
||||
class HomePage extends StatefulWidget {
|
||||
const HomePage({super.key});
|
||||
|
||||
@override
|
||||
State<HomePage> createState() => _HomePageState();
|
||||
}
|
||||
|
||||
class _HomePageState extends State<HomePage> {
|
||||
int _selectedIndex = 0; // Индекс выбранной вкладки
|
||||
late String _currentUserId; // ID текущего пользователя
|
||||
|
||||
// Список виджетов для каждой вкладки нижней навигации
|
||||
static final List<Widget> _widgetOptions = <Widget>[
|
||||
const TransactionsPage(), // Главная страница с транзакциями
|
||||
const ReportsPage(), // Страница отчетов
|
||||
const SmsPage(), // Страница SMS
|
||||
const SettingsPage(), // Страница настроек
|
||||
];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Получаем 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));
|
||||
}
|
||||
}
|
||||
|
||||
void _onItemTapped(int index) {
|
||||
setState(() {
|
||||
_selectedIndex = index;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final localizations = AppLocalizations.of(
|
||||
context,
|
||||
)!; // Получаем экземпляр локализации
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(localizations.appTitle), // Локализованный заголовок
|
||||
// Кнопки действий в AppBar теперь не нужны, так как настройки перенесены в BottomNavigationBar
|
||||
),
|
||||
body: Center(
|
||||
child: _widgetOptions[_selectedIndex], // Отображаем выбранный виджет
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return const AddTransactionDialog();
|
||||
},
|
||||
);
|
||||
},
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
bottomNavigationBar: BottomNavigationBar(
|
||||
items: <BottomNavigationBarItem>[
|
||||
BottomNavigationBarItem(
|
||||
icon: const Icon(Icons.home),
|
||||
label: localizations.homePageTitle, // Локализованный текст
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: const Icon(Icons.bar_chart),
|
||||
label: localizations.reportsPageTitle, // Локализованный текст
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: const Icon(Icons.sms),
|
||||
label: localizations.smsPageTitle,
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: const Icon(Icons.settings),
|
||||
label: localizations.settingsPageTitle, // Локализованный текст
|
||||
),
|
||||
],
|
||||
currentIndex: _selectedIndex,
|
||||
selectedItemColor: Theme.of(context).colorScheme.primary,
|
||||
unselectedItemColor: Theme.of(context).extension<CustomColors>()?.unselectedIcon,
|
||||
onTap: _onItemTapped,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Изменение: TransactionsPage преобразован в StatefulWidget для управления состоянием выбранного месяца.
|
||||
class TransactionsPage extends StatefulWidget {
|
||||
const TransactionsPage({super.key});
|
||||
|
||||
@override
|
||||
State<TransactionsPage> createState() => _TransactionsPageState();
|
||||
}
|
||||
|
||||
class _TransactionsPageState extends State<TransactionsPage> {
|
||||
// Добавление: Состояние для хранения выбранного месяца на уровне страницы.
|
||||
late DateTime _selectedMonth;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Инициализация: Устанавливаем текущий месяц.
|
||||
_selectedMonth = DateTime.now();
|
||||
}
|
||||
|
||||
// Добавление: Метод для изменения месяца, который будет передаваться в SummaryWidget.
|
||||
void _onMonthChanged(DateTime newMonth) {
|
||||
setState(() {
|
||||
_selectedMonth = newMonth;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final localizations = AppLocalizations.of(context)!;
|
||||
|
||||
return BlocBuilder<TransactionBloc, TransactionState>(
|
||||
builder: (context, state) {
|
||||
if (state is TransactionLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
} else if (state is TransactionLoaded) {
|
||||
// Добавление: Фильтрация транзакций по выбранному месяцу.
|
||||
final monthlyTransactions = state.transactions.where((t) {
|
||||
return t.dateTime.month == _selectedMonth.month && t.dateTime.year == _selectedMonth.year;
|
||||
}).toList();
|
||||
|
||||
return ListView(
|
||||
children: [
|
||||
// Изменение: SummaryWidget теперь получает все транзакции,
|
||||
// выбранный месяц и колбэк для его изменения.
|
||||
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: 'RUB',
|
||||
);
|
||||
},
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Text(
|
||||
localizations.transactionsHistoryTitle,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
),
|
||||
// Изменение: Отображаем отфильтрованный список транзакций.
|
||||
if (monthlyTransactions.isEmpty)
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Text(localizations.noTransactionsText),
|
||||
),
|
||||
)
|
||||
else
|
||||
Card(
|
||||
margin: const EdgeInsets.all(16.0),
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
),
|
||||
// Изменение: Используем отфильтрованный список.
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: monthlyTransactions.length,
|
||||
itemBuilder: (context, index) {
|
||||
final transaction = monthlyTransactions[index];
|
||||
return TransactionItem(transaction: transaction);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
} else if (state is TransactionError) {
|
||||
return Center(child: Text(localizations.transactionErrorText(state.message)));
|
||||
} else {
|
||||
return Center(child: Text(localizations.loadingTransactionsText));
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
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 '../../../models/category.dart';
|
||||
import '../../../models/tag.dart';
|
||||
import '../../../models/transaction_record.dart';
|
||||
import '../../../utils/category_utils.dart';
|
||||
|
||||
class AddTransactionDialog extends StatefulWidget {
|
||||
const AddTransactionDialog({super.key});
|
||||
|
||||
@override
|
||||
State<AddTransactionDialog> createState() => _AddTransactionDialogState();
|
||||
}
|
||||
|
||||
class _AddTransactionDialogState extends State<AddTransactionDialog> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _amountController = TextEditingController();
|
||||
final _vendorController = TextEditingController();
|
||||
final _dateController = TextEditingController();
|
||||
|
||||
bool _isIncome = false;
|
||||
Category? _selectedCategory;
|
||||
// Комментарий: Добавляем состояние для выбранного тега.
|
||||
Tag? _selectedTag;
|
||||
// Комментарий: Заменяем _selectedDate на _selectedDateTime для хранения даты и времени.
|
||||
DateTime _selectedDateTime = DateTime.now();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Комментарий: Устанавливаем начальное значение с датой и временем.
|
||||
_dateController.text = DateFormat('dd-MM-yyyy').add_Hm().format(_selectedDateTime);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_amountController.dispose();
|
||||
_vendorController.dispose();
|
||||
_dateController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// Комментарий: Этот метод теперь обрабатывает выбор и даты, и времени.
|
||||
Future<void> _selectDateTime(BuildContext context) async {
|
||||
final DateTime? pickedDate = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: _selectedDateTime,
|
||||
firstDate: DateTime(2000),
|
||||
lastDate: DateTime(2101),
|
||||
);
|
||||
// Комментарий: Если пользователь не выбрал дату, выходим из функции.
|
||||
if (pickedDate == null) return;
|
||||
|
||||
// ignore: use_build_context_synchronously
|
||||
final TimeOfDay? pickedTime = await showTimePicker(
|
||||
context: context,
|
||||
initialTime: TimeOfDay.fromDateTime(_selectedDateTime),
|
||||
);
|
||||
// Комментарий: Если пользователь не выбрал время, выходим из функции.
|
||||
if (pickedTime == null) return;
|
||||
|
||||
// Комментарий: Обновляем состояние с новой датой и временем.
|
||||
setState(() {
|
||||
_selectedDateTime = DateTime(
|
||||
pickedDate.year,
|
||||
pickedDate.month,
|
||||
pickedDate.day,
|
||||
pickedTime.hour,
|
||||
pickedTime.minute,
|
||||
);
|
||||
// Комментарий: Обновляем текстовое поле с отформатированной датой и временем.
|
||||
_dateController.text = DateFormat('dd-MM-yyyy').add_Hm().format(_selectedDateTime);
|
||||
});
|
||||
}
|
||||
|
||||
void _submitForm() {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
final amount = double.tryParse(_amountController.text);
|
||||
if (amount == null || _selectedCategory == null) {
|
||||
// Показать ошибку, если сумма некорректна или категория не выбрана
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
AppLocalizations.of(
|
||||
context,
|
||||
)!.transactionErrorText('Invalid data'),
|
||||
),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final authState = context.read<AuthBloc>().state;
|
||||
if (authState is! AuthAuthenticated) {
|
||||
// Показать ошибку, если пользователь не аутентифицирован
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
AppLocalizations.of(
|
||||
context,
|
||||
)!.transactionErrorText('User not authenticated'),
|
||||
),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Получаем валюту из настроек
|
||||
final settingsState = context.read<SettingsCubit>().state;
|
||||
final currency = (settingsState is SettingsLoaded)
|
||||
? settingsState.defaultCurrency
|
||||
: 'RUB';
|
||||
|
||||
// Получаем ID текущего пользователя из UserCubit
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
AppLocalizations.of(context)!.transactionErrorText('User not found'),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final newTransaction = TransactionRecord(
|
||||
amount: amount,
|
||||
vendor: _vendorController.text,
|
||||
category: _selectedCategory!,
|
||||
dateTime: _selectedDateTime,
|
||||
tag: _selectedTag,
|
||||
currency: currency,
|
||||
);
|
||||
|
||||
context.read<TransactionBloc>().add(
|
||||
AddTransaction(transaction: newTransaction),
|
||||
);
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final localizations = AppLocalizations.of(context)!;
|
||||
|
||||
|
||||
final categories = CategoryUtils.getDefaultCategories()
|
||||
.where((c) => c.isIncome == _isIncome).toList();
|
||||
|
||||
return AlertDialog(
|
||||
title: Text(localizations.addTransactionButton),
|
||||
content: Form(
|
||||
key: _formKey,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SwitchListTile(
|
||||
title: Text(localizations.income),
|
||||
value: _isIncome,
|
||||
onChanged: (bool value) {
|
||||
setState(() {
|
||||
_isIncome = value;
|
||||
_selectedCategory =
|
||||
null; // Сбрасываем категорию при смене типа
|
||||
});
|
||||
},
|
||||
),
|
||||
TextFormField(
|
||||
controller: _amountController,
|
||||
decoration: InputDecoration(labelText: localizations.amount),
|
||||
// Комментарий: Устанавливаем числовую клавиатуру с поддержкой десятичных чисел.
|
||||
keyboardType:
|
||||
const TextInputType.numberWithOptions(decimal: true),
|
||||
// Комментарий: Добавляем фильтр для ввода только чисел и одной точки.
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d*')),
|
||||
],
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return localizations.requiredField;
|
||||
}
|
||||
if (double.tryParse(value) == null) {
|
||||
return localizations.invalidNumber;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
TextFormField(
|
||||
controller: _vendorController,
|
||||
decoration: InputDecoration(labelText: localizations.vendor),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return localizations.requiredField;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
DropdownButtonFormField<Category>(
|
||||
value: _selectedCategory,
|
||||
decoration: InputDecoration(labelText: localizations.category),
|
||||
items: categories.map((Category category) {
|
||||
return DropdownMenuItem<Category>(
|
||||
value: category,
|
||||
child: Text(category.name),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (Category? newValue) {
|
||||
setState(() {
|
||||
_selectedCategory = newValue;
|
||||
});
|
||||
},
|
||||
validator: (value) =>
|
||||
value == null ? localizations.requiredField : null,
|
||||
),
|
||||
// Комментарий: Добавляем выпадающий список для выбора тега.
|
||||
// Он будет загружать теги асинхронно для текущего пользователя.
|
||||
FutureBuilder<List<Tag>>(
|
||||
future: GetIt.instance<ITagRepository>().getAll(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
if (snapshot.hasError) {
|
||||
return Text('Error: ${snapshot.error}');
|
||||
}
|
||||
final tags = snapshot.data ?? [];
|
||||
return DropdownButtonFormField<Tag>(
|
||||
value: _selectedTag,
|
||||
decoration: InputDecoration(labelText: localizations.tag),
|
||||
items: tags.map((Tag tag) {
|
||||
return DropdownMenuItem<Tag>(
|
||||
value: tag,
|
||||
child: Text(tag.name),
|
||||
);
|
||||
}).toList(),
|
||||
onChanged: (Tag? newValue) {
|
||||
setState(() {
|
||||
_selectedTag = newValue;
|
||||
});
|
||||
},
|
||||
// Комментарий: Тег не является обязательным полем.
|
||||
);
|
||||
},
|
||||
),
|
||||
TextFormField(
|
||||
controller: _dateController,
|
||||
decoration: InputDecoration(
|
||||
labelText: localizations.date,
|
||||
suffixIcon: IconButton(
|
||||
icon: const Icon(Icons.calendar_today),
|
||||
// Комментарий: Вызываем новый метод для выбора даты и времени.
|
||||
onPressed: () => _selectDateTime(context),
|
||||
),
|
||||
),
|
||||
readOnly: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(localizations.cancel),
|
||||
),
|
||||
ElevatedButton(onPressed: _submitForm, child: Text(localizations.save)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
|
||||
import 'package:animated_digit/animated_digit.dart';
|
||||
import 'package:budget_app/l10n/app_localizations.dart';
|
||||
import 'package:budget_app/models/transaction_record.dart';
|
||||
import 'package:budget_app/theme/custom_colors.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
// Изменение: Виджет преобразован в StatefulWidget для управления состоянием выбранного месяца.
|
||||
class SummaryWidget extends StatefulWidget {
|
||||
// Изменение: Виджет теперь принимает список всех транзакций, а не готовые суммы.
|
||||
final List<TransactionRecord> transactions;
|
||||
final String currencySymbol;
|
||||
// Добавление: Принимает выбранный месяц и колбэк для его изменения от родителя.
|
||||
final DateTime selectedMonth;
|
||||
final ValueChanged<DateTime> onMonthChanged;
|
||||
|
||||
const SummaryWidget({
|
||||
super.key,
|
||||
required this.transactions,
|
||||
required this.currencySymbol,
|
||||
required this.selectedMonth,
|
||||
required this.onMonthChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
State<SummaryWidget> createState() => _SummaryWidgetState();
|
||||
}
|
||||
|
||||
class _SummaryWidgetState extends State<SummaryWidget> {
|
||||
// Добавление: PageController для управления PageView.
|
||||
late PageController _pageController;
|
||||
// Добавление: Хранение начального месяца для расчетов.
|
||||
late DateTime _initialMonth;
|
||||
// Добавление: Общее количество месяцев для отображения.
|
||||
int _monthCount = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Изменение: Находим самую раннюю транзакцию для определения начального месяца.
|
||||
if (widget.transactions.isNotEmpty) {
|
||||
widget.transactions.sort((a, b) => a.dateTime.compareTo(b.dateTime));
|
||||
_initialMonth = DateTime(widget.transactions.first.dateTime.year,
|
||||
widget.transactions.first.dateTime.month);
|
||||
} else {
|
||||
_initialMonth = DateTime(DateTime.now().year, DateTime.now().month);
|
||||
}
|
||||
|
||||
_monthCount = _calculateMonthDifference(DateTime.now(), _initialMonth) + 1;
|
||||
if (_monthCount < 1) _monthCount = 1; // Как минимум один месяц должен быть
|
||||
|
||||
// Инициализация PageController на последней странице (текущий месяц).
|
||||
_pageController = PageController(initialPage: _monthCount - 1);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pageController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// Добавление: Вспомогательный метод для расчета разницы в месяцах.
|
||||
int _calculateMonthDifference(DateTime d1, DateTime d2) {
|
||||
return (d1.year - d2.year) * 12 + d1.month - d2.month;
|
||||
}
|
||||
|
||||
// Добавление: Метод для форматирования названия месяца.
|
||||
// Отображает год, если он отличается от текущего.
|
||||
String _formatMonth(BuildContext context, DateTime date) {
|
||||
final localizations = AppLocalizations.of(context)!;
|
||||
final now = DateTime.now();
|
||||
final format =
|
||||
date.year == now.year ? DateFormat.MMMM(localizations.localeName) : DateFormat.yMMMM(localizations.localeName);
|
||||
return format.format(date);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
elevation: 8.0,
|
||||
margin: const EdgeInsets.all(16.0),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16.0),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias, // Обрезаем контент по границам карточки
|
||||
child: SizedBox(
|
||||
height: 300,
|
||||
// Изменение: Используем Column для разделения PageView и индикатора.
|
||||
child: Column(
|
||||
children: [
|
||||
// PageView будет занимать все доступное пространство.
|
||||
Expanded(
|
||||
child: PageView.builder(
|
||||
controller: _pageController,
|
||||
itemCount: _monthCount,
|
||||
onPageChanged: (index) {
|
||||
final newMonth = DateTime(
|
||||
_initialMonth.year,
|
||||
_initialMonth.month + index,
|
||||
1,
|
||||
);
|
||||
widget.onMonthChanged(newMonth);
|
||||
},
|
||||
itemBuilder: (context, index) {
|
||||
final month = DateTime(
|
||||
_initialMonth.year,
|
||||
_initialMonth.month + index,
|
||||
1,
|
||||
);
|
||||
return _buildPage(context, month);
|
||||
},
|
||||
),
|
||||
),
|
||||
// Добавление: Индикатор теперь находится вне PageView.
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child: _buildPageIndicator(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Добавление: Метод для построения одной страницы (одного месяца).
|
||||
Widget _buildPage(BuildContext context, DateTime month) {
|
||||
final localizations = AppLocalizations.of(context)!;
|
||||
final customColors = Theme.of(context).extension<CustomColors>()!;
|
||||
final theme = Theme.of(context);
|
||||
|
||||
final monthlyTransactions = widget.transactions.where((t) {
|
||||
return t.dateTime.month == month.month && t.dateTime.year == month.year;
|
||||
}).toList();
|
||||
|
||||
final income = monthlyTransactions
|
||||
.where((t) => t.isIncome)
|
||||
.fold(0.0, (sum, item) => sum + item.amount);
|
||||
final expense = monthlyTransactions
|
||||
.where((t) => !t.isIncome)
|
||||
.fold(0.0, (sum, item) => sum + item.amount);
|
||||
final balance = income - expense;
|
||||
|
||||
// Изменение: Уменьшены вертикальные отступы для предотвращения переполнения.
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 16.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
localizations.balance,
|
||||
style: theme.textTheme.titleLarge?.copyWith(
|
||||
color: customColors.accent,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
AnimatedDigitWidget(
|
||||
value: balance,
|
||||
fractionDigits: 2,
|
||||
textStyle: theme.textTheme.displaySmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
suffix: ' ${widget.currencySymbol}',
|
||||
),
|
||||
const SizedBox(height: 16.0),
|
||||
Divider(
|
||||
height: 1,
|
||||
thickness: 1,
|
||||
color: customColors.divider,
|
||||
),
|
||||
const SizedBox(height: 16.0),
|
||||
IntrinsicHeight(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
_buildIncomeExpense(
|
||||
context,
|
||||
localizations.income,
|
||||
income,
|
||||
customColors.income!,
|
||||
Icons.arrow_circle_up_outlined,
|
||||
),
|
||||
VerticalDivider(
|
||||
width: 1,
|
||||
thickness: 1,
|
||||
color: customColors.divider,
|
||||
),
|
||||
_buildIncomeExpense(
|
||||
context,
|
||||
localizations.expense,
|
||||
expense,
|
||||
customColors.expense!,
|
||||
Icons.arrow_circle_down_outlined,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Spacer(), // Занимает оставшееся место
|
||||
Text(
|
||||
_formatMonth(context, month),
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurface.withOpacity(0.7),
|
||||
),
|
||||
),
|
||||
// Удаление: Индикатор перенесен из страницы.
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Изменение: Индикатор теперь слушает PageController.
|
||||
Widget _buildPageIndicator(BuildContext context) {
|
||||
final customColors = Theme.of(context).extension<CustomColors>()!;
|
||||
return AnimatedBuilder(
|
||||
animation: _pageController,
|
||||
builder: (context, child) {
|
||||
// Проверяем, инициализирован ли контроллер
|
||||
final page = _pageController.hasClients ? _pageController.page ?? 0 : _monthCount - 1.0;
|
||||
final isFirstMonth = page < 0.5;
|
||||
final isLastMonth = page > _monthCount - 1.5;
|
||||
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// Анимированная точка "назад"
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeOut,
|
||||
width: isFirstMonth ? 0 : 8.0,
|
||||
height: 8.0,
|
||||
margin: EdgeInsets.symmetric(horizontal: isFirstMonth ? 0 : 4.0),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: customColors.divider,
|
||||
),
|
||||
),
|
||||
|
||||
// Активная точка
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeOut,
|
||||
width: 12.0,
|
||||
height: 12.0,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: customColors.accent,
|
||||
),
|
||||
),
|
||||
|
||||
// Анимированная точка "вперед"
|
||||
AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeOut,
|
||||
width: isLastMonth ? 0 : 8.0,
|
||||
height: 8.0,
|
||||
margin: EdgeInsets.symmetric(horizontal: isLastMonth ? 0 : 4.0),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: customColors.divider,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildIncomeExpense(
|
||||
BuildContext context,
|
||||
String title,
|
||||
double amount,
|
||||
Color color,
|
||||
IconData icon,
|
||||
) {
|
||||
final theme = Theme.of(context);
|
||||
return Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
color: color,
|
||||
size: 32.0,
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
Text(
|
||||
title,
|
||||
style: theme.textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 4.0),
|
||||
AnimatedDigitWidget(
|
||||
value: amount,
|
||||
fractionDigits: 2,
|
||||
textStyle: theme.textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import '../../../l10n/app_localizations.dart';
|
||||
import '../../../models/transaction_record.dart';
|
||||
|
||||
/// Виджет для отображения одной транзакции в списке.
|
||||
///
|
||||
/// Этот виджет представляет собой карточку с подробной информацией о транзакции,
|
||||
/// включая поставщика, сумму, категорию, тег и дату.
|
||||
class TransactionItem extends StatelessWidget {
|
||||
final TransactionRecord transaction;
|
||||
|
||||
const TransactionItem({super.key, required this.transaction});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final localizations = AppLocalizations.of(context)!;
|
||||
final theme = Theme.of(context);
|
||||
// Форматируем дату в соответствии с локалью
|
||||
final formattedDate = DateFormat.yMMMd(localizations.localeName).format(transaction.dateTime);
|
||||
|
||||
// Убираем Card, так как обертка будет в родительском виджете.
|
||||
// Добавляем разделитель и уменьшаем отступы для компактности.
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Верхняя строка: Название поставщика и сумма
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
// Название поставщика
|
||||
Expanded(
|
||||
child: Text(
|
||||
transaction.vendor,
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
// Сумма транзакции
|
||||
// Используем .abs() чтобы избежать двойного минуса для расходов
|
||||
Text(
|
||||
'${transaction.isIncome ? '+' : '-'}${transaction.amount.abs().toStringAsFixed(2)} ${transaction.currency}',
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
color: transaction.isIncome
|
||||
? theme.colorScheme.primary
|
||||
: theme.colorScheme.error,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
// Средняя строка: Категория и тег
|
||||
Row(
|
||||
children: [
|
||||
// Иконка категории
|
||||
Icon(
|
||||
transaction.category.icon,
|
||||
color: transaction.category.color,
|
||||
size: 20.0, // Уменьшаем размер иконки
|
||||
),
|
||||
const SizedBox(width: 8.0),
|
||||
// Название категории и тега
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
transaction.category.name,
|
||||
style: theme.textTheme.bodySmall,
|
||||
),
|
||||
if (transaction.tag != null)
|
||||
Text(
|
||||
'#${transaction.tag!.name}',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurface.withOpacity(0.6),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Дата в правом углу
|
||||
Text(
|
||||
formattedDate,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurface.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Разделитель между транзакциями
|
||||
const Divider(height: 1, thickness: 1, indent: 16, endIndent: 16),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'settings_page.dart';
|
||||
|
||||
/**
|
||||
* Главная страница приложения для управления бюджетом.
|
||||
*
|
||||
* Этот виджет является основным экраном приложения и содержит:
|
||||
* - AppBar с заголовком и кнопкой настроек
|
||||
* - Основной контент с приветственным сообщением
|
||||
*
|
||||
* Особенности реализации:
|
||||
* 1. Наследуется от StatelessWidget, так как не содержит собственного состояния
|
||||
* 2. Использует Material Design через Scaffold
|
||||
* 3. Управление темой вынесено в SettingsService
|
||||
*/
|
||||
class HomePage extends StatelessWidget {
|
||||
/// Конструктор виджета
|
||||
///
|
||||
/// @param key - опциональный ключ для идентификации виджета
|
||||
const HomePage({super.key});
|
||||
|
||||
/**
|
||||
* Основной метод построения интерфейса виджета.
|
||||
*
|
||||
* Возвращает Scaffold - базовую структуру страницы Material Design,
|
||||
* которая включает:
|
||||
* 1. AppBar (верхнюю панель)
|
||||
* 2. Body (основное содержимое)
|
||||
*/
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
// Верхняя панель приложения
|
||||
appBar: AppBar(
|
||||
// Заголовок приложения
|
||||
title: const Text('Budget App'),
|
||||
|
||||
// Кнопки в правой части AppBar
|
||||
actions: [
|
||||
// Кнопка настроек
|
||||
IconButton(
|
||||
icon: const Icon(Icons.settings),
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => const SettingsPage()),
|
||||
);
|
||||
},
|
||||
tooltip: 'Настройки',
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Основное содержимое страницы
|
||||
body: const Center(
|
||||
// Вертикальное расположение элементов
|
||||
child: Column(
|
||||
// Выравнивание по центру по вертикали
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
|
||||
// Дочерние виджеты колонки
|
||||
children: [
|
||||
// Иконка кошелька
|
||||
Icon(Icons.account_balance_wallet, size: 64),
|
||||
|
||||
// Отступ между элементами
|
||||
SizedBox(height: 16),
|
||||
|
||||
// Приветственный текст
|
||||
Text(
|
||||
'Добро пожаловать в Budget App!',
|
||||
style: TextStyle(fontSize: 24),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '/l10n/app_localizations.dart';
|
||||
import '../../logic/auth/auth_bloc.dart';
|
||||
|
||||
class LoginPage extends StatefulWidget {
|
||||
const LoginPage({super.key});
|
||||
|
||||
@override
|
||||
State<LoginPage> createState() => _LoginPageState();
|
||||
}
|
||||
|
||||
class _LoginPageState extends State<LoginPage> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _emailController = TextEditingController();
|
||||
final _nameController = TextEditingController();
|
||||
|
||||
void _login() {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
context.read<AuthBloc>().add(AuthRegisterRequested(
|
||||
name: _nameController.text,
|
||||
email: _emailController.text,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final localizations = AppLocalizations.of(context)!;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(localizations.loginPageTitle),
|
||||
),
|
||||
body: BlocListener<AuthBloc, AuthState>(
|
||||
listener: (context, state) {
|
||||
if (state is AuthError) {
|
||||
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;
|
||||
}
|
||||
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<AuthBloc, AuthState>(
|
||||
builder: (context, state) {
|
||||
final isLoading = state is AuthLoading;
|
||||
|
||||
return ElevatedButton(
|
||||
onPressed: isLoading ? null : _login,
|
||||
child: isLoading
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: Text(localizations.loginButtonText),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '/l10n/app_localizations.dart';
|
||||
|
||||
class ReportsPage extends StatelessWidget {
|
||||
const ReportsPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final localizations = AppLocalizations.of(context)!; // Получаем экземпляр локализации
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(localizations.reportsPageTitle), // Локализованный заголовок
|
||||
),
|
||||
body: Center(
|
||||
child: Text(localizations.reportsPageTitle), // Локализованный текст
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+166
-44
@@ -1,56 +1,178 @@
|
||||
import 'package:budget_app/logic/sms/sms_cubit.dart';
|
||||
import 'package:budget_app/pages/category/category_list_page.dart';
|
||||
import 'package:budget_app/pages/tag/tag_list_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import '../services/settings_service.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';
|
||||
|
||||
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)!;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Настройки'),
|
||||
title: Text(localizations.settingsPageTitle),
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SwitchListTile(
|
||||
title: const Text('Темная тема'),
|
||||
subtitle: const Text('Переключить между светлой и темной темой'),
|
||||
value: _settingsService.isDarkMode,
|
||||
onChanged: (value) async {
|
||||
await _settingsService.setDarkMode(value);
|
||||
},
|
||||
),
|
||||
const Divider(),
|
||||
// Здесь можно добавить другие настройки
|
||||
],
|
||||
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<UserCubit, UserState>(
|
||||
builder: (context, userState) {
|
||||
if (userState is UserLoaded && userState.user != null) {
|
||||
// Комментарий: Как только пользователь загружен, мы загружаем его настройки.
|
||||
context.read<SettingsCubit>().loadSettings();
|
||||
return BlocBuilder<SettingsCubit, SettingsState>(
|
||||
builder: (context, state) {
|
||||
if (state is SettingsLoaded) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
// Комментарий: Обернули Column в SingleChildScrollView, чтобы избежать переполнения по вертикали.
|
||||
// Это позволяет прокручивать содержимое, если оно не помещается на экране.
|
||||
child: SingleChildScrollView(
|
||||
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
|
||||
context
|
||||
.read<SettingsCubit>()
|
||||
.toggleDarkMode(value);
|
||||
},
|
||||
),
|
||||
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) {
|
||||
context
|
||||
.read<SettingsCubit>()
|
||||
.changeLanguage(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(
|
||||
title: Text(localizations.currencySetting),
|
||||
subtitle: Text(localizations.currencyDescription),
|
||||
trailing: DropdownButton<String>(
|
||||
value: state.defaultCurrency,
|
||||
// Комментарий: При выборе новой валюты вызываем метод `changeCurrency` у Cubit.
|
||||
// Также передаем userId из UserService.
|
||||
onChanged: (String? newValue) {
|
||||
if (newValue != null) {
|
||||
|
||||
context
|
||||
.read<SettingsCubit>()
|
||||
.changeCurrency(newValue);
|
||||
}
|
||||
},
|
||||
// Комментарий: Формируем список доступных валют. Можно расширить этот список.
|
||||
items: <String>['RUB', 'USD', 'EUR']
|
||||
.map<DropdownMenuItem<String>>(
|
||||
(String value) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: value,
|
||||
child: Text(value),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
// Комментарий: ListTile для перехода на страницу редактирования категорий.
|
||||
ListTile(
|
||||
title: Text(localizations.editCategories),
|
||||
subtitle:
|
||||
Text(localizations.editCategoriesDescription),
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const CategoryListPage(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const Divider(),
|
||||
ListTile(
|
||||
title: Text(localizations.editTags),
|
||||
subtitle: Text(localizations.editTagsDescription),
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const TagListPage(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const Divider(),
|
||||
// Комментарий: ListTile для запуска процесса загрузки SMS-сообщений.
|
||||
ListTile(
|
||||
title: Text(localizations.loadSmsMessages),
|
||||
subtitle:
|
||||
Text(localizations.loadSmsMessagesDescription),
|
||||
onTap: () {
|
||||
// Комментарий: При нажатии на кнопку мы вызываем метод `loadSmsMessages` у SmsCubit.
|
||||
// Это инициирует процесс получения и сохранения SMS-сообщений.
|
||||
context.read<SmsCubit>().loadSmsMessages();
|
||||
},
|
||||
),
|
||||
const Divider(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
},
|
||||
);
|
||||
} else {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:budget_app/logic/sms/sms_cubit.dart';
|
||||
import 'package:budget_app/logic/sms/sms_state.dart';
|
||||
import 'package:budget_app/pages/sms/widgets/sms_message_widget.dart';
|
||||
import 'package:budget_app/l10n/app_localizations.dart';
|
||||
|
||||
/// Экран для отображения SMS сообщений.
|
||||
///
|
||||
/// Использует [SmsCubit] для получения и отображения
|
||||
/// последних SMS сообщений.
|
||||
class SmsPage extends StatelessWidget {
|
||||
const SmsPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(AppLocalizations.of(context)!.smsPageTitle),
|
||||
),
|
||||
body: BlocBuilder<SmsCubit, SmsState>(
|
||||
builder: (context, state) {
|
||||
if (state is SmsInitial) {
|
||||
// Начальное состояние, запускаем загрузку
|
||||
context.read<SmsCubit>().loadLastMessages();
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
} else if (state is SmsLoading) {
|
||||
// Состояние загрузки
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
} else if (state is SmsLoaded) {
|
||||
// Состояние успешной загрузки
|
||||
return ListView.builder(
|
||||
itemCount: state.messages.length,
|
||||
itemBuilder: (context, index) {
|
||||
return SmsMessageWidget(message: state.messages[index]);
|
||||
},
|
||||
);
|
||||
} else if (state is SmsPermissionDenied) {
|
||||
// Состояние отказа в разрешении
|
||||
return Center(
|
||||
child: Text(AppLocalizations.of(context)!.smsPermissionDenied),
|
||||
);
|
||||
} else if (state is SmsError) {
|
||||
// Состояние ошибки
|
||||
return Center(
|
||||
child: Text(state.message),
|
||||
);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import 'package:budget_app/l10n/app_localizations.dart';
|
||||
import 'package:budget_app/models/sms_message.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
/// Виджет для отображения одного SMS сообщения с улучшенным интерфейсом.
|
||||
///
|
||||
/// Отображает отправителя, дату, тело сообщения и статус обработки.
|
||||
/// Подсвечивает суммы в тексте сообщения.
|
||||
class SmsMessageWidget extends StatelessWidget {
|
||||
final SmsMessage message;
|
||||
|
||||
const SmsMessageWidget({super.key, required this.message});
|
||||
|
||||
// Форматирование даты для отображения
|
||||
String _formatDate(BuildContext context, DateTime? date) {
|
||||
if (date == null) return '';
|
||||
final locale = Localizations.localeOf(context).toString();
|
||||
return DateFormat.yMd(locale).add_jm().format(date);
|
||||
}
|
||||
|
||||
// Функция для отображения всплывающего меню
|
||||
void _showPopupMenu(BuildContext context, TapDownDetails details) {
|
||||
final RenderBox overlay =
|
||||
Overlay.of(context).context.findRenderObject() as RenderBox;
|
||||
showMenu(
|
||||
context: context,
|
||||
position: RelativeRect.fromRect(
|
||||
details.globalPosition & const Size(40, 40),
|
||||
Offset.zero & overlay.size,
|
||||
),
|
||||
items: [
|
||||
PopupMenuItem(
|
||||
child: Text(AppLocalizations.of(context)!.smsSettings),
|
||||
onTap: () {
|
||||
// TODO: Реализовать переход к настройкам обработки SMS
|
||||
},
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Text(AppLocalizations.of(context)!.createTransaction),
|
||||
onTap: () {
|
||||
// TODO: Реализовать создание транзакции из SMS
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTapDown: (details) => _showPopupMenu(context, details),
|
||||
child: Card(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Заголовок: отправитель и дата
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.person_outline, size: 16),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
message.sender ?? AppLocalizations.of(context)!.unknownSender,
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Text(
|
||||
_formatDate(context, message.date),
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Тело сообщения с подсветкой сумм
|
||||
Text(
|
||||
message.body ?? '',
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Футер: статус обработки
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
message.transactionId != null
|
||||
? Icons.check_circle_outline
|
||||
: Icons.error_outline,
|
||||
size: 16,
|
||||
color: message.transactionId != null
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
message.transactionId != null
|
||||
? AppLocalizations.of(context)!.smsProcessed
|
||||
: AppLocalizations.of(context)!.smsNotProcessed,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SplashScreen extends StatelessWidget {
|
||||
const SplashScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Scaffold(
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CircularProgressIndicator(),
|
||||
SizedBox(height: 20),
|
||||
Text('Загрузка...'),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import 'package:budget_app/l10n/app_localizations.dart';
|
||||
import 'package:budget_app/models/tag.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class TagEditPage extends StatefulWidget {
|
||||
final Tag? tag;
|
||||
final Function(String) onSave;
|
||||
|
||||
const TagEditPage({super.key, this.tag, required this.onSave});
|
||||
|
||||
@override
|
||||
_TagEditPageState createState() => _TagEditPageState();
|
||||
}
|
||||
|
||||
class _TagEditPageState extends State<TagEditPage> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
late String _name;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_name = widget.tag?.name ?? '';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final localizations = AppLocalizations.of(context)!;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
widget.tag == null
|
||||
? localizations.addTag
|
||||
: localizations.editTag,
|
||||
),
|
||||
),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
children: [
|
||||
TextFormField(
|
||||
initialValue: _name,
|
||||
decoration: InputDecoration(
|
||||
labelText: localizations.nameFieldLabel,
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return localizations.nameFieldEmptyError;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onSaved: (value) {
|
||||
_name = value!;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
_formKey.currentState!.save();
|
||||
widget.onSave(_name);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
child: Text(localizations.save),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import 'package:budget_app/l10n/app_localizations.dart';
|
||||
import 'package:budget_app/logic/tag/tag_cubit.dart';
|
||||
import 'package:budget_app/logic/tag/tag_state.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/add_tag_button.dart';
|
||||
import 'package:budget_app/pages/tag/widgets/tag_list_item.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
|
||||
class TagListPage extends StatefulWidget {
|
||||
const TagListPage({super.key});
|
||||
|
||||
@override
|
||||
State<TagListPage> createState() => _TagListPageState();
|
||||
}
|
||||
|
||||
class _TagListPageState extends State<TagListPage> {
|
||||
final GlobalKey<AnimatedListState> _listKey = GlobalKey();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => GetIt.instance<TagCubit>()..loadTags(),
|
||||
child: Builder(builder: (context) {
|
||||
final localizations = AppLocalizations.of(context)!;
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text(localizations.editTags)),
|
||||
body: BlocBuilder<TagCubit, TagState>(
|
||||
builder: (context, state) {
|
||||
if (state is TagLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
} else if (state is TagError) {
|
||||
return Center(child: Text(state.message));
|
||||
} else if (state is TagLoaded) {
|
||||
return AnimatedList(
|
||||
key: _listKey,
|
||||
initialItemCount: state.tags.length,
|
||||
itemBuilder: (context, index, animation) {
|
||||
final tag = state.tags[index];
|
||||
return SizeTransition(
|
||||
sizeFactor: animation,
|
||||
child: TagListItem(
|
||||
tag: tag,
|
||||
onEdit: () => _editTag(context, tag),
|
||||
onDelete: () => _deleteTag(context, tag, index),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
return const Center(child: Text('Начните работу с тегами'));
|
||||
}
|
||||
},
|
||||
),
|
||||
floatingActionButton: AddTagButton(
|
||||
onPressed: () => _addTag(context),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
void _addTag(BuildContext context) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => TagEditPage(
|
||||
onSave: (name) {
|
||||
final newTag = Tag(
|
||||
name: name,
|
||||
);
|
||||
context.read<TagCubit>().addTag(newTag);
|
||||
_listKey.currentState?.insertItem(0);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _editTag(BuildContext context, Tag tag) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => TagEditPage(
|
||||
tag: tag,
|
||||
onSave: (name) {
|
||||
final updatedTag = tag.copyWith(name: name);
|
||||
context.read<TagCubit>().updateTag(updatedTag);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _deleteTag(BuildContext context, Tag tag, int index) {
|
||||
context.read<TagCubit>().deleteTag(tag.id);
|
||||
_listKey.currentState?.removeItem(
|
||||
index,
|
||||
(context, animation) => SizeTransition(
|
||||
sizeFactor: animation,
|
||||
child: TagListItem(
|
||||
tag: tag,
|
||||
onEdit: () {},
|
||||
onDelete: () {},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AddTagButton extends StatelessWidget {
|
||||
final VoidCallback onPressed;
|
||||
|
||||
const AddTagButton({
|
||||
super.key,
|
||||
required this.onPressed,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FloatingActionButton(
|
||||
onPressed: onPressed,
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
foregroundColor: Theme.of(context).colorScheme.onPrimary,
|
||||
elevation: 4,
|
||||
child: const Icon(Icons.add),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:budget_app/models/tag.dart';
|
||||
|
||||
class TagListItem extends StatelessWidget {
|
||||
final Tag tag;
|
||||
final VoidCallback onEdit;
|
||||
final VoidCallback onDelete;
|
||||
|
||||
const TagListItem({
|
||||
super.key,
|
||||
required this.tag,
|
||||
required this.onEdit,
|
||||
required this.onDelete,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
tag.name,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.edit),
|
||||
onPressed: onEdit,
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete),
|
||||
onPressed: onDelete,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hive_ce/hive.dart';
|
||||
|
||||
class SettingsService extends ChangeNotifier {
|
||||
static const String _darkModeKey = 'darkMode';
|
||||
late final Box _settingsBox;
|
||||
|
||||
SettingsService() {
|
||||
_settingsBox = Hive.box('settings');
|
||||
}
|
||||
|
||||
bool get isDarkMode => _settingsBox.get(_darkModeKey, defaultValue: false);
|
||||
|
||||
Future<void> setDarkMode(bool value) async {
|
||||
await _settingsBox.put(_darkModeKey, value);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> toggleTheme() async {
|
||||
await setDarkMode(!isDarkMode);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import 'package:another_telephony/telephony.dart' as telephony_package;
|
||||
import 'package:budget_app/models/sms_message.dart';
|
||||
|
||||
/// Сервис для работы с SMS сообщениями.
|
||||
///
|
||||
/// Использует пакет [telephony] для доступа к SMS на устройстве.
|
||||
/// Предоставляет методы для запроса разрешений и получения
|
||||
/// последних SMS сообщений.
|
||||
class SmsService {
|
||||
final telephony_package.Telephony _telephony = telephony_package.Telephony.instance;
|
||||
|
||||
/// Запрашивает разрешения на чтение и отправку SMS.
|
||||
///
|
||||
/// Возвращает [true], если разрешения были предоставлены,
|
||||
/// иначе [false].
|
||||
Future<bool> requestPermissions() async {
|
||||
return await _telephony.requestPhoneAndSmsPermissions ?? false;
|
||||
}
|
||||
|
||||
/// Возвращает список последних SMS сообщений.
|
||||
///
|
||||
/// [count] - количество сообщений для получения.
|
||||
///
|
||||
/// Возвращает список объектов [SmsMessage].
|
||||
/// В случае ошибки или отсутствия разрешений, возвращает пустой список.
|
||||
Future<List<SmsMessage>> getLastSmsMessages(int count) async {
|
||||
final bool? permissionsGranted =
|
||||
await _telephony.requestPhoneAndSmsPermissions;
|
||||
if (permissionsGranted ?? false) {
|
||||
final List<telephony_package.SmsMessage> messages = await _telephony.getInboxSms(
|
||||
columns: [telephony_package.SmsColumn.BODY, telephony_package.SmsColumn.ADDRESS, telephony_package.SmsColumn.DATE],
|
||||
sortOrder: [telephony_package.OrderBy(telephony_package.SmsColumn.DATE, sort: telephony_package.Sort.DESC)],
|
||||
);
|
||||
// Комментарий: Преобразуем сообщения из пакета telephony в нашу модель SmsMessage.
|
||||
return messages.take(count).map((msg) => SmsMessage(
|
||||
body: msg.body,
|
||||
sender: msg.address,
|
||||
date: DateTime.fromMillisecondsSinceEpoch(msg.date ?? 0),
|
||||
)).toList();
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// Комментарий: Метод для получения SMS-сообщений с определенной даты.
|
||||
Future<List<SmsMessage>> getSmsMessagesSince(DateTime sinceDate) async {
|
||||
final bool? permissionsGranted = await _telephony.requestPhoneAndSmsPermissions;
|
||||
if (permissionsGranted ?? false) {
|
||||
final List<telephony_package.SmsMessage> messages = await _telephony.getInboxSms(
|
||||
columns: [telephony_package.SmsColumn.BODY, telephony_package.SmsColumn.ADDRESS, telephony_package.SmsColumn.DATE],
|
||||
sortOrder: [telephony_package.OrderBy(telephony_package.SmsColumn.DATE, sort: telephony_package.Sort.ASC)],
|
||||
);
|
||||
// Комментарий: Фильтруем сообщения по дате и преобразуем их в нашу модель.
|
||||
return messages
|
||||
.where((msg) => (msg.date ?? 0) >= sinceDate.millisecondsSinceEpoch)
|
||||
.map((msg) => SmsMessage(
|
||||
body: msg.body,
|
||||
sender: msg.address,
|
||||
date: DateTime.fromMillisecondsSinceEpoch(msg.date ?? 0),
|
||||
))
|
||||
.toList();
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hive_ce/hive.dart';
|
||||
import '/models/user.dart';
|
||||
import '/data/repositories/interfaces/iuser_repository.dart';
|
||||
|
||||
/// Сервис для управления текущим пользователем приложения
|
||||
/// Использует ChangeNotifier для уведомления UI об изменениях
|
||||
class UserService extends ChangeNotifier {
|
||||
// Ключ для сохранения ID текущего пользователя в настройках
|
||||
static const String _currentUserKey = 'currentUserId';
|
||||
|
||||
late final Box _settingsBox; // Box для хранения настроек
|
||||
final IUserRepository _userRepository; // Репозиторий для работы с пользователями
|
||||
|
||||
User? _currentUser; // Текущий активный пользователь
|
||||
|
||||
/// Геттер для получения текущего пользователя
|
||||
User? get currentUser => _currentUser;
|
||||
|
||||
/// Проверяем, есть ли активный пользователь
|
||||
bool get hasCurrentUser => _currentUser != null;
|
||||
|
||||
/// Конструктор сервиса
|
||||
UserService(this._userRepository) {
|
||||
_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);
|
||||
await _userRepository.add(user);
|
||||
await setCurrentUser(user);
|
||||
}
|
||||
|
||||
/// Получение всех пользователей (для выбора)
|
||||
Future<List<User>> getAllUsers() async {
|
||||
return await _userRepository.getAll();
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'custom_colors.dart';
|
||||
|
||||
class AppTheme {
|
||||
// Светлая тема в черно-белой гамме
|
||||
@@ -11,7 +13,6 @@ class AppTheme {
|
||||
primary: Colors.black, // Основной цвет - черный
|
||||
secondary: Colors.grey[800]!, // Вторичный цвет - темно-серый
|
||||
surface: Colors.white, // Фон поверхностей
|
||||
background: Colors.grey[50]!, // Общий фон - очень светлый серый
|
||||
),
|
||||
appBarTheme: const AppBarTheme(
|
||||
backgroundColor: Colors.white, // Белый фон AppBar
|
||||
@@ -36,6 +37,22 @@ class AppTheme {
|
||||
),
|
||||
),
|
||||
),
|
||||
// Комментарий: Добавляем тему для выпадающих меню, чтобы цвет соответствовал фону.
|
||||
dropdownMenuTheme: DropdownMenuThemeData(
|
||||
menuStyle: MenuStyle(
|
||||
backgroundColor: WidgetStateProperty.all(Colors.white),
|
||||
),
|
||||
),
|
||||
// Изменяем цвета в соответствии с черно-белой палитрой
|
||||
extensions: <ThemeExtension<dynamic>>[
|
||||
CustomColors(
|
||||
income: Colors.grey[800]!, // Темно-серый для доходов
|
||||
expense: Colors.grey[600]!, // Серый для расходов
|
||||
divider: Colors.grey[300]!, // Светло-серый для разделителей
|
||||
accent: Colors.black, // Черный для акцентов
|
||||
unselectedIcon: Colors.grey[500]!, // Серый для невыбранных иконок
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -49,7 +66,6 @@ class AppTheme {
|
||||
primary: Colors.white, // Основной цвет - белый
|
||||
secondary: Colors.grey[300]!, // Вторичный цвет - светлый серый
|
||||
surface: Colors.grey[900]!, // Фон поверхностей
|
||||
background: Colors.black, // Общий фон - черный
|
||||
),
|
||||
appBarTheme: AppBarTheme(
|
||||
backgroundColor: Colors.grey[900]!, // Темно-серый фон AppBar
|
||||
@@ -75,6 +91,22 @@ class AppTheme {
|
||||
),
|
||||
),
|
||||
),
|
||||
// Комментарий: Добавляем тему для выпадающих меню, чтобы цвет соответствовал фону.
|
||||
dropdownMenuTheme: DropdownMenuThemeData(
|
||||
menuStyle: MenuStyle(
|
||||
backgroundColor: WidgetStateProperty.all(Colors.grey[800]),
|
||||
),
|
||||
),
|
||||
// Изменяем цвета в соответствии с черно-белой палитрой
|
||||
extensions: <ThemeExtension<dynamic>>[
|
||||
CustomColors(
|
||||
income: Colors.grey[300]!, // Светло-серый для доходов
|
||||
expense: Colors.grey[500]!, // Серый для расходов
|
||||
divider: Colors.grey[700]!, // Темно-серый для разделителей
|
||||
accent: Colors.white, // Белый для акцентов
|
||||
unselectedIcon: Colors.grey[400]!, // Светло-серый для невыбранных иконок
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
// Расширение темы для добавления пользовательских цветов.
|
||||
@immutable
|
||||
class CustomColors extends ThemeExtension<CustomColors> {
|
||||
const CustomColors({
|
||||
required this.income,
|
||||
required this.expense,
|
||||
this.divider, // Добавлен цвет для разделителей
|
||||
this.accent, // Добавлен цвет для акцентов
|
||||
this.unselectedIcon, // Цвет для невыбранных иконок
|
||||
});
|
||||
|
||||
final Color? income;
|
||||
final Color? expense;
|
||||
final Color? divider;
|
||||
final Color? accent;
|
||||
final Color? unselectedIcon; // Цвет для невыбранных иконок
|
||||
|
||||
@override
|
||||
CustomColors copyWith({
|
||||
Color? income,
|
||||
Color? expense,
|
||||
Color? divider,
|
||||
Color? accent,
|
||||
Color? unselectedIcon,
|
||||
}) {
|
||||
return CustomColors(
|
||||
income: income ?? this.income,
|
||||
expense: expense ?? this.expense,
|
||||
divider: divider ?? this.divider,
|
||||
accent: accent ?? this.accent,
|
||||
unselectedIcon: unselectedIcon ?? this.unselectedIcon,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
CustomColors lerp(ThemeExtension<CustomColors>? other, double t) {
|
||||
if (other is! CustomColors) {
|
||||
return this;
|
||||
}
|
||||
return CustomColors(
|
||||
income: Color.lerp(income, other.income, t),
|
||||
expense: Color.lerp(expense, other.expense, t),
|
||||
divider: Color.lerp(divider, other.divider, t),
|
||||
accent: Color.lerp(accent, other.accent, t),
|
||||
unselectedIcon: Color.lerp(unselectedIcon, other.unselectedIcon, t),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../models/category.dart';
|
||||
import '../services/user_service.dart';
|
||||
|
||||
/// Утилиты для работы с категориями
|
||||
class CategoryUtils {
|
||||
/// Возвращает список предопределенных категорий для конкретного пользователя
|
||||
/// Включает как категории доходов, так и расходов
|
||||
/// [userId] - идентификатор пользователя, для которого создаются категории
|
||||
static List<Category> getDefaultCategories(String userId) {
|
||||
static List<Category> getDefaultCategories() {
|
||||
return [
|
||||
// Категории доходов
|
||||
Category(
|
||||
@@ -16,7 +16,6 @@ class CategoryUtils {
|
||||
color: Colors.green,
|
||||
icon: Icons.attach_money,
|
||||
isIncome: true,
|
||||
userId: userId, // Привязываем к конкретному пользователю
|
||||
),
|
||||
Category(
|
||||
id: 'income_gift',
|
||||
@@ -24,7 +23,6 @@ class CategoryUtils {
|
||||
color: Colors.blue,
|
||||
icon: Icons.card_giftcard,
|
||||
isIncome: true,
|
||||
userId: userId, // Привязываем к конкретному пользователю
|
||||
),
|
||||
Category(
|
||||
id: 'income_freelance',
|
||||
@@ -32,7 +30,6 @@ class CategoryUtils {
|
||||
color: Colors.teal,
|
||||
icon: Icons.computer,
|
||||
isIncome: true,
|
||||
userId: userId, // Привязываем к конкретному пользователю
|
||||
),
|
||||
|
||||
// Категории расходов
|
||||
@@ -42,7 +39,6 @@ class CategoryUtils {
|
||||
color: Colors.red,
|
||||
icon: Icons.fastfood,
|
||||
isIncome: false,
|
||||
userId: userId, // Привязываем к конкретному пользователю
|
||||
),
|
||||
Category(
|
||||
id: 'expense_transport',
|
||||
@@ -50,7 +46,6 @@ class CategoryUtils {
|
||||
color: Colors.orange,
|
||||
icon: Icons.directions_car,
|
||||
isIncome: false,
|
||||
userId: userId, // Привязываем к конкретному пользователю
|
||||
),
|
||||
Category(
|
||||
id: 'expense_entertainment',
|
||||
@@ -58,7 +53,6 @@ class CategoryUtils {
|
||||
color: Colors.purple,
|
||||
icon: Icons.movie,
|
||||
isIncome: false,
|
||||
userId: userId, // Привязываем к конкретному пользователю
|
||||
),
|
||||
Category(
|
||||
id: 'expense_utilities',
|
||||
@@ -66,7 +60,6 @@ class CategoryUtils {
|
||||
color: Colors.blueGrey,
|
||||
icon: Icons.home,
|
||||
isIncome: false,
|
||||
userId: userId, // Привязываем к конкретному пользователю
|
||||
),
|
||||
Category(
|
||||
id: 'expense_shopping',
|
||||
@@ -74,7 +67,6 @@ class CategoryUtils {
|
||||
color: Colors.pink,
|
||||
icon: Icons.shopping_bag,
|
||||
isIncome: false,
|
||||
userId: userId, // Привязываем к конкретному пользователю
|
||||
),
|
||||
];
|
||||
}
|
||||
@@ -82,12 +74,12 @@ class CategoryUtils {
|
||||
/// Возвращает только категории доходов для конкретного пользователя
|
||||
/// [userId] - идентификатор пользователя
|
||||
static List<Category> getIncomeCategories(String userId) {
|
||||
return getDefaultCategories(userId).where((c) => c.isIncome).toList();
|
||||
return getDefaultCategories().where((c) => c.isIncome).toList();
|
||||
}
|
||||
|
||||
/// Возвращает только категории расходов для конкретного пользователя
|
||||
/// [userId] - идентификатор пользователя
|
||||
static List<Category> getExpenseCategories(String userId) {
|
||||
return getDefaultCategories(userId).where((c) => !c.isIncome).toList();
|
||||
return getDefaultCategories().where((c) => !c.isIncome).toList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,34 +5,28 @@ class TagUtils {
|
||||
// Комментарий: Мы изменили сигнатуру метода, добавив параметр `userId`.
|
||||
// Это необходимо, потому что конструктор `Tag` теперь требует `userId`.
|
||||
/// Возвращает список предопределенных тегов для конкретного пользователя
|
||||
static List<Tag> getDefaultTags(String userId) {
|
||||
static List<Tag> getDefaultTags() {
|
||||
return [
|
||||
Tag(
|
||||
id: 'tag_important',
|
||||
name: 'Важное',
|
||||
// Комментарий: Передаем `userId` при создании каждого тега по умолчанию.
|
||||
// Таким образом, эти теги будут принадлежать конкретному пользователю.
|
||||
userId: userId,
|
||||
),
|
||||
Tag(
|
||||
id: 'tag_work',
|
||||
|
||||
name: 'Работа',
|
||||
userId: userId,
|
||||
),
|
||||
Tag(
|
||||
id: 'tag_family',
|
||||
name: 'Семья',
|
||||
userId: userId,
|
||||
),
|
||||
Tag(
|
||||
id: 'tag_friends',
|
||||
name: 'Друзья',
|
||||
userId: userId,
|
||||
),
|
||||
Tag(
|
||||
id: 'tag_holiday',
|
||||
name: 'Отпуск',
|
||||
userId: userId,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
import '../models/transaction_record.dart';
|
||||
import '../models/category.dart';
|
||||
import '../models/tag.dart';
|
||||
import 'category_utils.dart';
|
||||
import 'tag_utils.dart';
|
||||
|
||||
/// Утилиты для работы с тестовыми транзакциями
|
||||
class TransactionUtils {
|
||||
/// Возвращает список тестовых транзакций для конкретного пользователя
|
||||
/// [userId] - идентификатор пользователя, для которого создаются транзакции
|
||||
static List<TransactionRecord> getSampleTransactions(String userId) {
|
||||
static List<TransactionRecord> getSampleTransactions() {
|
||||
// Получаем категории и теги для этого пользователя
|
||||
final categories = CategoryUtils.getDefaultCategories(userId);
|
||||
final tags = TagUtils.getDefaultTags(userId);
|
||||
final categories = CategoryUtils.getDefaultCategories();
|
||||
final tags = TagUtils.getDefaultTags();
|
||||
|
||||
return [
|
||||
// Доходы
|
||||
@@ -22,7 +19,6 @@ class TransactionUtils {
|
||||
dateTime: DateTime.now().subtract(const Duration(days: 5)),
|
||||
vendor: 'ООО "Рога и копыта"',
|
||||
currency: 'RUB',
|
||||
userId: userId, // Указываем userId для тестовой транзакции
|
||||
),
|
||||
TransactionRecord(
|
||||
category: categories.firstWhere((c) => c.id == 'income_freelance'),
|
||||
@@ -31,7 +27,6 @@ class TransactionUtils {
|
||||
dateTime: DateTime.now().subtract(const Duration(days: 2)),
|
||||
vendor: 'Фриланс проект',
|
||||
currency: 'RUB',
|
||||
userId: userId, // Указываем userId для тестовой транзакции
|
||||
),
|
||||
|
||||
// Расходы
|
||||
@@ -42,7 +37,6 @@ class TransactionUtils {
|
||||
dateTime: DateTime.now().subtract(const Duration(days: 1)),
|
||||
vendor: 'Пятерочка',
|
||||
currency: 'RUB',
|
||||
userId: userId, // Указываем userId для тестовой транзакции
|
||||
),
|
||||
TransactionRecord(
|
||||
category: categories.firstWhere((c) => c.id == 'expense_transport'),
|
||||
@@ -51,7 +45,6 @@ class TransactionUtils {
|
||||
dateTime: DateTime.now().subtract(const Duration(hours: 12)),
|
||||
vendor: 'Яндекс Такси',
|
||||
currency: 'RUB',
|
||||
userId: userId, // Указываем userId для тестовой транзакции
|
||||
),
|
||||
TransactionRecord(
|
||||
category: categories.firstWhere((c) => c.id == 'expense_entertainment'),
|
||||
@@ -60,7 +53,6 @@ class TransactionUtils {
|
||||
dateTime: DateTime.now().subtract(const Duration(hours: 6)),
|
||||
vendor: 'Кинотеатр',
|
||||
currency: 'RUB',
|
||||
userId: userId, // Указываем userId для тестовой транзакции
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
+335
-18
@@ -5,18 +5,42 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: _fe_analyzer_shared
|
||||
sha256: e55636ed79578b9abca5fecf9437947798f5ef7456308b5cb85720b793eac92f
|
||||
sha256: da0d9209ca76bde579f2da330aeb9df62b6319c834fa7baae052021b0462401f
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "82.0.0"
|
||||
version: "85.0.0"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: analyzer
|
||||
sha256: "904ae5bb474d32c38fb9482e2d925d5454cda04ddd0e55d2e6826bc72f6ba8c0"
|
||||
sha256: f6154230675c44a191f2e20d16eeceb4aa18b30ca732db4efaf94c6a7d43cfa6
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.4.5"
|
||||
version: "7.5.2"
|
||||
animated_digit:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: animated_digit
|
||||
sha256: "22300a550b83e08ac4a0ef9c6fc7e800bbebc34978d997e6346da03d69fbb7a8"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.3"
|
||||
another_telephony:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: another_telephony
|
||||
sha256: "7dd16759099ea3e4ce762c4f5bbfa097940335555d5caa916aaa52eec6a70cb6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.4.1"
|
||||
archive:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: archive
|
||||
sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.6.1"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -33,6 +57,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.13.0"
|
||||
bloc:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: bloc
|
||||
sha256: "52c10575f4445c61dd9e0cafcc6356fdd827c4c64dd7945ef3c4105f6b6ac189"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.0.0"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -45,10 +77,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build
|
||||
sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0
|
||||
sha256: "51dc711996cbf609b90cbe5b335bbce83143875a9d58e4b5c6d3c4f684d3dda7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.2"
|
||||
version: "2.5.4"
|
||||
build_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -69,26 +101,26 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_resolvers
|
||||
sha256: b9e4fda21d846e192628e7a4f6deda6888c36b5b69ba02ff291a01fd529140f0
|
||||
sha256: ee4257b3f20c0c90e72ed2b57ad637f694ccba48839a821e87db762548c22a62
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.4"
|
||||
version: "2.5.4"
|
||||
build_runner:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: build_runner
|
||||
sha256: "058fe9dce1de7d69c4b84fada934df3e0153dd000758c4d65964d0166779aa99"
|
||||
sha256: "382a4d649addbfb7ba71a3631df0ec6a45d5ab9b098638144faf27f02778eb53"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.15"
|
||||
version: "2.5.4"
|
||||
build_runner_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_runner_core
|
||||
sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021"
|
||||
sha256: "85fbbb1036d576d966332a3f5ce83f2ce66a40bea1a94ad2d5fc29a19a0d3792"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.0.0"
|
||||
version: "9.1.2"
|
||||
built_collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -121,6 +153,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.4"
|
||||
chunked_stream:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: chunked_stream
|
||||
sha256: b2fde5f81d780f0c1699b8347cae2e413412ae947fc6e64727cc48c6bb54c95c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.2"
|
||||
circular_buffer:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: circular_buffer
|
||||
sha256: b3a315fef3fee7fe58879643fc8ce21c7c2449d01c1a8a396dc9e24687f335c4
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.12.0"
|
||||
clock:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -161,6 +209,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.6"
|
||||
csv:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: csv
|
||||
sha256: c6aa2679b2a18cb57652920f674488d89712efaf4d3fdf2e537215b35fc19d6c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.0"
|
||||
cupertino_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -169,6 +225,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.8"
|
||||
dart_console:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dart_console
|
||||
sha256: "03c23e1f9cc3ac02b608f834808003e6510a5b292a0449f43dfac1c78bd8ee85"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.2"
|
||||
dart_style:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -177,6 +241,46 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
dcli:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dcli
|
||||
sha256: "881e88bbad0ada4e3a085a0b55e05afa8e4199392c0c45ac18e3dedc37305b9b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.2"
|
||||
dcli_common:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dcli_common
|
||||
sha256: f8f77bea6a6d7e4ec2dc24cb4f274fc582938057c2cba44ed0650195ecfcd3ad
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.2"
|
||||
dcli_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dcli_core
|
||||
sha256: "29fb4833aa950900936646190b30315db511a853273b9fe31d360e5f72c7560b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.2"
|
||||
dcli_terminal:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dcli_terminal
|
||||
sha256: fb50860855c6b2841aed5bcfb315fa83d1401e109691b6585773a24c149dc4b0
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.2"
|
||||
equatable:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: equatable
|
||||
sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.7"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -214,6 +318,30 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_bloc:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_bloc
|
||||
sha256: cf51747952201a455a1c840f8171d273be009b932c75093020f9af64f2123e38
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.1.1"
|
||||
flutter_colorpicker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_colorpicker
|
||||
sha256: "969de5f6f9e2a570ac660fb7b501551451ea2a1ab9e2097e89475f60e07816ea"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
flutter_iconpicker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_iconpicker
|
||||
sha256: d53b35bcb73325fcfdd36931769a8e7ff33b38e3b7c39b518a226fd0a5f3dc29
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.1"
|
||||
flutter_lints:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@@ -222,11 +350,24 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.0"
|
||||
flutter_localizations:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
font_awesome_flutter:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: font_awesome_flutter
|
||||
sha256: d3a89184101baec7f4600d58840a764d2ef760fe1c5a20ef9e6b0e9b24a07a3a
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.8.0"
|
||||
frontend_server_client:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -235,6 +376,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
functional_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: functional_data
|
||||
sha256: "76d17dc707c40e552014f5a49c0afcc3f1e3f05e800cd6b7872940bfe41a5039"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
get_it:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -251,6 +400,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
globbing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: globbing
|
||||
sha256: "4f89cfaf6fa74c9c1740a96259da06bd45411ede56744e28017cc534a12b6e2d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
graphs:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -307,6 +464,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.2"
|
||||
ini:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: ini
|
||||
sha256: "12a76c53591ffdf86d1265be3f986888a6dfeb34a85957774bc65912d989a173"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.0"
|
||||
intl:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: intl
|
||||
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.20.2"
|
||||
io:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -331,6 +504,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.2"
|
||||
json2yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: json2yaml
|
||||
sha256: da94630fbc56079426fdd167ae58373286f603371075b69bf46d848d63ba3e51
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.1"
|
||||
json_annotation:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -371,14 +552,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.1.1"
|
||||
lists:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: lists
|
||||
sha256: "4ca5c19ae4350de036a7e996cdd1ee39c93ac0a2b840f4915459b7d0a7d4ab27"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
logger:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: logger
|
||||
sha256: be4b23575aac7ebf01f225a241eb7f6b5641eeaf43c6a8613510fc2f8cf187d1
|
||||
sha256: "2621da01aabaf223f8f961e751f2c943dbb374dc3559b982f200ccedadaa6999"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.0"
|
||||
version: "2.6.0"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -419,6 +608,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
native_synchronization_temp:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: native_synchronization_temp
|
||||
sha256: f9ad36a5054c606db10e3dc0c9c352e6d0d56d08621af5c470abf9fa41da40fa
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.1"
|
||||
nested:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: nested
|
||||
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
package_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -507,6 +712,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.1"
|
||||
posix:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: posix
|
||||
sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.3"
|
||||
provider:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: provider
|
||||
sha256: "4abbd070a04e9ddc287673bf5a030c7ca8b685ff70218720abab8b092f53dd84"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.5"
|
||||
pub_semver:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -515,6 +736,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
pubspec_lock:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pubspec_lock
|
||||
sha256: ed5fc1ecd0cdc0e14475a091afcb2c4cbb00e74cebff17635e9abbec18d76cc4
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
pubspec_manager:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pubspec_manager
|
||||
sha256: "4000db36057ddc9c95f1c56fd209ce54b1e7c621280f52e159a83342b1e33d62"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
pubspec_parse:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -523,6 +760,30 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.0"
|
||||
scope:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: scope
|
||||
sha256: "0b056e5b64ca16a2db9e1eb35cf7fd05a9e99a6b15140f82bfa651d081e4819b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.1.0"
|
||||
scrollview_observer:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: scrollview_observer
|
||||
sha256: "174d4efe7b79459a07662175c4db42c9862dcf78d3978e6e9c2d6c0d8137f4ca"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.26.1"
|
||||
settings_yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: settings_yaml
|
||||
sha256: "31c389f57d21518866ff36ec08cb15bf5c28aa6d324c09ba34a5547474f2b603"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.3.0"
|
||||
shelf:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -568,6 +829,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.10.1"
|
||||
sprintf:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sprintf
|
||||
sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.0"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -600,6 +869,30 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.1"
|
||||
strings:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: strings
|
||||
sha256: "052836499f03897d3860a603b330c1ea3c8a14177b21f34b15a1295f36024aae"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
sum_types:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sum_types
|
||||
sha256: c0a0fad9a518d011987e1d9f27fc336194294e55dafdc3699363e52aa5776e09
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.5"
|
||||
system_info2:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: system_info2
|
||||
sha256: "65206bbef475217008b5827374767550a5420ce70a04d2d7e94d1d2253f3efc9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -632,14 +925,30 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
unicode:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: unicode
|
||||
sha256: "0f69e46593d65245774d4f17125c6084d2c20b4e473a983f6e21b7d7762218f1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.1"
|
||||
uuid:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: uuid
|
||||
sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313"
|
||||
sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.7"
|
||||
version: "4.5.1"
|
||||
validators2:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: validators2
|
||||
sha256: "5c63054b2f47b6a3f39e0d0e3f5d38829db4545250144a34c9e1585466de4814"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.0"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -660,10 +969,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: watcher
|
||||
sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104"
|
||||
sha256: "0b7fd4a0bbc4b92641dbf20adfd7e3fd1398fe17102d94b674234563e110088a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
version: "1.1.2"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -688,6 +997,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.3"
|
||||
win32:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32
|
||||
sha256: "66814138c3562338d05613a6e368ed8cfb237ad6d64a9e9334be3f309acfca03"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.14.0"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
+18
-6
@@ -30,23 +30,34 @@ environment:
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
another_telephony: ^0.4.1
|
||||
animated_digit: ^3.2.0
|
||||
|
||||
# The following adds the Cupertino Icons font to your application.
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
cupertino_icons: ^1.0.8
|
||||
hive_ce: ^2.11.3
|
||||
hive_ce_flutter: ^2.3.1
|
||||
get_it: ^7.2.0
|
||||
hive_ce: ^2.11.3 # Обновлено до последней версии
|
||||
hive_ce_flutter: ^2.3.1 # Обновлено до последней версии
|
||||
get_it: ^7.7.0 # Обновлено до последней версии
|
||||
path_provider: ^2.0.15
|
||||
uuid: ^3.0.7
|
||||
uuid: ^4.0.0 # Обновлено до последней версии
|
||||
logger: ^2.5.0
|
||||
flutter_bloc: ^9.1.1
|
||||
equatable: ^2.0.7
|
||||
flutter_localizations:
|
||||
sdk: flutter
|
||||
intl: ^0.20.2
|
||||
bloc:
|
||||
flutter_colorpicker: ^1.1.0
|
||||
flutter_iconpicker: ^4.0.1
|
||||
|
||||
|
||||
dev_dependencies:
|
||||
hive_ce_generator: ^1.9.2
|
||||
hive_ce_generator: ^1.9.2 # Возвращено к совместимой версии
|
||||
build_runner: ^2.4.0
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
flutter_lints: ^5.0.0
|
||||
flutter_lints: ^5.0.0 # Возвращено к совместимой версии
|
||||
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
@@ -54,6 +65,7 @@ dev_dependencies:
|
||||
|
||||
# The following section is specific to Flutter packages.
|
||||
flutter:
|
||||
generate: true
|
||||
|
||||
# The following line ensures that the Material Icons font is
|
||||
# included with your application, so that you can use the icons in
|
||||
|
||||
Reference in New Issue
Block a user