import 'dart:convert'; import 'dart:io'; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../models/fuel_entry.dart'; import '../models/vehicle.dart'; /// Owns the on-disk layout for the app: a `data.json` file holding vehicles /// and fuel entries, plus a `receipts/` subfolder holding receipt photos. /// The parent directory containing both is user-configurable (see /// [setSaveDirectory]) and persisted across launches via SharedPreferences. class StorageService { static const _prefsKey = 'save_directory_path'; static const _dataFileName = 'fuel_tax_data.json'; static const _receiptsFolderName = 'receipts'; late Directory _saveDirectory; Directory get saveDirectory => _saveDirectory; Directory get receiptsDirectory => Directory(p.join(_saveDirectory.path, _receiptsFolderName)); File get _dataFile => File(p.join(_saveDirectory.path, _dataFileName)); /// Must be called once before any other method. Loads the previously /// chosen save directory, falling back to the app's own documents /// directory the first time the app runs. Future init() async { final prefs = await SharedPreferences.getInstance(); final storedPath = prefs.getString(_prefsKey); if (storedPath != null && await Directory(storedPath).exists()) { _saveDirectory = Directory(storedPath); } else { final docsDir = await getApplicationDocumentsDirectory(); _saveDirectory = Directory(p.join(docsDir.path, 'FuelTaxTracker')); } await _saveDirectory.create(recursive: true); await receiptsDirectory.create(recursive: true); } Future<({List vehicles, List entries})> loadData() async { if (!await _dataFile.exists()) { return (vehicles: [], entries: []); } final raw = await _dataFile.readAsString(); if (raw.trim().isEmpty) { return (vehicles: [], entries: []); } final json = jsonDecode(raw) as Map; final vehicles = (json['vehicles'] as List? ?? []) .map((v) => Vehicle.fromJson(v as Map)) .toList(); final entries = (json['fuelEntries'] as List? ?? []) .map((e) => FuelEntry.fromJson(e as Map)) .toList(); return (vehicles: vehicles, entries: entries); } Future saveData({ required List vehicles, required List entries, }) async { final json = { 'vehicles': vehicles.map((v) => v.toJson()).toList(), 'fuelEntries': entries.map((e) => e.toJson()).toList(), }; await _dataFile.writeAsString(const JsonEncoder.withIndent(' ').convert(json)); } /// Copies a captured receipt image into the receipts folder and returns /// the path it was stored at. Future storeReceiptImage(File sourceImage, String fuelEntryId) async { final ext = p.extension(sourceImage.path); final destPath = p.join(receiptsDirectory.path, '$fuelEntryId$ext'); final copied = await sourceImage.copy(destPath); return copied.path; } Future deleteReceiptImage(String? path) async { if (path == null) return; final file = File(path); if (await file.exists()) { await file.delete(); } } /// Changes where the data file and receipt images live, moving any /// existing data/images from the old location into the new one. Future setSaveDirectory(String newPath) async { final newDir = Directory(newPath); await newDir.create(recursive: true); final newReceiptsDir = Directory(p.join(newPath, _receiptsFolderName)); await newReceiptsDir.create(recursive: true); final oldDataFile = _dataFile; final oldReceiptsDir = receiptsDirectory; if (await oldDataFile.exists() && p.equals(oldDataFile.path, p.join(newPath, _dataFileName)) == false) { await oldDataFile.copy(p.join(newPath, _dataFileName)); } if (await oldReceiptsDir.exists()) { await for (final entity in oldReceiptsDir.list()) { if (entity is File) { final destPath = p.join(newReceiptsDir.path, p.basename(entity.path)); if (!p.equals(entity.path, destPath)) { await entity.copy(destPath); } } } } _saveDirectory = newDir; final prefs = await SharedPreferences.getInstance(); await prefs.setString(_prefsKey, newPath); } }