97 lines
3.2 KiB
Dart
97 lines
3.2 KiB
Dart
import 'dart:convert';
|
|
import 'dart:io';
|
|
|
|
import 'package:path/path.dart' as p;
|
|
import 'package:path_provider/path_provider.dart';
|
|
|
|
import '../models/fuel_entry.dart';
|
|
import '../models/vehicle.dart';
|
|
|
|
/// Owns the local, offline staging area: a `data.json` file holding
|
|
/// vehicles and fuel entries, plus a `receipts/` subfolder holding receipt
|
|
/// photos not yet uploaded to Drive. This is always
|
|
/// `ApplicationDocumentsDirectory/FuelTaxTracker` — not user-configurable —
|
|
/// since Drive (via [DriveSyncService]) is now the real shared destination
|
|
/// and this is just a local cache/holding area used while offline.
|
|
class StorageService {
|
|
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.
|
|
Future<void> init() async {
|
|
final docsDir = await getApplicationDocumentsDirectory();
|
|
_saveDirectory = Directory(p.join(docsDir.path, 'FuelTaxTracker'));
|
|
|
|
await _saveDirectory.create(recursive: true);
|
|
await receiptsDirectory.create(recursive: true);
|
|
}
|
|
|
|
Future<({List<Vehicle> vehicles, List<FuelEntry> entries})> loadData() async {
|
|
if (!await dataFile.exists()) {
|
|
return (vehicles: <Vehicle>[], entries: <FuelEntry>[]);
|
|
}
|
|
|
|
final raw = await dataFile.readAsString();
|
|
if (raw.trim().isEmpty) {
|
|
return (vehicles: <Vehicle>[], entries: <FuelEntry>[]);
|
|
}
|
|
|
|
return decodeData(raw);
|
|
}
|
|
|
|
({List<Vehicle> vehicles, List<FuelEntry> entries}) decodeData(String raw) {
|
|
final json = jsonDecode(raw) as Map<String, dynamic>;
|
|
final vehicles = (json['vehicles'] as List<dynamic>? ?? [])
|
|
.map((v) => Vehicle.fromJson(v as Map<String, dynamic>))
|
|
.toList();
|
|
final entries = (json['fuelEntries'] as List<dynamic>? ?? [])
|
|
.map((e) => FuelEntry.fromJson(e as Map<String, dynamic>))
|
|
.toList();
|
|
|
|
return (vehicles: vehicles, entries: entries);
|
|
}
|
|
|
|
String encodeData({
|
|
required List<Vehicle> vehicles,
|
|
required List<FuelEntry> entries,
|
|
}) {
|
|
final json = {
|
|
'vehicles': vehicles.map((v) => v.toJson()).toList(),
|
|
'fuelEntries': entries.map((e) => e.toJson()).toList(),
|
|
};
|
|
return const JsonEncoder.withIndent(' ').convert(json);
|
|
}
|
|
|
|
Future<void> saveData({
|
|
required List<Vehicle> vehicles,
|
|
required List<FuelEntry> entries,
|
|
}) async {
|
|
await dataFile.writeAsString(encodeData(vehicles: vehicles, entries: entries));
|
|
}
|
|
|
|
/// Copies a captured receipt image into the local receipts folder and
|
|
/// returns the path it was stored at, pending upload to Drive.
|
|
Future<String> 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<void> deleteReceiptImage(String? path) async {
|
|
if (path == null) return;
|
|
final file = File(path);
|
|
if (await file.exists()) {
|
|
await file.delete();
|
|
}
|
|
}
|
|
}
|