A whole bunch of changes

This commit is contained in:
Courtney Arnold 2026-08-15 17:52:25 -05:00
parent 901ec678a3
commit 956231048e
122 changed files with 5442 additions and 914 deletions

View file

@ -3,6 +3,7 @@ import 'dart:io';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart' show ThemeMode;
import 'package:shared_preferences/shared_preferences.dart';
import 'package:uuid/uuid.dart';
@ -22,6 +23,7 @@ const _prefsKeyCloudFolderPath = 'cloud_folder_path';
const _prefsKeyKeepReceiptPhotosLocally = 'keep_receipt_photos_locally';
const _prefsKeyStaleLockMinutes = 'stale_lock_minutes';
const _prefsKeyKeepMaxQualityReceiptPhotos = 'keep_max_quality_receipt_photos';
const _prefsKeyThemeMode = 'theme_mode';
const defaultStaleLockMinutes = 10;
const minStaleLockMinutes = 1;
@ -85,6 +87,8 @@ class AppState extends ChangeNotifier {
/// the original camera/gallery image is kept as-is.
bool keepMaxQualityReceiptPhotos = false;
ThemeMode themeMode = ThemeMode.system;
/// Set if [init] fails. The UI shows this (with a retry option) instead
/// of spinning forever — an unhandled exception here previously left
/// `isLoading` stuck at true with no feedback at all.
@ -109,6 +113,10 @@ class AppState extends ChangeNotifier {
staleLockMinutes = prefs.getInt(_prefsKeyStaleLockMinutes) ?? defaultStaleLockMinutes;
keepMaxQualityReceiptPhotos =
prefs.getBool(_prefsKeyKeepMaxQualityReceiptPhotos) ?? false;
themeMode = ThemeMode.values.firstWhere(
(mode) => mode.name == prefs.getString(_prefsKeyThemeMode),
orElse: () => ThemeMode.system,
);
} catch (e) {
initError = e;
isLoading = false;
@ -289,6 +297,13 @@ class AppState extends ChangeNotifier {
notifyListeners();
}
Future<void> setThemeMode(ThemeMode mode) async {
themeMode = mode;
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_prefsKeyThemeMode, mode.name);
notifyListeners();
}
/// Clamped to [minStaleLockMinutes, maxStaleLockMinutes] — see the
/// Settings "Advanced" section, which restricts the picker to that range
/// anyway; this is a defensive backstop for any other caller.
@ -373,6 +388,34 @@ class AppState extends ChangeNotifier {
return entry;
}
/// Corrects the logged values (date, gallons, price/gal, total cost) for
/// an existing fuel entry — the receipt photo itself isn't editable here,
/// only the data recorded about it (e.g. fixing a misread OCR value).
/// [entryId]'s receipt photo reference (local path and/or cloud file id)
/// carries over untouched.
Future<void> updateFuelEntry({
required String entryId,
required DateTime date,
required double gallons,
required double pricePerGallon,
required double totalCost,
}) async {
final existing = fuelEntries.firstWhere((e) => e.id == entryId);
final updated = FuelEntry(
id: existing.id,
vehicleId: existing.vehicleId,
date: date,
gallons: gallons,
pricePerGallon: pricePerGallon,
totalCost: totalCost,
receiptImagePath: existing.receiptImagePath,
receiptDriveFileId: existing.receiptDriveFileId,
updatedAt: DateTime.now().toUtc(),
);
await database.saveFuelEntry(updated);
await _persist();
}
Future<void> deleteFuelEntry(String entryId) async {
final entry = fuelEntries.firstWhere((e) => e.id == entryId);
await database.deleteReceiptImageFile(entry.receiptImagePath);
@ -380,6 +423,30 @@ class AppState extends ChangeNotifier {
await _persist();
}
/// Wipes every vehicle, fuel entry, and local receipt photo — "starting
/// completely fresh". Uses the same soft-delete tombstones as any other
/// delete rather than a hard SQL `DELETE`, so if a cloud sync is
/// connected, the deletion pushes on next sync instead of the old data
/// just getting silently re-imported from the remote copy. This is purely
/// local + whatever propagates through sync — it never reaches into the
/// cloud folder directly to delete anything sitting there.
Future<void> purgeAllData() async {
await database.purgeAllData(DateTime.now().toUtc());
await _persist();
}
/// Wipes every fuel entry (and its local receipt photo) dated within
/// [start]–[end] inclusive, across all vehicles. Vehicles themselves are
/// left alone. Same soft-delete/sync-propagation reasoning as
/// [purgeAllData].
Future<void> purgeFuelEntriesInRange(DateTime start, DateTime end) async {
final localPaths = await database.purgeFuelEntriesInRange(start, end, DateTime.now().toUtc());
for (final path in localPaths) {
await database.deleteReceiptImageFile(path);
}
await _persist();
}
List<FuelEntry> entriesForVehicle(String vehicleId) {
final list = fuelEntries.where((e) => e.vehicleId == vehicleId).toList();
list.sort((a, b) => b.date.compareTo(a.date));
@ -401,6 +468,14 @@ class AppState extends ChangeNotifier {
}
}
FuelEntry? fuelEntryById(String id) {
try {
return fuelEntries.firstWhere((e) => e.id == id);
} catch (_) {
return null;
}
}
/// Exact match against an active vehicle's VIN — same comparison
/// [DatabaseService.vinExists] does, not case-insensitive, since VIN
/// isn't normalized to any particular case on manual entry (only OCR