import 'dart:async'; import 'dart:io'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:flutter/foundation.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:uuid/uuid.dart'; import '../models/fuel_entry.dart'; import '../models/vehicle.dart'; import 'drive_auth_service.dart'; import 'drive_sync_service.dart'; import 'storage_service.dart'; const _prefsKeyDriveFolderId = 'drive_app_folder_id'; const _prefsKeyDriveFolderPath = 'drive_app_folder_path'; /// Single source of truth for the app's in-memory data (vehicles + fuel /// entries), backed by [StorageService] for local persistence and /// [DriveSyncService] for pushing/pulling the shared Drive copy. Screens /// read from this via Provider and call its mutating methods, which write /// through to local disk immediately and kick off a best-effort background /// sync to Drive. class AppState extends ChangeNotifier { final StorageService storage = StorageService(); final DriveAuthService driveAuth = DriveAuthService(); late final DriveSyncService driveSync = DriveSyncService(authService: driveAuth, storageService: storage); final _uuid = const Uuid(); List vehicles = []; List fuelEntries = []; bool isLoading = true; bool isDriveConnected = false; String? driveAccountEmail; String? driveFolderPath; bool isSyncing = false; DateTime? lastSyncedAt; Object? lastSyncError; StreamSubscription>? _connectivitySubscription; Future init() async { await storage.init(); final data = await storage.loadData(); vehicles = data.vehicles; fuelEntries = data.entries; isLoading = false; notifyListeners(); _connectivitySubscription = Connectivity().onConnectivityChanged.listen((results) { if (results.any((r) => r != ConnectivityResult.none)) { unawaited(syncNow()); } }); unawaited(_restoreDriveConnection()); } @override void dispose() { _connectivitySubscription?.cancel(); super.dispose(); } Future _restoreDriveConnection() async { final signedIn = await driveAuth.attemptSilentSignIn(); if (!signedIn) return; isDriveConnected = true; driveAccountEmail = driveAuth.currentAccountEmail; final prefs = await SharedPreferences.getInstance(); final folderId = prefs.getString(_prefsKeyDriveFolderId); driveFolderPath = prefs.getString(_prefsKeyDriveFolderPath); if (folderId != null) { driveSync.configure(folderId); } notifyListeners(); if (folderId != null) { unawaited(syncNow()); } } Future connectDrive() async { final email = await driveAuth.signIn(); isDriveConnected = true; driveAccountEmail = email; notifyListeners(); } Future disconnectDrive() async { await driveAuth.signOut(); driveSync.clearConfiguration(); isDriveConnected = false; driveAccountEmail = null; driveFolderPath = null; final prefs = await SharedPreferences.getInstance(); await prefs.remove(_prefsKeyDriveFolderId); await prefs.remove(_prefsKeyDriveFolderPath); notifyListeners(); } Future chooseDriveFolder({ required String parentId, required String breadcrumbPath, }) async { final folderId = await driveSync.selectAppFolder(parentId); driveFolderPath = breadcrumbPath; final prefs = await SharedPreferences.getInstance(); await prefs.setString(_prefsKeyDriveFolderId, folderId); await prefs.setString(_prefsKeyDriveFolderPath, breadcrumbPath); notifyListeners(); unawaited(syncNow()); } Future syncNow() async { if (isSyncing || !driveSync.isConfigured) return; isSyncing = true; notifyListeners(); final result = await driveSync.syncNow( localVehicles: vehicles, localFuelEntries: fuelEntries, ); if (result.ranSync) { vehicles = result.vehicles!; fuelEntries = result.fuelEntries!; await storage.saveData(vehicles: vehicles, entries: fuelEntries); lastSyncedAt = DateTime.now(); lastSyncError = null; } else if (result.error != null) { lastSyncError = result.error; } isSyncing = false; notifyListeners(); } Future addVehicle({ required String make, required String model, required String color, required String licensePlate, }) async { vehicles.add(Vehicle( id: _uuid.v4(), make: make, model: model, color: color, licensePlate: licensePlate, updatedAt: DateTime.now().toUtc(), )); await _persist(); } Future updateVehicle(Vehicle updated) async { final index = vehicles.indexWhere((v) => v.id == updated.id); if (index != -1) { vehicles[index] = updated.copyWith(updatedAt: DateTime.now().toUtc()); await _persist(); } } Future deleteVehicle(String vehicleId) async { for (final entry in fuelEntries.where((e) => e.vehicleId == vehicleId)) { await storage.deleteReceiptImage(entry.receiptImagePath); } fuelEntries.removeWhere((e) => e.vehicleId == vehicleId); vehicles.removeWhere((v) => v.id == vehicleId); await _persist(); } Future addFuelEntry({ required String vehicleId, required DateTime date, required double gallons, required double pricePerGallon, required double totalCost, File? receiptImage, }) async { final id = _uuid.v4(); String? storedImagePath; if (receiptImage != null) { storedImagePath = await storage.storeReceiptImage(receiptImage, id); } final entry = FuelEntry( id: id, vehicleId: vehicleId, date: date, gallons: gallons, pricePerGallon: pricePerGallon, totalCost: totalCost, receiptImagePath: storedImagePath, updatedAt: DateTime.now().toUtc(), ); fuelEntries.add(entry); await _persist(); return entry; } Future deleteFuelEntry(String entryId) async { final entry = fuelEntries.firstWhere((e) => e.id == entryId); await storage.deleteReceiptImage(entry.receiptImagePath); fuelEntries.removeWhere((e) => e.id == entryId); await _persist(); } List entriesForVehicle(String vehicleId) { final list = fuelEntries.where((e) => e.vehicleId == vehicleId).toList(); list.sort((a, b) => b.date.compareTo(a.date)); return list; } double totalGallonsForVehicle(String vehicleId) => fuelEntries .where((e) => e.vehicleId == vehicleId) .fold(0.0, (sum, e) => sum + e.gallons); double get totalGallonsAllVehicles => fuelEntries.fold(0.0, (sum, e) => sum + e.gallons); Vehicle? vehicleById(String id) { try { return vehicles.firstWhere((v) => v.id == id); } catch (_) { return null; } } Future _persist() async { await storage.saveData(vehicles: vehicles, entries: fuelEntries); notifyListeners(); unawaited(syncNow()); } }