import 'dart:convert'; import 'dart:io'; import 'dart:math'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'expenses.freezed.dart'; part 'expenses.g.dart'; typedef ExpensesList = List; @freezed abstract class BaseExpenseInfo with _$BaseExpenseInfo { const factory BaseExpenseInfo({ required String label, required double cost, required DateTime time, }) = _BaseExpenseInfo; } @freezed abstract class Expense with _$Expense { const Expense._(); const factory Expense({ /// Internal id used to identify the expense required int id, /// Label of the expense required String? label, /// The cost shall always be a positive value required double cost, /// Time associated with the expense (seconds since epoch) required int time, /// Associated file mime type required String mimeType, }) = _Expense; factory Expense.fromJson(Map json) => _$ExpenseFromJson(json); /// Get associated expense file name String get localFileName { if (mimeType == "application/pdf") return "$id.pdf"; if (mimeType == "image/jpeg") return "$id.jpeg"; if (mimeType == "image/png") return "$id.png"; return id.toString(); } } @riverpod Future expenses(Ref ref) async => ExpensesManager.instance(); class ExpensesManager { final String storagePath; ExpensesManager._({required this.storagePath}); /// Get an instance of this manager static Future instance() async { final appDir = await getApplicationDocumentsDirectory(); final subDir = p.join(appDir.absolute.path, "expenses"); final result = await Directory(subDir).create(recursive: true); return ExpensesManager._(storagePath: result.absolute.path); } /// Get expenses list file path File get expenseFile => File(p.join(storagePath, "list.json")); /// Get the files storage path Directory get filesStoragePath => Directory(p.join(storagePath, "exp_files")); /// Get the current list of expenses Future getList() async { // On first save the list does not exists. if (!await expenseFile.exists()) { return []; } final jsonDec = jsonDecode(await expenseFile.readAsString()); return List.from(jsonDec.map((m) => Expense.fromJson(m))); } /// Save the list of expenses Future saveList(ExpensesList list) async { final jsonDoc = jsonEncode(list.map((t) => t.toJson()).toList()); await expenseFile.writeAsString(jsonDoc); } /// Add a new expense to the list Future add({ required BaseExpenseInfo info, required List fileContent, required String fileMimeType, }) async { final list = await getList(); final exp = Expense( id: (list.lastOrNull?.id ?? 0) + Random().nextInt(1000), label: info.label, cost: info.cost, time: (info.time.millisecondsSinceEpoch / 1000).floor(), mimeType: fileMimeType, ); // Create files storage directory if required if (!await filesStoragePath.exists()) { await filesStoragePath.create(recursive: true); } // Save associated file final file = File( p.join(filesStoragePath.absolute.path, exp.localFileName), ); await file.writeAsBytes(fileContent); // Save the list of expenses list.add(exp); await saveList(list); } }