still cant believe this works

This commit is contained in:
stopflock
2025-08-05 22:56:47 -05:00
parent 41db39ea3c
commit 640903d954
4 changed files with 133 additions and 47 deletions

View File

@@ -10,6 +10,9 @@ import 'services/auth_service.dart';
import 'services/uploader.dart';
import 'services/profile_service.dart';
// Enum for upload mode (Production, OSM Sandbox, Simulate)
enum UploadMode { production, sandbox, simulate }
// ------------------ AddCameraSession ------------------
class AddCameraSession {
AddCameraSession({required this.profile, this.directionDegrees = 0});
@@ -18,6 +21,7 @@ class AddCameraSession {
LatLng? target;
}
// ------------------ AppState ------------------
class AppState extends ChangeNotifier {
AppState() {
@@ -30,22 +34,26 @@ class AppState extends ChangeNotifier {
final List<CameraProfile> _profiles = [];
final Set<CameraProfile> _enabled = {};
static const String _enabledPrefsKey = 'enabled_profiles';
// Test mode - prevents actual uploads to OSM
bool _testMode = false;
static const String _testModePrefsKey = 'test_mode';
bool get testMode => _testMode;
Future<void> setTestMode(bool enabled) async {
_testMode = enabled;
// Upload mode: production, sandbox, or simulate (in-memory, no uploads)
UploadMode _uploadMode = UploadMode.production;
static const String _uploadModePrefsKey = 'upload_mode';
UploadMode get uploadMode => _uploadMode;
Future<void> setUploadMode(UploadMode mode) async {
_uploadMode = mode;
// Update AuthService to match new mode
_auth.setUploadMode(mode);
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(_testModePrefsKey, enabled);
print('AppState: Test mode ${enabled ? 'enabled' : 'disabled'}');
await prefs.setInt(_uploadModePrefsKey, mode.index);
print('AppState: Upload mode set to $mode');
notifyListeners();
}
// For legacy bool test mode
static const String _legacyTestModePrefsKey = 'test_mode';
AddCameraSession? _session;
AddCameraSession? get session => _session;
final List<PendingUpload> _queue = [];
Timer? _uploadTimer;
@@ -58,7 +66,7 @@ class AppState extends ChangeNotifier {
_profiles.add(CameraProfile.alpr());
_profiles.addAll(await ProfileService().load());
// Load enabled profile IDs and test mode from prefs
// Load enabled profile IDs and upload/test mode from prefs
final prefs = await SharedPreferences.getInstance();
final enabledIds = prefs.getStringList(_enabledPrefsKey);
if (enabledIds != null && enabledIds.isNotEmpty) {
@@ -68,7 +76,21 @@ class AppState extends ChangeNotifier {
// By default, all are enabled
_enabled.addAll(_profiles);
}
_testMode = prefs.getBool(_testModePrefsKey) ?? false;
// Upload mode loading (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);
}
// Ensure AuthService follows loaded mode
_auth.setUploadMode(_uploadMode);
await _loadQueue();
@@ -287,30 +309,30 @@ class AppState extends ChangeNotifier {
_uploadTimer = Timer.periodic(const Duration(seconds: 10), (t) async {
if (_queue.isEmpty) return;
// Retrieve access after every tick (accounts for re-login)
final access = await _auth.getAccessToken();
if (access == null) return; // not logged in
final item = _queue.first;
bool ok;
if (_testMode) {
// Test mode - simulate successful upload without actually calling OSM API
print('AppState: Test mode - simulating upload for ${item.coord}');
if (_uploadMode == UploadMode.simulate) {
// Simulate successful upload without calling real API
print('AppState: UploadMode.simulate - simulating upload for ${item.coord}');
await Future.delayed(const Duration(seconds: 1)); // Simulate network delay
ok = true;
print('AppState: Test mode - simulated upload successful');
print('AppState: Simulated upload successful');
} else {
// Real upload
// 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 && _testMode) {
// In test mode, manually remove from queue since Uploader callback won't be called
if (ok && _uploadMode == UploadMode.simulate) {
// Remove manually for simulate mode
_queue.remove(item);
_saveQueue();
notifyListeners();

View File

@@ -143,22 +143,59 @@ class SettingsScreen extends StatelessWidget {
),
),
const Divider(),
// Test mode toggle
SwitchListTile(
secondary: const Icon(Icons.bug_report),
title: const Text('Test Mode'),
subtitle: const Text('Simulate uploads without sending to OSM'),
value: appState.testMode,
onChanged: (value) => appState.setTestMode(value),
// Upload mode selector - Production/Sandbox/Simulate
ListTile(
leading: const Icon(Icons.cloud_upload),
title: const Text('Upload Destination'),
subtitle: const Text('Choose where cameras are uploaded'),
trailing: DropdownButton<UploadMode>(
value: appState.uploadMode,
items: const [
DropdownMenuItem(
value: UploadMode.production,
child: Text('Production'),
),
DropdownMenuItem(
value: UploadMode.sandbox,
child: Text('Sandbox'),
),
DropdownMenuItem(
value: UploadMode.simulate,
child: Text('Simulate'),
),
],
onChanged: (mode) {
if (mode != null) appState.setUploadMode(mode);
},
),
),
// Help text
Padding(
padding: const EdgeInsets.only(left: 56, top: 2, right: 16, bottom: 12),
child: Builder(
builder: (context) {
switch (appState.uploadMode) {
case UploadMode.production:
return const Text('Upload to the live OSM database (visible to all users)', style: TextStyle(fontSize: 12, color: Colors.black87));
case UploadMode.sandbox:
return const Text('Upload to the OSM Sandbox (safe for testing, data resets regularly)', style: TextStyle(fontSize: 12, color: Colors.orange));
case UploadMode.simulate:
default:
return const Text('Simulate uploads (does not contact OSM servers)', style: TextStyle(fontSize: 12, color: Colors.deepPurple));
}
},
),
),
const Divider(),
// Queue management
ListTile(
leading: const Icon(Icons.queue),
title: Text('Pending uploads: ${appState.pendingCount}'),
subtitle: appState.testMode
? const Text('Test mode enabled - uploads simulated')
: const Text('Tap to view queue'),
subtitle: appState.uploadMode == UploadMode.simulate
? const Text('Simulate mode enabled uploads simulated')
: appState.uploadMode == UploadMode.sandbox
? const Text('Sandbox mode uploads go to OSM Sandbox')
: const Text('Tap to view queue'),
onTap: appState.pendingCount > 0 ? () {
_showQueueDialog(context, appState);
} : null,

View File

@@ -7,31 +7,39 @@ import 'package:oauth2_client/oauth2_helper.dart';
import 'package:http/http.dart' as http;
/// Handles PKCE OAuth login with OpenStreetMap.
import '../app_state.dart';
class AuthService {
static const String _clientId = 'Js6Fn3NR3HEGaD0ZIiHBQlV9LrVcHmsOsDmApHtSyuY';
static const _redirect = 'flockmap://auth';
late final OAuth2Helper _helper;
late OAuth2Helper _helper;
String? _displayName;
UploadMode _mode = UploadMode.production;
AuthService() {
AuthService({UploadMode mode = UploadMode.production}) {
setUploadMode(mode);
}
void setUploadMode(UploadMode mode) {
_mode = mode;
final isSandbox = (mode == UploadMode.sandbox);
final authBase = isSandbox
? 'https://master.apis.dev.openstreetmap.org' // sandbox auth
: 'https://www.openstreetmap.org';
final client = OAuth2Client(
authorizeUrl: 'https://www.openstreetmap.org/oauth2/authorize',
tokenUrl: 'https://www.openstreetmap.org/oauth2/token',
authorizeUrl: '$authBase/oauth2/authorize',
tokenUrl: '$authBase/oauth2/token',
redirectUri: _redirect,
customUriScheme: 'flockmap',
);
_helper = OAuth2Helper(
client,
clientId: _clientId,
scopes: ['read_prefs', 'write_api'],
enablePKCE: true,
);
print('AuthService: Initialized with scopes: [read_prefs, write_api]');
print('AuthService: Client ID: $_clientId');
print('AuthService: Redirect URI: $_redirect');
print('AuthService: Initialized for $mode with $authBase');
}
Future<bool> isLoggedIn() async =>
@@ -81,13 +89,19 @@ class AuthService {
/* ───────── helper ───────── */
String get _apiHost {
return _mode == UploadMode.sandbox
? 'https://api06.dev.openstreetmap.org'
: 'https://api.openstreetmap.org';
}
Future<String?> _fetchUsername(String accessToken) async {
try {
print('AuthService: Fetching username from OSM API...');
print('AuthService: Fetching username from OSM API ($_apiHost) ...');
print('AuthService: Access token (first 20 chars): ${accessToken.substring(0, math.min(20, accessToken.length))}...');
final resp = await http.get(
Uri.parse('https://api.openstreetmap.org/api/0.6/user/details.json'),
Uri.parse('$_apiHost/api/0.6/user/details.json'),
headers: {'Authorization': 'Bearer $accessToken'},
);
print('AuthService: OSM API response status: ${resp.statusCode}');
@@ -101,7 +115,7 @@ class AuthService {
try {
print('AuthService: Checking token permissions...');
final permResp = await http.get(
Uri.parse('https://api.openstreetmap.org/api/0.6/permissions.json'),
Uri.parse('$_apiHost/api/0.6/permissions.json'),
headers: {'Authorization': 'Bearer $accessToken'},
);
print('AuthService: Permissions response ${permResp.statusCode}: ${permResp.body}');

View File

@@ -3,11 +3,14 @@ import 'package:http/http.dart' as http;
import '../models/pending_upload.dart';
import '../app_state.dart';
class Uploader {
Uploader(this.accessToken, this.onSuccess);
Uploader(this.accessToken, this.onSuccess, {this.uploadMode = UploadMode.production});
final String accessToken;
final void Function() onSuccess;
final UploadMode uploadMode;
Future<bool> upload(PendingUpload p) async {
try {
@@ -68,14 +71,24 @@ class Uploader {
}
}
String get _host {
switch (uploadMode) {
case UploadMode.sandbox:
return 'api06.dev.openstreetmap.org';
case UploadMode.production:
default:
return 'api.openstreetmap.org';
}
}
Future<http.Response> _post(String path, String body) => http.post(
Uri.https('api.openstreetmap.org', path),
Uri.https(_host, path),
headers: _headers,
body: body,
);
Future<http.Response> _put(String path, String body) => http.put(
Uri.https('api.openstreetmap.org', path),
Uri.https(_host, path),
headers: _headers,
body: body,
);