184 lines
		
	
	
		
			5.1 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			184 lines
		
	
	
		
			5.1 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
import 'dart:convert';
 | 
						|
import 'dart:io';
 | 
						|
import 'dart:math';
 | 
						|
import 'dart:typed_data';
 | 
						|
 | 
						|
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<Expense>;
 | 
						|
 | 
						|
@freezed
 | 
						|
abstract class BaseExpenseInfo with _$BaseExpenseInfo {
 | 
						|
  const BaseExpenseInfo._();
 | 
						|
 | 
						|
  const factory BaseExpenseInfo({
 | 
						|
    required String? label,
 | 
						|
    required double cost,
 | 
						|
    required DateTime time,
 | 
						|
  }) = _BaseExpenseInfo;
 | 
						|
 | 
						|
  /// Get expense time as second
 | 
						|
  int get timeAsSeconds => (time.millisecondsSinceEpoch / 1000).floor();
 | 
						|
}
 | 
						|
 | 
						|
@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<String, dynamic> json) =>
 | 
						|
      _$ExpenseFromJson(json);
 | 
						|
 | 
						|
  /// Get base expense information
 | 
						|
  BaseExpenseInfo get baseExpense =>
 | 
						|
      BaseExpenseInfo(label: label, cost: cost, time: dateTime);
 | 
						|
 | 
						|
  /// 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();
 | 
						|
  }
 | 
						|
 | 
						|
  /// Get expense date
 | 
						|
  DateTime get dateTime => DateTime.fromMillisecondsSinceEpoch(time * 1000);
 | 
						|
}
 | 
						|
 | 
						|
@riverpod
 | 
						|
Future<ExpensesManager> expenses(Ref ref) async => ExpensesManager.instance();
 | 
						|
 | 
						|
class ExpensesManager {
 | 
						|
  final String storagePath;
 | 
						|
 | 
						|
  ExpensesManager._({required this.storagePath});
 | 
						|
 | 
						|
  /// Get an instance of this manager
 | 
						|
  static Future<ExpensesManager> 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<ExpensesList> getList() async {
 | 
						|
    // On first save the list does not exists.
 | 
						|
    if (!await expenseFile.exists()) {
 | 
						|
      return [];
 | 
						|
    }
 | 
						|
 | 
						|
    final jsonDec = jsonDecode(await expenseFile.readAsString());
 | 
						|
    return List<Expense>.from(jsonDec.map((m) => Expense.fromJson(m)));
 | 
						|
  }
 | 
						|
 | 
						|
  /// Save the list of expenses
 | 
						|
  Future<void> 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<void> add({
 | 
						|
    required BaseExpenseInfo info,
 | 
						|
    required List<int> 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.timeAsSeconds,
 | 
						|
      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);
 | 
						|
  }
 | 
						|
 | 
						|
  /// Get a single expense information by its ID
 | 
						|
  Future<Expense?> getById(int id) async {
 | 
						|
    final list = await getList();
 | 
						|
    return list.firstWhere((e) => e.id == id);
 | 
						|
  }
 | 
						|
 | 
						|
  /// Load the file associated with an expense
 | 
						|
  Future<Uint8List> loadFile(Expense expense) async {
 | 
						|
    final path = p.join(filesStoragePath.absolute.path, expense.localFileName);
 | 
						|
    return File(path).readAsBytes();
 | 
						|
  }
 | 
						|
 | 
						|
  /// Update expense information
 | 
						|
  Future<void> updateExpense(Expense expense, BaseExpenseInfo newInfo) async {
 | 
						|
    final list = await getList();
 | 
						|
    final entry = list.indexWhere((e) => e.id == expense.id);
 | 
						|
    list[entry] = Expense(
 | 
						|
      id: expense.id,
 | 
						|
      label: newInfo.label,
 | 
						|
      cost: newInfo.cost,
 | 
						|
      time: newInfo.timeAsSeconds,
 | 
						|
      mimeType: expense.mimeType,
 | 
						|
    );
 | 
						|
    saveList(list);
 | 
						|
  }
 | 
						|
 | 
						|
  /// Delete an expense
 | 
						|
  Future<void> deleteExpense(Expense expense) async {
 | 
						|
    // Remove expense from the list
 | 
						|
    final list = await getList();
 | 
						|
    await saveList(list.where((e) => e.id != expense.id).toList());
 | 
						|
 | 
						|
    // Delete associated file, if any
 | 
						|
    final filePath = File(
 | 
						|
      p.join(filesStoragePath.absolute.path, expense.localFileName),
 | 
						|
    );
 | 
						|
    if (await filePath.exists()) {
 | 
						|
      await filePath.delete();
 | 
						|
    }
 | 
						|
  }
 | 
						|
}
 |