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:
2025-07-13 00:17:44 +03:00
99 changed files with 6491 additions and 603 deletions
+28
View File
@@ -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.
-
+31
View File
@@ -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"
}
]
}
+3
View File
@@ -0,0 +1,3 @@
{
"dart.flutterSdkPath": "C:\\Sanders\\Flutter\\flutter_windows_3.32.2-stable\\flutter"
}
+1 -1
View File
@@ -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
+10
View File
@@ -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
+3
View File
@@ -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:
+3
View File
@@ -0,0 +1,3 @@
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
+56 -73
View File
@@ -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();
}
}
+17 -10
View File
@@ -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);
}
-6
View File
@@ -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';
-32
View File
@@ -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 -7
View File
@@ -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
+16 -2
View File
@@ -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
View File
@@ -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>();
}
+60
View File
@@ -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"
}
+476
View File
@@ -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, youll need to edit this
/// file.
///
/// First, open your projects ios/Runner.xcworkspace Xcode workspace file.
/// Then, in the Project Navigator, open the Info.plist file under the Runner
/// projects 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.',
);
}
+184
View File
@@ -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';
}
+187
View File
@@ -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 => 'Не обработано';
}
+60
View File
@@ -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": "Не обработано"
}
View File
+95
View File
@@ -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()));
}
}
}
+32
View File
@@ -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];
}
+33
View File
@@ -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];
}
+61
View File
@@ -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'));
}
}
}
+30
View File
@@ -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];
}
+76
View File
@@ -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);
}
}
+51
View File
@@ -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];
}
+122
View File
@@ -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)
// );
// }
}
}
+39
View File
@@ -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];
}
+52
View File
@@ -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()}'));
}
}
}
+18
View File
@@ -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);
}
View File
@@ -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];
}
+120
View File
@@ -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 [];
}
}
}
+37
View File
@@ -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
View File
@@ -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,
);
},
),
);
}
}
+80
View File
@@ -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)';
}
}
+50
View File
@@ -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
View File
@@ -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];
}
+3 -3
View File
@@ -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
+13
View File
@@ -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,
});
}
+39
View File
@@ -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;
}
+65
View File
@@ -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();
}
+118
View File
@@ -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;
}
+42
View File
@@ -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,
);
}
}
+53
View File
@@ -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
View File
@@ -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];
}
+3 -3
View File
@@ -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
+43 -10
View File
@@ -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];
}
+3 -3
View File
@@ -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
View File
@@ -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,
);
}
}
+8 -2
View File
@@ -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
+202
View File
@@ -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),
),
],
),
),
),
);
}
}
+102
View File
@@ -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,
),
],
),
),
);
}
}
+211
View File
@@ -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)),
],
);
}
}
+305
View File
@@ -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),
],
);
}
}
-79
View File
@@ -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),
),
],
),
),
);
}
}
View File
+99
View File
@@ -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),
);
},
),
],
),
),
),
),
);
}
}
+20
View File
@@ -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
View File
@@ -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());
}
},
),
),
);
+54
View File
@@ -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,
),
],
),
],
),
),
),
);
}
}
+21
View File
@@ -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('Загрузка...'),
],
),
),
);
}
}
+74
View File
@@ -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),
),
],
),
),
),
);
}
}
+111
View File
@@ -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: () {},
),
),
);
}
}
+21
View File
@@ -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),
);
}
}
+47
View File
@@ -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,
),
],
),
),
);
}
}
-22
View File
@@ -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);
}
}
+64
View File
@@ -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 [];
}
}
-64
View File
@@ -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();
}
}
+34 -2
View File
@@ -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]!, // Светло-серый для невыбранных иконок
),
],
);
}
}
+51
View File
@@ -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),
);
}
}
+4 -12
View File
@@ -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();
}
}
+2 -8
View File
@@ -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,
),
];
}
+3 -11
View File
@@ -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
View File
@@ -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
View File
@@ -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