breakup app_state

This commit is contained in:
stopflock
2025-08-21 18:39:09 -05:00
parent 865f91ea55
commit df5e26f78d
9 changed files with 767 additions and 422 deletions
+140
View File
@@ -0,0 +1,140 @@
import 'package:flutter/material.dart';
import '../services/auth_service.dart';
import 'settings_state.dart';
class AuthState extends ChangeNotifier {
final AuthService _auth = AuthService();
String? _username;
// Getters
bool get isLoggedIn => _username != null;
String get username => _username ?? '';
AuthService get authService => _auth;
// Initialize auth state and check existing login
Future<void> init(UploadMode uploadMode) async {
_auth.setUploadMode(uploadMode);
try {
if (await _auth.isLoggedIn()) {
print('AuthState: User appears to be logged in, fetching username...');
_username = await _auth.login();
if (_username != null) {
print("AuthState: Successfully retrieved username: $_username");
} else {
print('AuthState: Failed to retrieve username despite being logged in');
}
} else {
print('AuthState: User is not logged in');
}
} catch (e) {
print("AuthState: Error during auth initialization: $e");
}
}
Future<void> login() async {
try {
print('AuthState: Starting login process...');
_username = await _auth.login();
if (_username != null) {
print("AuthState: Login successful for user: $_username");
} else {
print('AuthState: Login failed - no username returned');
}
} catch (e) {
print("AuthState: Login error: $e");
_username = null;
}
notifyListeners();
}
Future<void> logout() async {
await _auth.logout();
_username = null;
notifyListeners();
}
Future<void> refreshAuthState() async {
try {
print('AuthState: Refreshing auth state...');
if (await _auth.isLoggedIn()) {
print('AuthState: Token exists, fetching username...');
_username = await _auth.login();
if (_username != null) {
print("AuthState: Auth refresh successful: $_username");
} else {
print('AuthState: Auth refresh failed - no username');
}
} else {
print('AuthState: No valid token found');
_username = null;
}
} catch (e) {
print("AuthState: Auth refresh error: $e");
_username = null;
}
notifyListeners();
}
Future<void> forceLogin() async {
try {
print('AuthState: Starting forced fresh login...');
_username = await _auth.forceLogin();
if (_username != null) {
print("AuthState: Forced login successful: $_username");
} else {
print('AuthState: Forced login failed - no username returned');
}
} catch (e) {
print("AuthState: Forced login error: $e");
_username = null;
}
notifyListeners();
}
Future<bool> validateToken() async {
try {
return await _auth.isLoggedIn();
} catch (e) {
print("AuthState: Token validation error: $e");
return false;
}
}
// Handle upload mode changes
Future<void> onUploadModeChanged(UploadMode mode) async {
_auth.setUploadMode(mode);
// Refresh user display for active mode, validating token
try {
if (await _auth.isLoggedIn()) {
print('AuthState: Switching mode, token exists; validating...');
final isValid = await validateToken();
if (isValid) {
print("AuthState: Switching mode; fetching username for $mode...");
_username = await _auth.login();
if (_username != null) {
print("AuthState: Switched mode, now logged in as $_username");
} else {
print('AuthState: Switched mode but failed to retrieve username');
}
} else {
print('AuthState: Switching mode, token invalid—auto-logout.');
await logout(); // This clears _username also.
}
} else {
_username = null;
print("AuthState: Mode change: not logged in in $mode");
}
} catch (e) {
_username = null;
print("AuthState: Mode change user restoration error: $e");
}
notifyListeners();
}
Future<String?> getAccessToken() async {
return await _auth.getAccessToken();
}
}
+87
View File
@@ -0,0 +1,87 @@
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../models/camera_profile.dart';
import '../services/profile_service.dart';
class ProfileState extends ChangeNotifier {
static const String _enabledPrefsKey = 'enabled_profiles';
final List<CameraProfile> _profiles = [];
final Set<CameraProfile> _enabled = {};
// Getters
List<CameraProfile> get profiles => List.unmodifiable(_profiles);
bool isEnabled(CameraProfile p) => _enabled.contains(p);
List<CameraProfile> get enabledProfiles =>
_profiles.where(isEnabled).toList(growable: false);
// Initialize profiles from built-in and custom sources
Future<void> init() async {
// Initialize profiles: built-in + custom
_profiles.add(CameraProfile.alpr());
_profiles.addAll(await ProfileService().load());
// Load enabled profile IDs from prefs
final prefs = await SharedPreferences.getInstance();
final enabledIds = prefs.getStringList(_enabledPrefsKey);
if (enabledIds != null && enabledIds.isNotEmpty) {
// Restore enabled profiles by id
_enabled.addAll(_profiles.where((p) => enabledIds.contains(p.id)));
} else {
// By default, all are enabled
_enabled.addAll(_profiles);
}
}
void toggleProfile(CameraProfile p, bool e) {
if (e) {
_enabled.add(p);
} else {
_enabled.remove(p);
// Safety: Always have at least one enabled profile
if (_enabled.isEmpty) {
final builtIn = _profiles.firstWhere((profile) => profile.builtin, orElse: () => _profiles.first);
_enabled.add(builtIn);
}
}
_saveEnabledProfiles();
notifyListeners();
}
void addOrUpdateProfile(CameraProfile p) {
final idx = _profiles.indexWhere((x) => x.id == p.id);
if (idx >= 0) {
_profiles[idx] = p;
} else {
_profiles.add(p);
_enabled.add(p);
_saveEnabledProfiles();
}
ProfileService().save(_profiles);
notifyListeners();
}
void deleteProfile(CameraProfile p) {
if (p.builtin) return;
_enabled.remove(p);
_profiles.removeWhere((x) => x.id == p.id);
// Safety: Always have at least one enabled profile
if (_enabled.isEmpty) {
final builtIn = _profiles.firstWhere((profile) => profile.builtin, orElse: () => _profiles.first);
_enabled.add(builtIn);
}
_saveEnabledProfiles();
ProfileService().save(_profiles);
notifyListeners();
}
// Save enabled profile IDs to disk
Future<void> _saveEnabledProfiles() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setStringList(
_enabledPrefsKey,
_enabled.map((p) => p.id).toList(),
);
}
}
+61
View File
@@ -0,0 +1,61 @@
import 'package:flutter/material.dart';
import 'package:latlong2/latlong.dart';
import '../models/camera_profile.dart';
// ------------------ AddCameraSession ------------------
class AddCameraSession {
AddCameraSession({required this.profile, this.directionDegrees = 0});
CameraProfile profile;
double directionDegrees;
LatLng? target;
}
class SessionState extends ChangeNotifier {
AddCameraSession? _session;
// Getter
AddCameraSession? get session => _session;
void startAddSession(List<CameraProfile> enabledProfiles) {
_session = AddCameraSession(profile: enabledProfiles.first);
notifyListeners();
}
void updateSession({
double? directionDeg,
CameraProfile? profile,
LatLng? target,
}) {
if (_session == null) return;
bool dirty = false;
if (directionDeg != null && directionDeg != _session!.directionDegrees) {
_session!.directionDegrees = directionDeg;
dirty = true;
}
if (profile != null && profile != _session!.profile) {
_session!.profile = profile;
dirty = true;
}
if (target != null) {
_session!.target = target;
dirty = true;
}
if (dirty) notifyListeners();
}
void cancelSession() {
_session = null;
notifyListeners();
}
AddCameraSession? commitSession() {
if (_session?.target == null) return null;
final session = _session!;
_session = null;
notifyListeners();
return session;
}
}
+80
View File
@@ -0,0 +1,80 @@
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../widgets/tile_provider_with_cache.dart';
// Enum for upload mode (Production, OSM Sandbox, Simulate)
enum UploadMode { production, sandbox, simulate }
class SettingsState extends ChangeNotifier {
static const String _offlineModePrefsKey = 'offline_mode';
static const String _maxCamerasPrefsKey = 'max_cameras';
static const String _uploadModePrefsKey = 'upload_mode';
static const String _legacyTestModePrefsKey = 'test_mode';
bool _offlineMode = false;
int _maxCameras = 250;
UploadMode _uploadMode = UploadMode.simulate;
// Getters
bool get offlineMode => _offlineMode;
int get maxCameras => _maxCameras;
UploadMode get uploadMode => _uploadMode;
// Initialize settings from preferences
Future<void> init() async {
final prefs = await SharedPreferences.getInstance();
// Load offline mode
_offlineMode = prefs.getBool(_offlineModePrefsKey) ?? false;
// Load max cameras
if (prefs.containsKey(_maxCamerasPrefsKey)) {
_maxCameras = prefs.getInt(_maxCamerasPrefsKey) ?? 250;
}
// Load upload mode (including migration from old test_mode bool)
if (prefs.containsKey(_uploadModePrefsKey)) {
final idx = prefs.getInt(_uploadModePrefsKey) ?? 0;
if (idx >= 0 && idx < UploadMode.values.length) {
_uploadMode = UploadMode.values[idx];
}
} else if (prefs.containsKey(_legacyTestModePrefsKey)) {
// migrate legacy test_mode (true->simulate, false->prod)
final legacy = prefs.getBool(_legacyTestModePrefsKey) ?? false;
_uploadMode = legacy ? UploadMode.simulate : UploadMode.production;
await prefs.remove(_legacyTestModePrefsKey);
await prefs.setInt(_uploadModePrefsKey, _uploadMode.index);
}
}
Future<void> setOfflineMode(bool enabled) async {
final wasOffline = _offlineMode;
_offlineMode = enabled;
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(_offlineModePrefsKey, enabled);
if (wasOffline && !enabled) {
// Transitioning from offline to online: clear tile cache!
TileProviderWithCache.clearCache();
}
notifyListeners();
}
set maxCameras(int n) {
if (n < 10) n = 10; // minimum
_maxCameras = n;
SharedPreferences.getInstance().then((prefs) {
prefs.setInt(_maxCamerasPrefsKey, n);
});
notifyListeners();
}
Future<void> setUploadMode(UploadMode mode) async {
_uploadMode = mode;
final prefs = await SharedPreferences.getInstance();
await prefs.setInt(_uploadModePrefsKey, mode.index);
notifyListeners();
}
}
+169
View File
@@ -0,0 +1,169 @@
import 'dart:convert';
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../models/pending_upload.dart';
import '../models/osm_camera_node.dart';
import '../services/camera_cache.dart';
import '../services/uploader.dart';
import '../widgets/camera_provider_with_cache.dart';
import 'settings_state.dart';
import 'session_state.dart';
class UploadQueueState extends ChangeNotifier {
final List<PendingUpload> _queue = [];
Timer? _uploadTimer;
// Getters
int get pendingCount => _queue.length;
List<PendingUpload> get pendingUploads => List.unmodifiable(_queue);
// Initialize by loading queue from storage
Future<void> init() async {
await _loadQueue();
}
// Add a completed session to the upload queue
void addFromSession(AddCameraSession session) {
final upload = PendingUpload(
coord: session.target!,
direction: session.directionDegrees,
profile: session.profile,
);
_queue.add(upload);
_saveQueue();
// Add to camera cache immediately so it shows on the map
// Create a temporary node with a negative ID (to distinguish from real OSM nodes)
// Using timestamp as negative ID to ensure uniqueness
final tempId = -DateTime.now().millisecondsSinceEpoch;
final tags = Map<String, String>.from(upload.profile.tags);
tags['direction'] = upload.direction.toStringAsFixed(0);
tags['_pending_upload'] = 'true'; // Mark as pending for potential UI distinction
final tempNode = OsmCameraNode(
id: tempId,
coord: upload.coord,
tags: tags,
);
CameraCache.instance.addOrUpdate([tempNode]);
// Notify camera provider to update the map
CameraProviderWithCache.instance.notifyListeners();
notifyListeners();
}
void clearQueue() {
print("UploadQueueState: Clearing upload queue (${_queue.length} items)");
_queue.clear();
_saveQueue();
notifyListeners();
}
void removeFromQueue(PendingUpload upload) {
print("UploadQueueState: Removing upload from queue: ${upload.coord}");
_queue.remove(upload);
_saveQueue();
notifyListeners();
}
void retryUpload(PendingUpload upload) {
upload.error = false;
upload.attempts = 0;
_saveQueue();
notifyListeners();
}
// Start the upload processing loop
void startUploader({
required bool offlineMode,
required UploadMode uploadMode,
required Future<String?> Function() getAccessToken,
}) {
_uploadTimer?.cancel();
// No uploads without queue, or if offline mode is enabled.
if (_queue.isEmpty || offlineMode) return;
_uploadTimer = Timer.periodic(const Duration(seconds: 10), (t) async {
if (_queue.isEmpty || offlineMode) {
_uploadTimer?.cancel();
return;
}
// Find the first queue item that is NOT in error state and act on that
final item = _queue.where((pu) => !pu.error).cast<PendingUpload?>().firstOrNull;
if (item == null) return;
// Retrieve access after every tick (accounts for re-login)
final access = await getAccessToken();
if (access == null) return; // not logged in
bool ok;
if (uploadMode == UploadMode.simulate) {
// Simulate successful upload without calling real API
print("UploadQueueState: UploadMode.simulate - simulating upload for ${item.coord}");
await Future.delayed(const Duration(seconds: 1)); // Simulate network delay
ok = true;
print('UploadQueueState: Simulated upload successful');
} else {
// Real upload -- pass uploadMode so uploader can switch between prod and sandbox
final up = Uploader(access, () {
_queue.remove(item);
_saveQueue();
notifyListeners();
}, uploadMode: uploadMode);
ok = await up.upload(item);
}
if (ok && uploadMode == UploadMode.simulate) {
// Remove manually for simulate mode
_queue.remove(item);
_saveQueue();
notifyListeners();
}
if (!ok) {
item.attempts++;
if (item.attempts >= 3) {
// Mark as error and stop the uploader. User can manually retry.
item.error = true;
_saveQueue();
notifyListeners();
_uploadTimer?.cancel();
} else {
await Future.delayed(const Duration(seconds: 20));
}
}
});
}
void stopUploader() {
_uploadTimer?.cancel();
}
// ---------- Queue persistence ----------
Future<void> _saveQueue() async {
final prefs = await SharedPreferences.getInstance();
final jsonList = _queue.map((e) => e.toJson()).toList();
await prefs.setString('queue', jsonEncode(jsonList));
}
Future<void> _loadQueue() async {
final prefs = await SharedPreferences.getInstance();
final jsonStr = prefs.getString('queue');
if (jsonStr == null) return;
final list = jsonDecode(jsonStr) as List<dynamic>;
_queue
..clear()
..addAll(list.map((e) => PendingUpload.fromJson(e)));
}
@override
void dispose() {
_uploadTimer?.cancel();
super.dispose();
}
}