This commit is contained in:
2026-03-22 18:00:44 +03:00
parent 3e4b4db50c
commit 2fe390b068
16 changed files with 3744 additions and 74 deletions
+6
View File
@@ -0,0 +1,6 @@
targets:
$default:
builders:
drift_dev:
options:
store_date_time_values_as_text: true
+256
View File
@@ -0,0 +1,256 @@
import 'dart:io';
import 'package:drift/drift.dart';
import 'package:drift/native.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as p;
import '../../core/utils/result.dart';
import 'tables.dart';
part 'app_database.g.dart';
@DriftDatabase(tables: [Transactions, Categories, Budgets, ExchangeRates])
class AppDatabase extends _$AppDatabase {
AppDatabase() : super(_openConnection());
@override
int get schemaVersion => 1;
// ============================================================================
// TRANSACTIONS
// ============================================================================
/// Get all transactions ordered by date descending
Future<List<dynamic>> getAllTransactions() {
return (select(
transactions,
)..orderBy([(t) => OrderingTerm.desc(t.date)])).get();
}
/// Get transactions by date range
Future<List<dynamic>> getTransactionsByDateRange(
DateTime start,
DateTime end,
) {
return (select(transactions)
..where((t) => t.date.isBiggerOrEqualValue(start))
..where((t) => t.date.isSmallerOrEqualValue(end))
..orderBy([(t) => OrderingTerm.desc(t.date)]))
.get();
}
/// Get transactions by type
Future<List<dynamic>> getTransactionsByType(String type) {
return (select(transactions)
..where((t) => t.type.equals(type))
..orderBy([(t) => OrderingTerm.desc(t.date)]))
.get();
}
/// Get transactions by category
Future<List<dynamic>> getTransactionsByCategory(String category) {
return (select(transactions)
..where((t) => t.category.equals(category))
..orderBy([(t) => OrderingTerm.desc(t.date)]))
.get();
}
/// Search transactions by note or category
Future<List<dynamic>> searchTransactions(String query) {
final lowerQuery = query.toLowerCase();
return (select(transactions)
..where(
(t) =>
t.category.lower().like('%$lowerQuery%') |
t.note.lower().like('%$lowerQuery%'),
)
..orderBy([(t) => OrderingTerm.desc(t.date)]))
.get();
}
/// Get transaction by ID
Future<dynamic> getTransactionById(String id) {
return (select(
transactions,
)..where((t) => t.id.equals(id))).getSingleOrNull();
}
/// Insert transaction
Future<Result<void>> insertTransaction(
TransactionsCompanion transaction,
) async {
return asyncResultOf(() async {
await into(transactions).insert(transaction);
});
}
/// Update transaction
Future<Result<void>> updateTransaction(dynamic transaction) async {
return asyncResultOf(() async {
final companion = transaction as TransactionsCompanion;
final updated = await (update(
transactions,
)..where((t) => t.id.equals(companion.id.value))).write(companion);
if (updated == 0) {
throw Exception('Transaction not found');
}
});
}
/// Delete transaction
Future<Result<void>> deleteTransaction(String id) async {
return asyncResultOf(() async {
final deleted = await (delete(
transactions,
)..where((t) => t.id.equals(id))).go();
if (deleted == 0) {
throw Exception('Transaction not found');
}
});
}
/// Delete all transactions
Future<void> deleteAllTransactions() {
return delete(transactions).go();
}
/// Get recurring transactions that need processing
Future<List<dynamic>> getRecurringTransactions() {
return (select(
transactions,
)..where((t) => t.recurrence.equals('none').not())).get();
}
// ============================================================================
// CATEGORIES
// ============================================================================
/// Get all categories
Future<List<Category>> getAllCategories() {
return select(categories).get();
}
/// Get categories by type
Future<List<Category>> getCategoriesByType(String type) {
return (select(categories)..where((c) => c.type.equals(type))).get();
}
/// Insert category
Future<int> insertCategory(CategoriesCompanion category) {
return into(categories).insert(category);
}
/// Update category
Future<bool> updateCategory(Category category) {
return update(categories).replace(category);
}
/// Delete category
Future<int> deleteCategory(int id) {
return (delete(categories)..where((c) => c.id.equals(id))).go();
}
// ============================================================================
// BUDGETS
// ============================================================================
/// Get budget for month/year
Future<Budget?> getBudget(int month, int year) {
return (select(budgets)
..where((b) => b.month.equals(month) & b.year.equals(year)))
.getSingleOrNull();
}
/// Insert or update budget
Future<void> upsertBudget(BudgetsCompanion budget) {
return into(budgets).insertOnConflictUpdate(budget);
}
/// Delete budget
Future<int> deleteBudget(int id) {
return (delete(budgets)..where((b) => b.id.equals(id))).go();
}
// ============================================================================
// EXCHANGE RATES
// ============================================================================
/// Get exchange rate
Future<ExchangeRate?> getExchangeRate(String from, String to) {
return (select(exchangeRates)
..where((r) => r.fromCurrency.equals(from) & r.toCurrency.equals(to)))
.getSingleOrNull();
}
/// Insert or update exchange rate
Future<void> upsertExchangeRate(ExchangeRatesCompanion rate) {
return into(exchangeRates).insertOnConflictUpdate(rate);
}
/// Get all exchange rates
Future<List<ExchangeRate>> getAllExchangeRates() {
return select(exchangeRates).get();
}
/// Delete old exchange rates (older than 24 hours)
Future<int> deleteOldExchangeRates() {
final yesterday = DateTime.now().subtract(const Duration(hours: 24));
return (delete(
exchangeRates,
)..where((r) => r.updatedAt.isSmallerThanValue(yesterday))).go();
}
// ============================================================================
// STATISTICS & AGGREGATIONS
// ============================================================================
/// Get total balance
Future<double> getTotalBalance() async {
final txs = await getAllTransactions();
return txs.fold<double>(0.0, (sum, tx) {
return tx.type == 'income' ? sum + tx.amount : sum - tx.amount;
});
}
/// Get total income for date range
Future<double> getTotalIncome(DateTime start, DateTime end) async {
final txs = await getTransactionsByDateRange(start, end);
return txs
.where((tx) => tx.type == 'income')
.fold<double>(0.0, (sum, tx) => sum + tx.amount);
}
/// Get total expense for date range
Future<double> getTotalExpense(DateTime start, DateTime end) async {
final txs = await getTransactionsByDateRange(start, end);
return txs
.where((tx) => tx.type == 'expense')
.fold<double>(0.0, (sum, tx) => sum + tx.amount);
}
/// Get category totals for date range
Future<Map<String, double>> getCategoryTotals(
DateTime start,
DateTime end,
String type,
) async {
final txs = await getTransactionsByDateRange(start, end);
final filtered = txs.where((tx) => tx.type == type);
final Map<String, double> totals = {};
for (final tx in filtered) {
totals[tx.category] = (totals[tx.category] ?? 0) + tx.amount;
}
return totals;
}
}
LazyDatabase _openConnection() {
return LazyDatabase(() async {
final dbFolder = await getApplicationDocumentsDirectory();
final file = File(p.join(dbFolder.path, 'casha.db'));
return NativeDatabase(file);
});
}
File diff suppressed because it is too large Load Diff
+49
View File
@@ -0,0 +1,49 @@
import 'package:drift/drift.dart';
/// Transactions table
class Transactions extends Table {
TextColumn get id => text()();
RealColumn get amount => real()();
TextColumn get category => text()();
TextColumn get type => text()(); // 'income' or 'expense'
DateTimeColumn get date => dateTime()();
TextColumn get note => text().nullable()();
TextColumn get recurrence => text().withDefault(const Constant('none'))();
DateTimeColumn get lastOccurrence => dateTime().nullable()();
TextColumn get currency => text().withDefault(const Constant('\$'))();
TextColumn get currencyCode => text().withDefault(const Constant('USD'))();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
@override
Set<Column> get primaryKey => {id};
}
/// Categories table for custom categories
class Categories extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get name => text().withLength(min: 1, max: 50)();
TextColumn get type => text()(); // 'income' or 'expense'
TextColumn get icon => text().nullable()();
TextColumn get color => text().nullable()();
BoolColumn get isDefault => boolean().withDefault(const Constant(false))();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
}
/// Budgets table for monthly budgets
class Budgets extends Table {
IntColumn get id => integer().autoIncrement()();
RealColumn get amount => real()();
TextColumn get categoryId => text().nullable()();
IntColumn get month => integer()();
IntColumn get year => integer()();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
}
/// Exchange rates cache
class ExchangeRates extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get fromCurrency => text()();
TextColumn get toCurrency => text()();
RealColumn get rate => real()();
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
}
@@ -0,0 +1,195 @@
import 'package:drift/drift.dart';
import '../../core/utils/result.dart';
import '../../shared/models/transaction.dart' as model;
import '../database/app_database.dart';
class TransactionRepository {
final AppDatabase _db;
TransactionRepository(this._db);
/// Get all transactions
Future<Result<List<model.Transaction>>> getAll() async {
return asyncResultOf(() async {
final transactions = await _db.getAllTransactions();
return transactions.map<model.Transaction>(_toModel).toList();
});
}
/// Get transactions by date range
Future<Result<List<model.Transaction>>> getByDateRange(
DateTime start,
DateTime end,
) async {
return asyncResultOf(() async {
final transactions = await _db.getTransactionsByDateRange(start, end);
return transactions.map<model.Transaction>(_toModel).toList();
});
}
/// Get transactions by type
Future<Result<List<model.Transaction>>> getByType(
model.TransactionType type,
) async {
return asyncResultOf(() async {
final transactions = await _db.getTransactionsByType(type.name);
return transactions.map<model.Transaction>(_toModel).toList();
});
}
/// Search transactions
Future<Result<List<model.Transaction>>> search(String query) async {
return asyncResultOf(() async {
final transactions = await _db.searchTransactions(query);
return transactions.map<model.Transaction>(_toModel).toList();
});
}
/// Get transaction by ID
Future<Result<model.Transaction?>> getById(String id) async {
return asyncResultOf(() async {
final transaction = await _db.getTransactionById(id);
return transaction != null ? _toModel(transaction) : null;
});
}
/// Add transaction
Future<Result<void>> add(model.Transaction transaction) async {
return asyncResultOf(() async {
final companion = _toCompanion(transaction);
final result = await _db.insertTransaction(companion);
if (result.isFailure) {
throw Exception(result.errorOrNull);
}
});
}
/// Update transaction
Future<Result<void>> update(model.Transaction transaction) async {
return asyncResultOf(() async {
final dbTransaction = _toDbModel(transaction);
final result = await _db.updateTransaction(dbTransaction);
if (result.isFailure) {
throw Exception(result.errorOrNull);
}
});
}
/// Delete transaction
Future<Result<void>> delete(String id) async {
return _db.deleteTransaction(id);
}
/// Delete all transactions
Future<Result<void>> deleteAll() async {
return asyncResultOf(() async {
await _db.deleteAllTransactions();
});
}
/// Get recurring transactions
Future<Result<List<model.Transaction>>> getRecurring() async {
return asyncResultOf(() async {
final transactions = await _db.getRecurringTransactions();
return transactions.map<model.Transaction>(_toModel).toList();
});
}
/// Get total balance
Future<Result<double>> getTotalBalance() async {
return asyncResultOf(() async {
return await _db.getTotalBalance();
});
}
/// Get total income for date range
Future<Result<double>> getTotalIncome(DateTime start, DateTime end) async {
return asyncResultOf(() async {
return await _db.getTotalIncome(start, end);
});
}
/// Get total expense for date range
Future<Result<double>> getTotalExpense(DateTime start, DateTime end) async {
return asyncResultOf(() async {
return await _db.getTotalExpense(start, end);
});
}
/// Get category totals
Future<Result<Map<String, double>>> getCategoryTotals(
DateTime start,
DateTime end,
model.TransactionType type,
) async {
return asyncResultOf(() async {
return await _db.getCategoryTotals(start, end, type.name);
});
}
// ============================================================================
// CONVERTERS
// ============================================================================
/// Convert database model to app model
model.Transaction _toModel(dynamic dbTransaction) {
return model.Transaction(
id: dbTransaction.id as String,
amount: dbTransaction.amount as double,
category: dbTransaction.category as String,
type: (dbTransaction.type as String) == 'income'
? model.TransactionType.income
: model.TransactionType.expense,
date: dbTransaction.date as DateTime,
note: dbTransaction.note as String?,
recurrence: _parseRecurrence(dbTransaction.recurrence as String),
lastOccurrence: dbTransaction.lastOccurrence as DateTime?,
currency: dbTransaction.currency as String,
currencyCode: dbTransaction.currencyCode as String,
);
}
/// Convert app model to database model
dynamic _toDbModel(model.Transaction transaction) {
// This will be replaced with proper TransactionData after code generation
return TransactionsCompanion(
id: Value(transaction.id),
amount: Value(transaction.amount),
category: Value(transaction.category),
type: Value(transaction.type.name),
date: Value(transaction.date),
note: Value(transaction.note),
recurrence: Value(transaction.recurrence.name),
lastOccurrence: Value(transaction.lastOccurrence),
currency: Value(transaction.currency),
currencyCode: Value(transaction.currencyCode),
createdAt: Value(DateTime.now()),
);
}
/// Convert app model to companion for insert
TransactionsCompanion _toCompanion(model.Transaction transaction) {
return TransactionsCompanion(
id: Value(transaction.id),
amount: Value(transaction.amount),
category: Value(transaction.category),
type: Value(transaction.type.name),
date: Value(transaction.date),
note: Value(transaction.note),
recurrence: Value(transaction.recurrence.name),
lastOccurrence: Value(transaction.lastOccurrence),
currency: Value(transaction.currency),
currencyCode: Value(transaction.currencyCode),
);
}
/// Parse recurrence type
model.RecurrenceType _parseRecurrence(String recurrence) {
return model.RecurrenceType.values.firstWhere(
(e) => e.name == recurrence,
orElse: () => model.RecurrenceType.none,
);
}
}
+12 -9
View File
@@ -3,37 +3,40 @@ import '../../shared/models/transaction.dart';
import '../dashboard/provider.dart'; import '../dashboard/provider.dart';
final categoryExpenseProvider = Provider<Map<String, double>>((ref) { final categoryExpenseProvider = Provider<Map<String, double>>((ref) {
final txs = ref.watch(transactionsProvider) final txsAsync = ref.watch(transactionsProvider);
.where((t) => t.type == TransactionType.expense); final txs = txsAsync.valueOrNull ?? [];
final filtered = txs.where((t) => t.type == TransactionType.expense);
final map = <String, double>{}; final map = <String, double>{};
for (final t in txs) { for (final t in filtered) {
map[t.category] = (map[t.category] ?? 0) + t.amount; map[t.category] = (map[t.category] ?? 0) + t.amount;
} }
return map; return map;
}); });
final categoryIncomeProvider = Provider<Map<String, double>>((ref) { final categoryIncomeProvider = Provider<Map<String, double>>((ref) {
final txs = ref.watch(transactionsProvider) final txsAsync = ref.watch(transactionsProvider);
.where((t) => t.type == TransactionType.income); final txs = txsAsync.valueOrNull ?? [];
final filtered = txs.where((t) => t.type == TransactionType.income);
final map = <String, double>{}; final map = <String, double>{};
for (final t in txs) { for (final t in filtered) {
map[t.category] = (map[t.category] ?? 0) + t.amount; map[t.category] = (map[t.category] ?? 0) + t.amount;
} }
return map; return map;
}); });
final monthlyBreakdownProvider = Provider<List<MonthlyData>>((ref) { final monthlyBreakdownProvider = Provider<List<MonthlyData>>((ref) {
final txs = ref.watch(transactionsProvider) final txsAsync = ref.watch(transactionsProvider);
.where((t) => t.type == TransactionType.expense); final txs = txsAsync.valueOrNull ?? [];
final filtered = txs.where((t) => t.type == TransactionType.expense);
final now = DateTime.now(); final now = DateTime.now();
final months = <MonthlyData>[]; final months = <MonthlyData>[];
for (var i = 5; i >= 0; i--) { for (var i = 5; i >= 0; i--) {
final month = DateTime(now.year, now.month - i, 1); final month = DateTime(now.year, now.month - i, 1);
final total = txs final total = filtered
.where((t) => t.date.year == month.year && t.date.month == month.month) .where((t) => t.date.year == month.year && t.date.month == month.month)
.fold(0.0, (sum, t) => sum + t.amount); .fold(0.0, (sum, t) => sum + t.amount);
months.add(MonthlyData(month: month, amount: total)); months.add(MonthlyData(month: month, amount: total));
+83 -46
View File
@@ -3,6 +3,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import '../../core/services/card_color_service.dart'; import '../../core/services/card_color_service.dart';
import '../../core/utils/result.dart'; import '../../core/utils/result.dart';
import '../../data/database/app_database.dart' as db;
import '../../data/repositories/transaction_repository.dart';
import '../../shared/models/transaction.dart'; import '../../shared/models/transaction.dart';
import '../../shared/services/storage_service.dart'; import '../../shared/services/storage_service.dart';
import '../settings/provider.dart'; import '../settings/provider.dart';
@@ -11,53 +13,86 @@ final sharedPreferencesProvider = Provider<SharedPreferences>((ref) {
throw UnimplementedError('Override in main'); throw UnimplementedError('Override in main');
}); });
final appDatabaseProvider = Provider<db.AppDatabase>((ref) {
return db.AppDatabase();
});
final transactionRepositoryProvider = Provider<TransactionRepository>((ref) {
final db = ref.watch(appDatabaseProvider);
return TransactionRepository(db);
});
final storageServiceProvider = Provider<StorageService>((ref) { final storageServiceProvider = Provider<StorageService>((ref) {
return StorageService(ref.watch(sharedPreferencesProvider)); return StorageService(ref.watch(sharedPreferencesProvider));
}); });
final transactionsProvider = final transactionsProvider =
StateNotifierProvider<TransactionsNotifier, List<Transaction>>((ref) { StateNotifierProvider<TransactionsNotifier, AsyncValue<List<Transaction>>>((
final storage = ref.watch(storageServiceProvider); ref,
return TransactionsNotifier(storage); ) {
final repository = ref.watch(transactionRepositoryProvider);
return TransactionsNotifier(repository);
}); });
class TransactionsNotifier extends StateNotifier<List<Transaction>> { class TransactionsNotifier
final StorageService _storage; extends StateNotifier<AsyncValue<List<Transaction>>> {
final TransactionRepository _repository;
TransactionsNotifier(this._storage) TransactionsNotifier(this._repository) : super(const AsyncValue.loading()) {
: super(_storage.loadTransactionsUnsafe()); _load();
}
Future<void> _load() async {
state = const AsyncValue.loading();
final result = await _repository.getAll();
state = result.isSuccess
? AsyncValue.data(result.dataOrNull!)
: AsyncValue.error(result.errorOrNull!, StackTrace.current);
}
Future<Result<void>> add(Transaction transaction) async { Future<Result<void>> add(Transaction transaction) async {
final result = await _storage.addTransaction(transaction); final result = await _repository.add(transaction);
return result.onSuccess((_) {
state = _storage.loadTransactionsUnsafe(); if (result.isSuccess) {
}); await _load();
}
return result;
} }
Future<Result<void>> update(Transaction transaction) async { Future<Result<void>> update(Transaction transaction) async {
final result = await _storage.updateTransaction(transaction); final result = await _repository.update(transaction);
return result.onSuccess((_) {
state = _storage.loadTransactionsUnsafe(); if (result.isSuccess) {
}); await _load();
}
return result;
} }
Future<Result<void>> delete(String id) async { Future<Result<void>> delete(String id) async {
final result = await _storage.deleteTransaction(id); final result = await _repository.delete(id);
return result.onSuccess((_) {
state = _storage.loadTransactionsUnsafe(); if (result.isSuccess) {
}); await _load();
}
return result;
} }
void restore(Transaction transaction) { Future<void> restore(Transaction transaction) async {
state = [...state, transaction]; await _repository.add(transaction);
_storage.addTransaction(transaction); await _load();
} }
void clearAll() { Future<void> clearAll() async {
state = []; await _repository.deleteAll();
SharedPreferences.getInstance().then( state = const AsyncValue.data([]);
(prefs) => prefs.remove('transactions'), }
);
Future<void> refresh() async {
await _load();
} }
} }
@@ -76,7 +111,8 @@ final timeFilterProvider = StateProvider<TimeFilter>(
); );
final totalBalanceProvider = Provider<double>((ref) { final totalBalanceProvider = Provider<double>((ref) {
final txs = ref.watch(transactionsProvider); final txsAsync = ref.watch(transactionsProvider);
final txs = txsAsync.valueOrNull ?? [];
final exchangeService = ref.watch(exchangeRateServiceProvider); final exchangeService = ref.watch(exchangeRateServiceProvider);
final targetCurrency = ref.watch(currencyProvider).code; final targetCurrency = ref.watch(currencyProvider).code;
@@ -91,26 +127,26 @@ final totalBalanceProvider = Provider<double>((ref) {
}); });
final totalIncomeProvider = Provider<double>((ref) { final totalIncomeProvider = Provider<double>((ref) {
final txs = ref final txsAsync = ref.watch(transactionsProvider);
.watch(transactionsProvider) final txs = txsAsync.valueOrNull ?? [];
.where((t) => t.type == TransactionType.income); final filtered = txs.where((t) => t.type == TransactionType.income);
final exchangeService = ref.watch(exchangeRateServiceProvider); final exchangeService = ref.watch(exchangeRateServiceProvider);
final targetCurrency = ref.watch(currencyProvider).code; final targetCurrency = ref.watch(currencyProvider).code;
return txs.fold(0.0, (sum, t) { return filtered.fold(0.0, (sum, t) {
return sum + return sum +
exchangeService.convert(t.amount, t.currencyCode, targetCurrency); exchangeService.convert(t.amount, t.currencyCode, targetCurrency);
}); });
}); });
final totalExpenseProvider = Provider<double>((ref) { final totalExpenseProvider = Provider<double>((ref) {
final txs = ref final txsAsync = ref.watch(transactionsProvider);
.watch(transactionsProvider) final txs = txsAsync.valueOrNull ?? [];
.where((t) => t.type == TransactionType.expense); final filtered = txs.where((t) => t.type == TransactionType.expense);
final exchangeService = ref.watch(exchangeRateServiceProvider); final exchangeService = ref.watch(exchangeRateServiceProvider);
final targetCurrency = ref.watch(currencyProvider).code; final targetCurrency = ref.watch(currencyProvider).code;
return txs.fold(0.0, (sum, t) { return filtered.fold(0.0, (sum, t) {
return sum + return sum +
exchangeService.convert(t.amount, t.currencyCode, targetCurrency); exchangeService.convert(t.amount, t.currencyCode, targetCurrency);
}); });
@@ -118,25 +154,26 @@ final totalExpenseProvider = Provider<double>((ref) {
final currentMonthExpenseProvider = Provider<double>((ref) { final currentMonthExpenseProvider = Provider<double>((ref) {
final now = DateTime.now(); final now = DateTime.now();
final txs = ref final txsAsync = ref.watch(transactionsProvider);
.watch(transactionsProvider) final txs = txsAsync.valueOrNull ?? [];
.where( final filtered = txs.where(
(t) => (t) =>
t.type == TransactionType.expense && t.type == TransactionType.expense &&
t.date.year == now.year && t.date.year == now.year &&
t.date.month == now.month, t.date.month == now.month,
); );
final exchangeService = ref.watch(exchangeRateServiceProvider); final exchangeService = ref.watch(exchangeRateServiceProvider);
final targetCurrency = ref.watch(currencyProvider).code; final targetCurrency = ref.watch(currencyProvider).code;
return txs.fold(0.0, (sum, t) { return filtered.fold(0.0, (sum, t) {
return sum + return sum +
exchangeService.convert(t.amount, t.currencyCode, targetCurrency); exchangeService.convert(t.amount, t.currencyCode, targetCurrency);
}); });
}); });
final filteredTransactionsProvider = Provider<List<Transaction>>((ref) { final filteredTransactionsProvider = Provider<List<Transaction>>((ref) {
final txs = ref.watch(transactionsProvider); final txsAsync = ref.watch(transactionsProvider);
final txs = txsAsync.valueOrNull ?? [];
final query = ref.watch(searchQueryProvider).toLowerCase(); final query = ref.watch(searchQueryProvider).toLowerCase();
final typeFilter = ref.watch(transactionFilterProvider); final typeFilter = ref.watch(transactionFilterProvider);
final timeFilter = ref.watch(timeFilterProvider); final timeFilter = ref.watch(timeFilterProvider);
+22 -15
View File
@@ -26,7 +26,11 @@ class BudgetNotifier extends StateNotifier<double?> {
state = budget; state = budget;
} }
void onCurrencyChanged(String oldCode, String newCode, ExchangeRateService rates) { void onCurrencyChanged(
String oldCode,
String newCode,
ExchangeRateService rates,
) {
if (state == null) return; if (state == null) return;
final converted = rates.convert(state!, oldCode, newCode); final converted = rates.convert(state!, oldCode, newCode);
setBudget(converted); setBudget(converted);
@@ -64,12 +68,12 @@ class CurrencyNotifier extends StateNotifier<CurrencyInfo> {
} }
} }
final currencyProvider = StateNotifierProvider<CurrencyNotifier, CurrencyInfo>( final currencyProvider = StateNotifierProvider<CurrencyNotifier, CurrencyInfo>((
(ref) { ref,
final prefs = ref.watch(sharedPreferencesProvider); ) {
return CurrencyNotifier(prefs); final prefs = ref.watch(sharedPreferencesProvider);
}, return CurrencyNotifier(prefs);
); });
class ThemeModeNotifier extends StateNotifier<ThemeMode> { class ThemeModeNotifier extends StateNotifier<ThemeMode> {
final SharedPreferences _prefs; final SharedPreferences _prefs;
@@ -95,12 +99,12 @@ class ThemeModeNotifier extends StateNotifier<ThemeMode> {
} }
} }
final themeProvider = StateNotifierProvider<ThemeModeNotifier, ThemeMode>( final themeProvider = StateNotifierProvider<ThemeModeNotifier, ThemeMode>((
(ref) { ref,
final prefs = ref.watch(sharedPreferencesProvider); ) {
return ThemeModeNotifier(prefs); final prefs = ref.watch(sharedPreferencesProvider);
}, return ThemeModeNotifier(prefs);
); });
final exchangeRateServiceProvider = Provider<ExchangeRateService>((ref) { final exchangeRateServiceProvider = Provider<ExchangeRateService>((ref) {
final prefs = ref.watch(sharedPreferencesProvider); final prefs = ref.watch(sharedPreferencesProvider);
@@ -111,7 +115,9 @@ final ratesInitProvider = FutureProvider<void>((ref) async {
await ref.read(exchangeRateServiceProvider).fetchRates(); await ref.read(exchangeRateServiceProvider).fetchRates();
}); });
final hapticEnabledProvider = StateNotifierProvider<HapticNotifier, bool>((ref) { final hapticEnabledProvider = StateNotifierProvider<HapticNotifier, bool>((
ref,
) {
return HapticNotifier(); return HapticNotifier();
}); });
@@ -141,7 +147,8 @@ class ExportService {
ExportService(this._ref); ExportService(this._ref);
Future<String> exportToCSV() async { Future<String> exportToCSV() async {
final transactions = _ref.read(transactionsProvider); final transactionsAsync = _ref.read(transactionsProvider);
final transactions = transactionsAsync.valueOrNull ?? [];
final currency = _ref.read(currencyProvider); final currency = _ref.read(currencyProvider);
final fmt = _ref.read(amountFormatProvider); final fmt = _ref.read(amountFormatProvider);
+8 -3
View File
@@ -1,15 +1,15 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:intl/date_symbol_data_local.dart'; import 'package:intl/date_symbol_data_local.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'app/app.dart'; import 'app/app.dart';
import 'core/services/haptic_service.dart'; import 'core/services/haptic_service.dart';
import 'data/database/app_database.dart';
import 'features/dashboard/provider.dart'; import 'features/dashboard/provider.dart';
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
await initializeDateFormatting('en_US', null); await initializeDateFormatting('en_US', null);
await initializeDateFormatting('ru_RU', null); await initializeDateFormatting('ru_RU', null);
await initializeDateFormatting('en', null); await initializeDateFormatting('en', null);
@@ -18,9 +18,14 @@ void main() async {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
await HapticService.init(); await HapticService.init();
final database = AppDatabase();
runApp( runApp(
ProviderScope( ProviderScope(
overrides: [sharedPreferencesProvider.overrideWithValue(prefs)], overrides: [
sharedPreferencesProvider.overrideWithValue(prefs),
appDatabaseProvider.overrideWithValue(database),
],
child: const App(), child: const App(),
), ),
); );
@@ -6,6 +6,10 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) { void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin");
sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar);
} }
+1
View File
@@ -3,6 +3,7 @@
# #
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
sqlite3_flutter_libs
) )
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST
@@ -7,8 +7,10 @@ import Foundation
import local_auth_darwin import local_auth_darwin
import shared_preferences_foundation import shared_preferences_foundation
import sqlite3_flutter_libs
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
LocalAuthPlugin.register(with: registry.registrar(forPlugin: "LocalAuthPlugin")) LocalAuthPlugin.register(with: registry.registrar(forPlugin: "LocalAuthPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin"))
} }
+257 -1
View File
@@ -1,6 +1,22 @@
# Generated by pub # Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile # See https://dart.dev/tools/pub/glossary#lockfile
packages: packages:
_fe_analyzer_shared:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: "8d7ff3948166b8ec5da0fbb5962000926b8e02f2ed9b3e51d1738905fbd4c98d"
url: "https://pub.dev"
source: hosted
version: "93.0.0"
analyzer:
dependency: transitive
description:
name: analyzer
sha256: de7148ed2fcec579b19f122c1800933dfa028f6d9fd38a152b04b1516cec120b
url: "https://pub.dev"
source: hosted
version: "10.0.1"
ansicolor: ansicolor:
dependency: transitive dependency: transitive
description: description:
@@ -41,6 +57,54 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.2" version: "2.1.2"
build:
dependency: transitive
description:
name: build
sha256: aadd943f4f8cc946882c954c187e6115a84c98c81ad1d9c6cbf0895a8c85da9c
url: "https://pub.dev"
source: hosted
version: "4.0.5"
build_config:
dependency: transitive
description:
name: build_config
sha256: "4070d2a59f8eec34c97c86ceb44403834899075f66e8a9d59706f8e7834f6f71"
url: "https://pub.dev"
source: hosted
version: "1.3.0"
build_daemon:
dependency: transitive
description:
name: build_daemon
sha256: bf05f6e12cfea92d3c09308d7bcdab1906cd8a179b023269eed00c071004b957
url: "https://pub.dev"
source: hosted
version: "4.1.1"
build_runner:
dependency: "direct dev"
description:
name: build_runner
sha256: "521daf8d189deb79ba474e43a696b41c49fb3987818dbacf3308f1e03673a75e"
url: "https://pub.dev"
source: hosted
version: "2.13.1"
built_collection:
dependency: transitive
description:
name: built_collection
sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100"
url: "https://pub.dev"
source: hosted
version: "5.1.1"
built_value:
dependency: transitive
description:
name: built_value
sha256: "6ae8a6435a8c6520c7077b107e77f1fb4ba7009633259a4d49a8afd8e7efc5e9"
url: "https://pub.dev"
source: hosted
version: "8.12.4"
characters: characters:
dependency: transitive dependency: transitive
description: description:
@@ -49,6 +113,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.1" version: "1.4.1"
charcode:
dependency: transitive
description:
name: charcode
sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a
url: "https://pub.dev"
source: hosted
version: "1.4.0"
checked_yaml: checked_yaml:
dependency: transitive dependency: transitive
description: description:
@@ -81,6 +153,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.0" version: "1.0.0"
code_builder:
dependency: transitive
description:
name: code_builder
sha256: "6a6cab2ba4680d6423f34a9b972a4c9a94ebe1b62ecec4e1a1f2cba91fd1319d"
url: "https://pub.dev"
source: hosted
version: "4.11.1"
collection: collection:
dependency: transitive dependency: transitive
description: description:
@@ -89,6 +169,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.19.1" version: "1.19.1"
convert:
dependency: transitive
description:
name: convert
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
url: "https://pub.dev"
source: hosted
version: "3.1.2"
crypto: crypto:
dependency: transitive dependency: transitive
description: description:
@@ -113,6 +201,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.8" version: "1.0.8"
dart_style:
dependency: transitive
description:
name: dart_style
sha256: "29f7ecc274a86d32920b1d9cfc7502fa87220da41ec60b55f329559d5732e2b2"
url: "https://pub.dev"
source: hosted
version: "3.1.7"
drift:
dependency: "direct main"
description:
name: drift
sha256: "61f876c0291b194980bafd203f48e85d5fb04e4a7334367d1a89f44004dbcb83"
url: "https://pub.dev"
source: hosted
version: "2.32.0"
drift_dev:
dependency: "direct dev"
description:
name: drift_dev
sha256: d687e955cc4b1706ad49b3860fcc1045c09bbf1d84c3c7383615f7f9c3320aa2
url: "https://pub.dev"
source: hosted
version: "2.32.0"
equatable: equatable:
dependency: transitive dependency: transitive
description: description:
@@ -248,6 +360,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.3.3" version: "6.3.3"
graphs:
dependency: transitive
description:
name: graphs
sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
hooks: hooks:
dependency: transitive dependency: transitive
description: description:
@@ -272,6 +392,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.6.0" version: "1.6.0"
http_multi_server:
dependency: transitive
description:
name: http_multi_server
sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8
url: "https://pub.dev"
source: hosted
version: "3.2.2"
http_parser: http_parser:
dependency: transitive dependency: transitive
description: description:
@@ -296,6 +424,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.19.0" version: "0.19.0"
io:
dependency: transitive
description:
name: io
sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
url: "https://pub.dev"
source: hosted
version: "1.0.5"
json_annotation: json_annotation:
dependency: transitive dependency: transitive
description: description:
@@ -408,6 +544,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.17.0" version: "1.17.0"
mime:
dependency: transitive
description:
name: mime
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
native_toolchain_c: native_toolchain_c:
dependency: transitive dependency: transitive
description: description:
@@ -424,8 +568,16 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "9.3.0" version: "9.3.0"
path: package_config:
dependency: transitive dependency: transitive
description:
name: package_config
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
url: "https://pub.dev"
source: hosted
version: "2.2.0"
path:
dependency: "direct main"
description: description:
name: path name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
@@ -504,6 +656,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.8" version: "2.1.8"
pool:
dependency: transitive
description:
name: pool
sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d"
url: "https://pub.dev"
source: hosted
version: "1.5.2"
posix: posix:
dependency: transitive dependency: transitive
description: description:
@@ -520,6 +680,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.0" version: "2.2.0"
pubspec_parse:
dependency: transitive
description:
name: pubspec_parse
sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082"
url: "https://pub.dev"
source: hosted
version: "1.5.0"
recase:
dependency: transitive
description:
name: recase
sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213
url: "https://pub.dev"
source: hosted
version: "4.1.0"
riverpod: riverpod:
dependency: transitive dependency: transitive
description: description:
@@ -600,11 +776,35 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.1" version: "2.4.1"
shelf:
dependency: transitive
description:
name: shelf
sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
url: "https://pub.dev"
source: hosted
version: "1.4.2"
shelf_web_socket:
dependency: transitive
description:
name: shelf_web_socket
sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925"
url: "https://pub.dev"
source: hosted
version: "3.0.0"
sky_engine: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
source_gen:
dependency: transitive
description:
name: source_gen
sha256: "732792cfd197d2161a65bb029606a46e0a18ff30ef9e141a7a82172b05ea8ecd"
url: "https://pub.dev"
source: hosted
version: "4.2.2"
source_span: source_span:
dependency: transitive dependency: transitive
description: description:
@@ -613,6 +813,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.10.2" version: "1.10.2"
sqlite3:
dependency: transitive
description:
name: sqlite3
sha256: caa693ad15a587a2b4fde093b728131a1827903872171089dedb16f7665d3a91
url: "https://pub.dev"
source: hosted
version: "3.2.0"
sqlite3_flutter_libs:
dependency: "direct main"
description:
name: sqlite3_flutter_libs
sha256: eeb9e3a45207649076b808f8a5a74d68770d0b7f26ccef6d5f43106eee5375ad
url: "https://pub.dev"
source: hosted
version: "0.5.42"
sqlparser:
dependency: transitive
description:
name: sqlparser
sha256: ab2b467425f1d4f3acfa5fd11a08226f7d6c26ff102c06be1807e1dff34e050b
url: "https://pub.dev"
source: hosted
version: "0.44.3"
stack_trace: stack_trace:
dependency: transitive dependency: transitive
description: description:
@@ -637,6 +861,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.4" version: "2.1.4"
stream_transform:
dependency: transitive
description:
name: stream_transform
sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871
url: "https://pub.dev"
source: hosted
version: "2.1.1"
string_scanner: string_scanner:
dependency: transitive dependency: transitive
description: description:
@@ -701,6 +933,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "15.0.2" version: "15.0.2"
watcher:
dependency: transitive
description:
name: watcher
sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
web: web:
dependency: transitive dependency: transitive
description: description:
@@ -709,6 +949,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.1" version: "1.1.1"
web_socket:
dependency: transitive
description:
name: web_socket
sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
web_socket_channel:
dependency: transitive
description:
name: web_socket_channel
sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8
url: "https://pub.dev"
source: hosted
version: "3.0.3"
xdg_directories: xdg_directories:
dependency: transitive dependency: transitive
description: description:
+5
View File
@@ -22,6 +22,9 @@ dependencies:
sensors_plus: ^6.1.0 sensors_plus: ^6.1.0
local_auth: ^2.3.0 local_auth: ^2.3.0
flutter_colorpicker: ^1.1.0 flutter_colorpicker: ^1.1.0
drift: ^2.14.1
sqlite3_flutter_libs: ^0.5.20
path: ^1.8.3
flutter_launcher_icons: flutter_launcher_icons:
android: true android: true
@@ -50,6 +53,8 @@ dev_dependencies:
flutter_lints: ^6.0.0 flutter_lints: ^6.0.0
flutter_launcher_icons: ^0.14.1 flutter_launcher_icons: ^0.14.1
flutter_native_splash: ^2.4.3 flutter_native_splash: ^2.4.3
drift_dev: ^2.14.1
build_runner: ^2.4.7
flutter: flutter:
uses-material-design: true uses-material-design: true
@@ -7,8 +7,11 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <local_auth_windows/local_auth_plugin.h> #include <local_auth_windows/local_auth_plugin.h>
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
void RegisterPlugins(flutter::PluginRegistry* registry) { void RegisterPlugins(flutter::PluginRegistry* registry) {
LocalAuthPluginRegisterWithRegistrar( LocalAuthPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("LocalAuthPlugin")); registry->GetRegistrarForPlugin("LocalAuthPlugin"));
Sqlite3FlutterLibsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin"));
} }
+1
View File
@@ -4,6 +4,7 @@
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
local_auth_windows local_auth_windows
sqlite3_flutter_libs
) )
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST