refactor: migrate database from Drift to Isar
This commit is contained in:
@@ -1,12 +1,13 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:isar/isar.dart'; // Импорт Isar
|
||||
import 'screens/expenses_screen.dart';
|
||||
import 'theme/app_theme.dart';
|
||||
import 'database/database.dart'; // Import the database
|
||||
// import 'database/database.dart'; // Больше не нужен прямой импорт AppDatabase или IsarService
|
||||
|
||||
class MyApp extends StatefulWidget {
|
||||
final AppDatabase database; // Принимаем экземпляр базы данных
|
||||
final Isar isar; // Принимаем экземпляр Isar
|
||||
|
||||
const MyApp({Key? key, required this.database}) : super(key: key);
|
||||
const MyApp({Key? key, required this.isar}) : super(key: key); // Обновленный конструктор
|
||||
|
||||
@override
|
||||
State<MyApp> createState() => _MyAppState();
|
||||
@@ -32,7 +33,7 @@ class _MyAppState extends State<MyApp> {
|
||||
theme: AppTheme.lightTheme, // Светлая тема
|
||||
darkTheme: AppTheme.darkTheme, // Темная тема
|
||||
home: ExpensesScreen(
|
||||
database: widget.database, // Передаем базу данных в главный экран
|
||||
isar: widget.isar, // Передаем Isar в главный экран
|
||||
toggleTheme: toggleTheme, // Передаем функцию переключения темы
|
||||
isDarkMode: _isDarkMode, // Передаем текущее состояние темы
|
||||
),
|
||||
|
||||
@@ -1,80 +1,75 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/native.dart';
|
||||
// import 'package:flutter/material.dart'; // Не требуется напрямую, т.к. Category импортируется
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart'; // Для Color/IconData в Category
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
// import 'package:rxdart/rxdart.dart'; // Не используется
|
||||
import 'package:rxdart/rxdart.dart'; // Используем для map на потоке
|
||||
|
||||
// Импортируем модель категории для возвращаемого типа и утилиты
|
||||
// Импортируем модели
|
||||
import '../models/category.dart';
|
||||
// Удален ненужный импорт: import '../models/transaction_record.dart';
|
||||
import '../utils/category_utils.dart'; // Helper for category details
|
||||
import '../models/isar_transaction.dart'; // Наша Isar модель
|
||||
import '../utils/category_utils.dart'; // Помощник для деталей категории
|
||||
|
||||
// Эта строка указывает Drift сгенерировать файл database.g.dart
|
||||
part 'database.g.dart';
|
||||
// Этот файл больше не использует Drift, поэтому part 'database.g.dart'; удален.
|
||||
|
||||
// Определение таблицы Transactions
|
||||
// Имя таблицы в SQL будет 'transactions' (snake_case от имени класса)
|
||||
@DataClassName('Transaction') // Keep the generated class name as Transaction
|
||||
class Transactions extends Table {
|
||||
IntColumn get id => integer().autoIncrement()(); // Primary key
|
||||
TextColumn get categoryName => text().named('category_name')(); // Имя категории
|
||||
RealColumn get amount => real()(); // Сумма транзакции
|
||||
DateTimeColumn get date => dateTime()(); // Дата транзакции
|
||||
TextColumn get merchant => text()(); // Название продавца/магазина
|
||||
}
|
||||
// Класс-сервис для инкапсуляции логики работы с Isar
|
||||
class IsarService {
|
||||
final Isar isar;
|
||||
|
||||
// Класс базы данных
|
||||
// Аннотация @DriftDatabase указывает Drift сгенерировать код для этой базы данных
|
||||
@DriftDatabase(tables: [Transactions])
|
||||
class AppDatabase extends _$AppDatabase {
|
||||
AppDatabase() : super(_openConnection());
|
||||
|
||||
// Версия схемы. Увеличивайте при изменении структуры таблиц.
|
||||
@override
|
||||
int get schemaVersion => 1;
|
||||
IsarService(this.isar);
|
||||
|
||||
// --- Методы для работы с транзакциями ---
|
||||
|
||||
// Получить все транзакции в виде потока, упорядоченные по дате (сначала новые)
|
||||
// Возвращает non-nullable Stream<List<Transaction>> (Transaction - сгенерированный Drift класс)
|
||||
Stream<List<Transaction>> watchAllTransactions() {
|
||||
return (select(transactions)
|
||||
..orderBy([(t) => OrderingTerm(expression: t.date, mode: OrderingMode.desc)]))
|
||||
.watch();
|
||||
// Возвращает Stream<List<IsarTransaction>>
|
||||
Stream<List<IsarTransaction>> watchAllTransactions() {
|
||||
// isar.isarTransactions ссылается на коллекцию IsarTransaction
|
||||
// .where() без условий выбирает все
|
||||
// .sortByDateDesc() сортирует по полю date по убыванию
|
||||
// .watch(fireImmediately: true) создает поток, который срабатывает сразу
|
||||
return isar.isarTransactions
|
||||
.where()
|
||||
.sortByDateDesc()
|
||||
.watch(fireImmediately: true);
|
||||
}
|
||||
|
||||
// Получить транзакции, отфильтрованные по категории, в виде потока
|
||||
// Возвращает non-nullable Stream<List<Transaction>> (Transaction - сгенерированный Drift класс)
|
||||
Stream<List<Transaction>> watchFilteredTransactions(String categoryName) {
|
||||
if (categoryName == 'All') {
|
||||
// watchAllTransactions теперь возвращает non-nullable Stream
|
||||
return watchAllTransactions(); // Return all if filter is 'All'
|
||||
}
|
||||
// Otherwise, filter by the provided category name
|
||||
return (select(transactions)
|
||||
..where((t) => t.categoryName.equals(categoryName))
|
||||
..orderBy([(t) => OrderingTerm(expression: t.date, mode: OrderingMode.desc)]))
|
||||
.watch();
|
||||
}
|
||||
// Возвращает Stream<List<IsarTransaction>>
|
||||
Stream<List<IsarTransaction>> watchFilteredTransactions(String categoryName) {
|
||||
if (categoryName == 'All') {
|
||||
return watchAllTransactions(); // Возвращаем все, если фильтр 'All'
|
||||
}
|
||||
// Иначе фильтруем по categoryName
|
||||
return isar.isarTransactions
|
||||
.filter()
|
||||
.categoryNameEqualTo(categoryName) // Используем сгенерированный Isar фильтр
|
||||
.sortByDateDesc()
|
||||
.watch(fireImmediately: true);
|
||||
}
|
||||
|
||||
// Добавить новую транзакцию
|
||||
// Принимает TransactionsCompanion - сгенерированный Drift класс
|
||||
Future<int> addTransaction(TransactionsCompanion entry) {
|
||||
return into(transactions).insert(entry);
|
||||
// Принимает объект IsarTransaction
|
||||
Future<void> addTransaction(IsarTransaction transaction) async {
|
||||
// Используем writeTxn для выполнения операции записи
|
||||
await isar.writeTxn(() async {
|
||||
await isar.isarTransactions.put(transaction); // put добавляет или обновляет запись
|
||||
});
|
||||
}
|
||||
|
||||
// Добавить несколько транзакций (для начальных данных)
|
||||
Future<void> addTransactions(List<IsarTransaction> transactions) async {
|
||||
await isar.writeTxn(() async {
|
||||
await isar.isarTransactions.putAll(transactions);
|
||||
});
|
||||
}
|
||||
|
||||
// --- Методы для работы с категориями (вычисляются из транзакций) ---
|
||||
|
||||
// Вычислить и наблюдать за общими суммами по категориям
|
||||
Stream<List<Category>> calculateCategoryTotals() {
|
||||
// 1. Получаем поток всех транзакций (теперь non-nullable Stream<List<Transaction>>)
|
||||
// 1. Получаем поток всех транзакций
|
||||
return watchAllTransactions().map((transactionList) {
|
||||
// 2. Группируем транзакции по categoryName и суммируем amount
|
||||
final categoryTotals = <String, double>{};
|
||||
// transactionList теперь содержит объекты Transaction, сгенерированные Drift
|
||||
for (var transaction in transactionList) {
|
||||
categoryTotals.update(
|
||||
transaction.categoryName,
|
||||
@@ -85,9 +80,6 @@ class AppDatabase extends _$AppDatabase {
|
||||
|
||||
// 3. Преобразуем сгруппированные данные в список объектов Category
|
||||
return categoryTotals.entries.map((entry) {
|
||||
// Используем CategoryUtils для получения деталей (иконка, цвет)
|
||||
// Убедитесь, что CategoryUtils корректно обрабатывает все categoryName,
|
||||
// включая 'Utilities' из начальных данных, или предоставляет дефолтные значения.
|
||||
final categoryDetails = CategoryUtils.getCategoryDetails(entry.key);
|
||||
return Category(
|
||||
entry.key, // name
|
||||
@@ -96,7 +88,7 @@ class AppDatabase extends _$AppDatabase {
|
||||
categoryDetails.iconCode,
|
||||
);
|
||||
}).toList()
|
||||
// Сортируем категории по сумме (от большей к меньшей) для диаграммы/легенды
|
||||
// Сортируем категории по сумме (от большей к меньшей)
|
||||
..sort((a, b) => b.amount.compareTo(a.amount));
|
||||
});
|
||||
}
|
||||
@@ -104,42 +96,43 @@ class AppDatabase extends _$AppDatabase {
|
||||
// Пример добавления начальных данных (если база данных пуста)
|
||||
Future<void> insertInitialDataIfNeeded() async {
|
||||
// Проверяем, есть ли уже транзакции
|
||||
final existingTransactions = await select(transactions).get();
|
||||
if (existingTransactions.isEmpty) {
|
||||
print("Database is empty. Inserting initial data...");
|
||||
// Используем batch для эффективной вставки нескольких записей
|
||||
await batch((batch) {
|
||||
batch.insertAll(transactions, [
|
||||
// Используем TransactionsCompanion для создания записей для вставки
|
||||
TransactionsCompanion.insert(categoryName: 'Groceries', amount: 45.99, date: DateTime.now().subtract(const Duration(days: 1, hours: 2)), merchant: 'Whole Foods Market'),
|
||||
TransactionsCompanion.insert(categoryName: 'Subscriptions', amount: 39.99, date: DateTime.now().subtract(const Duration(days: 2, hours: 5)), merchant: 'Netflix Premium'),
|
||||
TransactionsCompanion.insert(categoryName: 'Restaurant', amount: 78.50, date: DateTime.now().subtract(const Duration(days: 2, hours: 19)), merchant: 'Italian Corner'),
|
||||
TransactionsCompanion.insert(categoryName: 'Shopping', amount: 132.75, date: DateTime.now().subtract(const Duration(days: 3, hours: 11)), merchant: 'Apple Store'),
|
||||
TransactionsCompanion.insert(categoryName: 'Groceries', amount: 23.45, date: DateTime.now().subtract(const Duration(days: 4, hours: 9)), merchant: 'Local Market'),
|
||||
TransactionsCompanion.insert(categoryName: 'Transport', amount: 15.00, date: DateTime.now().subtract(const Duration(days: 5, hours: 8)), merchant: 'City Bus'),
|
||||
TransactionsCompanion.insert(categoryName: 'Restaurant', amount: 56.80, date: DateTime.now().subtract(const Duration(days: 5, hours: 20)), merchant: 'Sushi Express'),
|
||||
// Убедитесь, что 'Utilities' определена в CategoryUtils или обрабатывается как неизвестная категория
|
||||
TransactionsCompanion.insert(categoryName: 'Utilities', amount: 85.20, date: DateTime.now().subtract(const Duration(days: 6, hours: 10)), merchant: 'Electricity Bill'),
|
||||
]);
|
||||
});
|
||||
print("Initial data inserted successfully.");
|
||||
final count = await isar.isarTransactions.count();
|
||||
if (count == 0) {
|
||||
print("Isar Database is empty. Inserting initial data...");
|
||||
final initialTransactions = [
|
||||
IsarTransaction(categoryName: 'Groceries', amount: 45.99, date: DateTime.now().subtract(const Duration(days: 1, hours: 2)), merchant: 'Whole Foods Market'),
|
||||
IsarTransaction(categoryName: 'Subscriptions', amount: 39.99, date: DateTime.now().subtract(const Duration(days: 2, hours: 5)), merchant: 'Netflix Premium'),
|
||||
IsarTransaction(categoryName: 'Restaurant', amount: 78.50, date: DateTime.now().subtract(const Duration(days: 2, hours: 19)), merchant: 'Italian Corner'),
|
||||
IsarTransaction(categoryName: 'Shopping', amount: 132.75, date: DateTime.now().subtract(const Duration(days: 3, hours: 11)), merchant: 'Apple Store'),
|
||||
IsarTransaction(categoryName: 'Groceries', amount: 23.45, date: DateTime.now().subtract(const Duration(days: 4, hours: 9)), merchant: 'Local Market'),
|
||||
IsarTransaction(categoryName: 'Transport', amount: 15.00, date: DateTime.now().subtract(const Duration(days: 5, hours: 8)), merchant: 'City Bus'),
|
||||
IsarTransaction(categoryName: 'Restaurant', amount: 56.80, date: DateTime.now().subtract(const Duration(days: 5, hours: 20)), merchant: 'Sushi Express'),
|
||||
// Убедитесь, что 'Utilities' определена в CategoryUtils или обрабатывается как неизвестная категория
|
||||
IsarTransaction(categoryName: 'Utilities', amount: 85.20, date: DateTime.now().subtract(const Duration(days: 6, hours: 10)), merchant: 'Electricity Bill'),
|
||||
];
|
||||
await addTransactions(initialTransactions);
|
||||
print("Initial data inserted successfully into Isar.");
|
||||
} else {
|
||||
print("Database already contains data (${existingTransactions.length} transactions). Skipping initial data insertion.");
|
||||
print("Isar Database already contains data ($count transactions). Skipping initial data insertion.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Функция для открытия соединения с базой данных
|
||||
LazyDatabase _openConnection() {
|
||||
// Вычисление пути к файлу базы данных в папке документов приложения
|
||||
return LazyDatabase(() async {
|
||||
final dbFolder = await getApplicationDocumentsDirectory();
|
||||
// Создаем файл 'db.sqlite' в этой папке
|
||||
final file = File(p.join(dbFolder.path, 'db.sqlite'));
|
||||
print("Database file path: ${file.path}"); // Логируем путь для отладки
|
||||
|
||||
// Используем NativeDatabase для открытия соединения
|
||||
// logStatements: true полезен для отладки SQL-запросов
|
||||
return NativeDatabase(file, logStatements: false);
|
||||
});
|
||||
// Статический метод для открытия базы данных Isar
|
||||
static Future<Isar> openDB() async {
|
||||
final dir = await getApplicationDocumentsDirectory();
|
||||
// Проверяем, открыт ли уже экземпляр Isar с таким именем
|
||||
// Это полезно для hot reload/restart
|
||||
if (Isar.instanceNames.isEmpty) {
|
||||
print("Opening Isar database at: ${dir.path}");
|
||||
return await Isar.open(
|
||||
[IsarTransactionSchema], // Передаем сгенерированную схему
|
||||
directory: dir.path,
|
||||
inspector: true, // Включаем инспектор для отладки (доступен в debug режиме)
|
||||
);
|
||||
}
|
||||
// Возвращаем существующий экземпляр
|
||||
print("Returning existing Isar instance.");
|
||||
// ignore: RETHROW_IF_NOT_THROWING
|
||||
return Future.value(Isar.getInstance()); // Возвращаем существующий экземпляр
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,29 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:isar/isar.dart'; // Импорт Isar
|
||||
// import 'package:path_provider/path_provider.dart'; // Больше не нужен здесь
|
||||
|
||||
import 'app.dart'; // Import the new app root widget
|
||||
import 'database/database.dart'; // Import the database
|
||||
import 'database/database.dart'; // Импортируем наш IsarService
|
||||
import 'models/isar_transaction.dart'; // Импортируем схему Isar
|
||||
|
||||
Future<void> main() async {
|
||||
// Необходимо для асинхронных операций перед runApp, например, инициализации БД
|
||||
// Необходимо для асинхронных операций перед runApp
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// Создаем единственный экземпляр базы данных для всего приложения
|
||||
final database = AppDatabase();
|
||||
// Инициализация Isar Core (нужно для нативных библиотек)
|
||||
// download: true скачает нужную библиотеку, если ее нет
|
||||
// await Isar.initializeIsarCore(download: true); // Больше не требуется с isar_flutter_libs
|
||||
|
||||
// Открываем базу данных Isar с помощью статического метода в IsarService
|
||||
final isar = await IsarService.openDB();
|
||||
|
||||
// Создаем экземпляр нашего сервиса (хотя он может быть не нужен здесь, если логика в IsarService)
|
||||
final isarService = IsarService(isar);
|
||||
|
||||
// Опционально: Вставляем начальные данные, если база данных пуста
|
||||
// Это полезно для первого запуска или демонстрации
|
||||
await database.insertInitialDataIfNeeded();
|
||||
// Используем метод из IsarService
|
||||
await isarService.insertInitialDataIfNeeded();
|
||||
|
||||
// Запускаем приложение, передавая экземпляр базы данных
|
||||
runApp(MyApp(database: database));
|
||||
// Запускаем приложение, передавая экземпляр Isar
|
||||
runApp(MyApp(isar: isar)); // Передаем isar вместо database
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
// Эта строка указывает Isar сгенерировать файл isar_transaction.g.dart
|
||||
part 'isar_transaction.g.dart';
|
||||
|
||||
@collection // Аннотация для Isar коллекции
|
||||
class IsarTransaction {
|
||||
Id id = Isar.autoIncrement; // Автоинкрементный ID
|
||||
|
||||
@Index() // Индекс для быстрого поиска/фильтрации по имени категории
|
||||
late String categoryName;
|
||||
|
||||
late double amount;
|
||||
|
||||
@Index() // Индекс для быстрой сортировки/фильтрации по дате
|
||||
late DateTime date;
|
||||
|
||||
late String merchant;
|
||||
|
||||
// Конструктор (необязателен, но удобен)
|
||||
IsarTransaction({
|
||||
required this.categoryName,
|
||||
required this.amount,
|
||||
required this.date,
|
||||
required this.merchant,
|
||||
});
|
||||
|
||||
// Пустой конструктор для Isar (не используется напрямую)
|
||||
// IsarTransaction();
|
||||
}
|
||||
@@ -1,13 +1,16 @@
|
||||
import 'package:drift/drift.dart' show Value; // Only import Value for optional fields
|
||||
// import 'package:drift/drift.dart' show Value; // Больше не нужно
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fl_chart/fl_chart.dart'; // Used indirectly by SpendingPieChart
|
||||
import 'package:intl/intl.dart'; // For date formatting
|
||||
import 'dart:async';
|
||||
import 'dart:math'; // For random data generation in sample add
|
||||
import 'package:isar/isar.dart'; // Импорт Isar
|
||||
|
||||
import '../database/database.dart' as db; // Import database with prefix 'db'
|
||||
// import '../database/database.dart' as db; // Старый импорт Drift
|
||||
import '../database/database.dart'; // Импортируем наш IsarService
|
||||
import '../models/category.dart'; // Keep Category model for UI structure (SpendingPieChart)
|
||||
// Import TransactionRecord with a different alias to avoid conflict with db.Transaction
|
||||
import '../models/isar_transaction.dart'; // Импортируем Isar модель
|
||||
// Import TransactionRecord with a different alias to avoid conflict
|
||||
import '../models/transaction_record.dart' as model;
|
||||
import '../widgets/summary_item.dart';
|
||||
import '../widgets/expandable_section.dart';
|
||||
@@ -20,13 +23,13 @@ import 'profile_screen.dart'; // Import profile screen
|
||||
class ExpensesScreen extends StatefulWidget {
|
||||
final Function toggleTheme;
|
||||
final bool isDarkMode;
|
||||
final db.AppDatabase database; // Accept database instance
|
||||
final Isar isar; // Принимаем экземпляр Isar
|
||||
|
||||
const ExpensesScreen({
|
||||
Key? key,
|
||||
required this.toggleTheme,
|
||||
required this.isDarkMode,
|
||||
required this.database, // Require database instance
|
||||
required this.isar, // Требуем экземпляр Isar
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
@@ -44,13 +47,19 @@ class _ExpensesScreenState extends State<ExpensesScreen> with TickerProviderStat
|
||||
int _selectedPieIndex = -1; // Index of the selected pie chart slice
|
||||
int _selectedNavIndex = 0; // Index for bottom navigation bar
|
||||
bool _isPieChartExpanded = true; // Controls visibility of the pie chart section
|
||||
bool _isFilterVisible = false; // Controls visibility of the filter chips
|
||||
bool _isFilterVisible = false; // Controls visibility of the filter chip row
|
||||
String _selectedFilter = 'All'; // Currently selected transaction filter
|
||||
|
||||
// Экземпляр нашего сервиса для удобства
|
||||
late IsarService _isarService;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// Создаем экземпляр сервиса, передавая Isar из виджета
|
||||
_isarService = IsarService(widget.isar);
|
||||
|
||||
// Initialize animation controller for pie chart fade/scale effect
|
||||
_pieChartAnimationController = AnimationController(
|
||||
vsync: this,
|
||||
@@ -84,14 +93,16 @@ class _ExpensesScreenState extends State<ExpensesScreen> with TickerProviderStat
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// Stream that provides transactions based on the selected filter
|
||||
Stream<List<db.Transaction>> _watchTransactions() {
|
||||
return widget.database.watchFilteredTransactions(_selectedFilter);
|
||||
// Stream that provides transactions based on the selected filter using IsarService
|
||||
Stream<List<IsarTransaction>> _watchTransactions() {
|
||||
// Используем метод из IsarService
|
||||
return _isarService.watchFilteredTransactions(_selectedFilter);
|
||||
}
|
||||
|
||||
// Stream that provides category totals calculated from transactions
|
||||
// Stream that provides category totals calculated from transactions using IsarService
|
||||
Stream<List<Category>> _watchCategoryTotals() {
|
||||
return widget.database.calculateCategoryTotals();
|
||||
// Используем метод из IsarService
|
||||
return _isarService.calculateCategoryTotals();
|
||||
}
|
||||
|
||||
// Toggles the visibility of the pie chart section with animation
|
||||
@@ -133,7 +144,7 @@ class _ExpensesScreenState extends State<ExpensesScreen> with TickerProviderStat
|
||||
});
|
||||
}
|
||||
|
||||
// --- Function to add a sample transaction (for testing/demo) ---
|
||||
// --- Function to add a sample transaction (using Isar) ---
|
||||
void _addSampleTransaction() async {
|
||||
final random = Random();
|
||||
// Get available category names from our utility class
|
||||
@@ -145,8 +156,8 @@ class _ExpensesScreenState extends State<ExpensesScreen> with TickerProviderStat
|
||||
final randomHour = random.nextInt(24);
|
||||
final randomMerchant = ['Amazon', 'Local Cafe', 'Gas Station', 'Online Store', 'Supermarket'][random.nextInt(5)];
|
||||
|
||||
// Create a companion object for inserting into the Drift table
|
||||
final newTransaction = db.TransactionsCompanion.insert(
|
||||
// Create an IsarTransaction object
|
||||
final newTransaction = IsarTransaction(
|
||||
categoryName: randomCategory,
|
||||
amount: randomAmount,
|
||||
date: DateTime.now().subtract(Duration(days: randomDay, hours: randomHour)),
|
||||
@@ -154,9 +165,9 @@ class _ExpensesScreenState extends State<ExpensesScreen> with TickerProviderStat
|
||||
);
|
||||
|
||||
try {
|
||||
// Insert the transaction into the database
|
||||
await widget.database.addTransaction(newTransaction);
|
||||
print('Sample transaction added: $randomCategory - \$${randomAmount.toStringAsFixed(2)}');
|
||||
// Insert the transaction into the database using IsarService
|
||||
await _isarService.addTransaction(newTransaction);
|
||||
print('Sample transaction added via Isar: $randomCategory - \$${randomAmount.toStringAsFixed(2)}');
|
||||
// Show a confirmation message
|
||||
if (mounted) { // Check if the widget is still in the tree
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
@@ -168,7 +179,7 @@ class _ExpensesScreenState extends State<ExpensesScreen> with TickerProviderStat
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error adding transaction: $e');
|
||||
print('Error adding Isar transaction: $e');
|
||||
// Show an error message
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
@@ -249,10 +260,11 @@ class _ExpensesScreenState extends State<ExpensesScreen> with TickerProviderStat
|
||||
),
|
||||
// Use StreamBuilder to get category totals for the summary and pie chart
|
||||
body: StreamBuilder<List<Category>>(
|
||||
stream: _watchCategoryTotals(),
|
||||
stream: _watchCategoryTotals(), // Используем метод IsarService
|
||||
builder: (context, categorySnapshot) {
|
||||
// Handle loading state
|
||||
if (categorySnapshot.connectionState == ConnectionState.waiting && !categorySnapshot.hasData) {
|
||||
// Показываем индикатор загрузки, если данные еще не пришли, но соединение устанавливается
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
// Handle error state
|
||||
@@ -260,7 +272,8 @@ class _ExpensesScreenState extends State<ExpensesScreen> with TickerProviderStat
|
||||
return Center(child: Text('Error loading categories: ${categorySnapshot.error}'));
|
||||
}
|
||||
|
||||
// Get categories data (or empty list if null)
|
||||
// Get categories data (or empty list if null/error)
|
||||
// Если есть ошибка, categories будет пустым списком, totalExpenses будет 0
|
||||
final categories = categorySnapshot.data ?? [];
|
||||
// Calculate total expenses from the categories stream data
|
||||
final totalExpenses = categories.fold(0.0, (sum, item) => sum + item.amount);
|
||||
@@ -454,7 +467,7 @@ class _ExpensesScreenState extends State<ExpensesScreen> with TickerProviderStat
|
||||
),
|
||||
),
|
||||
|
||||
// --- Transactions List (using StreamBuilder) ---
|
||||
// --- Transactions List (using StreamBuilder with Isar) ---
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||
child: Card( // Wrap list in a Card for background/border
|
||||
@@ -465,11 +478,12 @@ class _ExpensesScreenState extends State<ExpensesScreen> with TickerProviderStat
|
||||
side: BorderSide(color: theme.dividerColor.withOpacity(0.5), width: 1) // Subtle border
|
||||
),
|
||||
clipBehavior: Clip.antiAlias, // Clip list items to card shape
|
||||
child: StreamBuilder<List<db.Transaction>>(
|
||||
stream: _watchTransactions(), // Stream based on _selectedFilter
|
||||
child: StreamBuilder<List<IsarTransaction>>( // Ожидаем List<IsarTransaction>
|
||||
stream: _watchTransactions(), // Stream из IsarService
|
||||
builder: (context, transactionSnapshot) {
|
||||
// Handle loading state for transactions
|
||||
if (transactionSnapshot.connectionState == ConnectionState.waiting) {
|
||||
if (transactionSnapshot.connectionState == ConnectionState.waiting && !transactionSnapshot.hasData) {
|
||||
// Показываем индикатор, если данные еще не пришли
|
||||
return const SizedBox(
|
||||
height: 150, // Placeholder height
|
||||
child: Center(child: CircularProgressIndicator(strokeWidth: 2)),
|
||||
@@ -482,11 +496,11 @@ class _ExpensesScreenState extends State<ExpensesScreen> with TickerProviderStat
|
||||
child: Center(child: Text('Error: ${transactionSnapshot.error}')),
|
||||
);
|
||||
}
|
||||
// Get transaction data (or empty list)
|
||||
final transactions = transactionSnapshot.data ?? [];
|
||||
// Get transaction data (or empty list if null/error)
|
||||
final isarTransactions = transactionSnapshot.data ?? [];
|
||||
|
||||
// Display message if no transactions match the filter
|
||||
if (transactions.isEmpty) {
|
||||
if (isarTransactions.isEmpty) {
|
||||
return SizedBox(
|
||||
height: 150,
|
||||
child: Center(
|
||||
@@ -502,7 +516,7 @@ class _ExpensesScreenState extends State<ExpensesScreen> with TickerProviderStat
|
||||
|
||||
// Use ListView.builder to display transactions efficiently
|
||||
return ListView.separated(
|
||||
itemCount: transactions.length,
|
||||
itemCount: isarTransactions.length,
|
||||
physics: const NeverScrollableScrollPhysics(), // Disable inner scrolling
|
||||
shrinkWrap: true, // Fit content height
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0), // Padding inside the card
|
||||
@@ -511,24 +525,25 @@ class _ExpensesScreenState extends State<ExpensesScreen> with TickerProviderStat
|
||||
color: theme.dividerColor.withOpacity(0.3),
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
// Get the db.Transaction object from the stream
|
||||
final dbTransaction = transactions[index];
|
||||
// Get the IsarTransaction object from the stream
|
||||
final isarTx = isarTransactions[index];
|
||||
// Get the corresponding category details (icon, color)
|
||||
final categoryDetails = CategoryUtils.getCategoryDetails(dbTransaction.categoryName);
|
||||
final categoryDetails = CategoryUtils.getCategoryDetails(isarTx.categoryName);
|
||||
// Create the model.TransactionRecord needed by TransactionListItem
|
||||
// Преобразуем IsarTransaction -> model.TransactionRecord
|
||||
final transactionRecord = model.TransactionRecord(
|
||||
dbTransaction.categoryName,
|
||||
dbTransaction.amount,
|
||||
isarTx.categoryName,
|
||||
isarTx.amount,
|
||||
categoryDetails.iconCode, // Get icon from utils
|
||||
categoryDetails.colorCode, // Get color from utils
|
||||
dbTransaction.date,
|
||||
dbTransaction.merchant,
|
||||
isarTx.date,
|
||||
isarTx.merchant,
|
||||
);
|
||||
|
||||
// Use the TransactionListItem widget with the correct model type
|
||||
return TransactionListItem(
|
||||
transaction: transactionRecord, // Pass the model.TransactionRecord
|
||||
animation: kAlwaysCompleteAnimation, // Required by FadeTransition
|
||||
animation: kAlwaysCompleteAnimation, // Required by FadeTransition (or remove animation)
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -587,7 +602,7 @@ class _ExpensesScreenState extends State<ExpensesScreen> with TickerProviderStat
|
||||
),
|
||||
// Floating Action Button to add new transaction
|
||||
floatingActionButton: FloatingActionButton.extended( // Use extended FAB
|
||||
onPressed: _addSampleTransaction, // Add sample data on press
|
||||
onPressed: _addSampleTransaction, // Add sample data on press (uses Isar now)
|
||||
tooltip: 'Add Transaction',
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('Add'),
|
||||
|
||||
@@ -32,11 +32,13 @@ dependencies:
|
||||
sdk: flutter
|
||||
fl_chart: ^0.71.0
|
||||
intl: ^0.19.0 # For date formatting
|
||||
drift: ^2.26.0 # Added Drift
|
||||
sqlite3_flutter_libs: ^0.5.22 # Recommended for Drift on Flutter
|
||||
# --- Isar Dependencies ---
|
||||
isar: ^3.1.0+1
|
||||
isar_flutter_libs: ^3.1.0+1 # Contains Isar Core for Flutter
|
||||
# --- End Isar Dependencies ---
|
||||
path_provider: ^2.1.3 # To find database file location
|
||||
path: ^1.9.0 # To construct database file path
|
||||
rxdart: ^0.28.0 # Added rxdart for combining streams in database
|
||||
rxdart: ^0.28.0 # Keep for combining streams if needed, or remove if not used elsewhere
|
||||
# The following adds the Cupertino Icons font to your application.
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
cupertino_icons: ^1.0.8
|
||||
@@ -44,7 +46,9 @@ dependencies:
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
drift_dev: ^2.18.0 # Added Drift code generator
|
||||
# --- Isar Generator ---
|
||||
isar_generator: ^3.1.0+1
|
||||
# --- End Isar Generator ---
|
||||
build_runner: ^2.4.11 # Added build_runner
|
||||
|
||||
# The "flutter_lints" package below contains a set of recommended lints to
|
||||
|
||||
Reference in New Issue
Block a user