diff --git a/README.md b/README.md index 3c9db2a..fa2d99f 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,13 @@ A minimal Flutter scaffold for mapping and tagging Flock‑style ALPR cameras in OpenStreetMap. -## Platform setup notes +# NOTE: +Forks should register for their own oauth2 client id from OSM: https://www.openstreetmap.org/oauth2/applications +These are hardcoded in lib/services/auth_service.dart for each app. +If you discover a bug that causes bad behavior w/rt OSM API, you might want to register a new one for the patched version to distinguish them. You can also then delete the old version from OSM to prevent new people from using the old version. + +## Platform setup notes ### iOS Add location permission strings to `ios/Runner/Info.plist`: ```xml diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index b86c977..8508432 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -1,45 +1,52 @@ plugins { id("com.android.application") id("kotlin-android") - // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + // Flutter plugin *must* be applied last. id("dev.flutter.flutter-gradle-plugin") } android { namespace = "com.example.flock_map_app" - compileSdk = flutter.compileSdkVersion -// ndkVersion = flutter.ndkVersion + + // Matches current stable Flutter (compileSdk 34 as of July 2025) + compileSdk = 35 + + // NDK only needed if you build native plugins; keep your pinned version ndkVersion = "27.0.12077973" compileOptions { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } - kotlinOptions { jvmTarget = JavaVersion.VERSION_11.toString() } defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + // Application ID (package name) applicationId = "com.example.flock_map_app" - // You can update the following values to match your application needs. - // For more information, see: https://flutter.dev/to/review-gradle-config. - minSdk = flutter.minSdkVersion - targetSdk = flutter.targetSdkVersion + + // ──────────────────────────────────────────────────────────── + // oauth2_client 4.x & flutter_web_auth_2 5.x require minSdk 23 + // ──────────────────────────────────────────────────────────── + minSdk = 23 + targetSdk = 34 + + // Flutter tool injects these during `flutter build` versionCode = flutter.versionCode versionName = flutter.versionName } buildTypes { release { - // TODO: Add your own signing config for the release build. - // Signing with the debug keys for now, so `flutter run --release` works. + // Using debug signing so `flutter run --release` works out‑of‑box. signingConfig = signingConfigs.getByName("debug") } } } flutter { + // Path up to the Flutter project directory source = "../.." } + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 3dd5dc6..65034ea 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,43 +1,67 @@ + + + + android:label="flock_map_app" + android:icon="@mipmap/ic_launcher" + android:enableOnBackInvokedCallback="true"> + - + + + android:name="io.flutter.embedding.android.NormalTheme" + android:resource="@style/NormalTheme" /> + + + + + + + + + + - + + + + + + + + + + + + + - + @@ -45,3 +69,4 @@ + diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 570dd84..7fce1aa 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -47,5 +47,23 @@ UIApplicationSupportsIndirectInputEvents + + CFBundleURLTypes + + + CFBundleTypeRole + None + CFBundleURLSchemes + + flockmap + + + + + + LSApplicationQueriesSchemes + + https + diff --git a/lib/app_state.dart b/lib/app_state.dart index 7c5deab..5f2fe4f 100644 --- a/lib/app_state.dart +++ b/lib/app_state.dart @@ -1,50 +1,77 @@ +import 'dart:convert'; +import 'dart:async'; import 'package:flutter/material.dart'; import 'package:latlong2/latlong.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'models/camera_profile.dart'; import 'models/pending_upload.dart'; +import 'services/auth_service.dart'; +import 'services/uploader.dart'; +// ------------------ AddCameraSession ------------------ class AddCameraSession { AddCameraSession({required this.profile, this.directionDegrees = 0}); - CameraProfile profile; double directionDegrees; LatLng? target; } +// ------------------ AppState ------------------ class AppState extends ChangeNotifier { AppState() { - _profiles = [CameraProfile.alpr()]; - _enabled = {..._profiles}; // all enabled by default + _init(); } - // ---------- Auth ---------- - bool _loggedIn = false; - bool get isLoggedIn => _loggedIn; - void setLoggedIn(bool v) { - _loggedIn = v; - notifyListeners(); - } + final _auth = AuthService(); + String? _username; - // ---------- Profiles & toggles ---------- - late final List _profiles; - late final Set _enabled; - List get profiles => List.unmodifiable(_profiles); - bool isEnabled(CameraProfile p) => _enabled.contains(p); + late final List _profiles = [CameraProfile.alpr()]; + final Set _enabled = {}; - void toggleProfile(CameraProfile p, bool enable) { - enable ? _enabled.add(p) : _enabled.remove(p); - notifyListeners(); - } - - List get enabledProfiles => _profiles - .where((p) => _enabled.contains(p)) - .toList(growable: false); - - // ---------- Add-camera session ---------- AddCameraSession? _session; AddCameraSession? get session => _session; + final List _queue = []; + Timer? _uploadTimer; + + bool get isLoggedIn => _username != null; + String get username => _username ?? ''; + + // ---------- Init ---------- + Future _init() async { + _enabled.addAll(_profiles); + await _loadQueue(); + if (await _auth.isLoggedIn()) { + _username = await _auth.login(); + } + _startUploader(); + notifyListeners(); + } + + // ---------- Auth ---------- + Future login() async { + _username = await _auth.login(); + notifyListeners(); + } + + Future logout() async { + await _auth.logout(); + _username = null; + notifyListeners(); + } + + // ---------- Profiles ---------- + List get profiles => List.unmodifiable(_profiles); + bool isEnabled(CameraProfile p) => _enabled.contains(p); + List get enabledProfiles => + _profiles.where(isEnabled).toList(growable: false); + void toggleProfile(CameraProfile p, bool e) { + e ? _enabled.add(p) : _enabled.remove(p); + notifyListeners(); + } + + // ---------- Add‑camera session ---------- void startAddSession() { _session = AddCameraSession(profile: enabledProfiles.first); notifyListeners(); @@ -56,10 +83,21 @@ class AppState extends ChangeNotifier { LatLng? target, }) { if (_session == null) return; - if (directionDeg != null) _session!.directionDegrees = directionDeg; - if (profile != null) _session!.profile = profile; - if (target != null) _session!.target = target; - notifyListeners(); + + 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(); // <-- slider & map update } void cancelSession() { @@ -67,10 +105,6 @@ class AppState extends ChangeNotifier { notifyListeners(); } - // ---------- Pending uploads ---------- - final List _queue = []; - List get queue => List.unmodifiable(_queue); - void commitSession() { if (_session?.target == null) return; _queue.add( @@ -80,8 +114,62 @@ class AppState extends ChangeNotifier { profile: _session!.profile, ), ); + _saveQueue(); _session = null; notifyListeners(); } + + // ---------- Queue persistence ---------- + Future _saveQueue() async { + final prefs = await SharedPreferences.getInstance(); + final jsonList = _queue.map((e) => e.toJson()).toList(); + await prefs.setString('queue', jsonEncode(jsonList)); + } + + Future _loadQueue() async { + final prefs = await SharedPreferences.getInstance(); + final jsonStr = prefs.getString('queue'); + if (jsonStr == null) return; + final list = jsonDecode(jsonStr) as List; + _queue + ..clear() + ..addAll(list.map((e) => PendingUpload.fromJson(e))); + } + + // ---------- Uploader ---------- + void _startUploader() { + _uploadTimer?.cancel(); + + // No uploads without auth or queue. + if (_queue.isEmpty) return; + + _uploadTimer = Timer.periodic(const Duration(seconds: 10), (t) async { + if (_queue.isEmpty) return; + + final access = await _auth.getAccessToken(); + if (access == null) return; // not logged in + + final item = _queue.first; + final up = Uploader(access, () { + _queue.remove(item); + _saveQueue(); + notifyListeners(); + }); + + final ok = await up.upload(item); + if (!ok) { + item.attempts++; + if (item.attempts >= 3) { + // give up until next launch + _uploadTimer?.cancel(); + } else { + await Future.delayed(const Duration(seconds: 20)); + } + } + }); + } + + // ---------- Exposed getters ---------- + int get pendingCount => _queue.length; } diff --git a/lib/models/pending_upload.dart b/lib/models/pending_upload.dart index f1a3e96..b0d8fcf 100644 --- a/lib/models/pending_upload.dart +++ b/lib/models/pending_upload.dart @@ -5,12 +5,28 @@ class PendingUpload { final LatLng coord; final double direction; final CameraProfile profile; - final DateTime queuedAt; + int attempts; PendingUpload({ required this.coord, required this.direction, required this.profile, - }) : queuedAt = DateTime.now(); + this.attempts = 0, + }); + + Map toJson() => { + 'lat': coord.latitude, + 'lon': coord.longitude, + 'dir': direction, + 'profile': profile.name, + 'attempts': attempts, + }; + + factory PendingUpload.fromJson(Map j) => PendingUpload( + coord: LatLng(j['lat'], j['lon']), + direction: j['dir'], + profile: CameraProfile.alpr(), // only built‑in for now + attempts: j['attempts'] ?? 0, + ); } diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index a6001bc..3ad7a90 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -13,34 +13,25 @@ class HomeScreen extends StatefulWidget { } class _HomeScreenState extends State { + final GlobalKey _scaffoldKey = GlobalKey(); bool _followMe = true; - Future _startAddCamera(BuildContext context) async { + void _openAddCameraSheet() { final appState = context.read(); appState.startAddSession(); + final session = appState.session!; // guaranteed non‑null now - final submitted = await showModalBottomSheet( - context: context, - isScrollControlled: true, - enableDrag: false, - isDismissible: false, - builder: (_) => const AddCameraSheet(), + _scaffoldKey.currentState!.showBottomSheet( + (ctx) => AddCameraSheet(session: session), ); - - if (submitted == true) { - appState.commitSession(); - if (mounted) { - ScaffoldMessenger.of(context) - .showSnackBar(const SnackBar(content: Text('Camera queued'))); - } - } else { - appState.cancelSession(); - } } @override Widget build(BuildContext context) { + final appState = context.watch(); + return Scaffold( + key: _scaffoldKey, appBar: AppBar( title: const Text('Flock Map'), actions: [ @@ -61,11 +52,13 @@ class _HomeScreenState extends State { if (_followMe) setState(() => _followMe = false); }, ), - floatingActionButton: FloatingActionButton.extended( - onPressed: () => _startAddCamera(context), - icon: const Icon(Icons.add_location_alt), - label: const Text('Tag Camera'), - ), + floatingActionButton: appState.session == null + ? FloatingActionButton.extended( + onPressed: _openAddCameraSheet, + icon: const Icon(Icons.add_location_alt), + label: const Text('Tag Camera'), + ) + : null, ); } } diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index d61ffc1..bad1e36 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -15,11 +15,30 @@ class SettingsScreen extends StatelessWidget { body: ListView( padding: const EdgeInsets.all(16), children: [ - SwitchListTile( - title: const Text('Logged in to OSM (OAuth – coming soon)'), - value: appState.isLoggedIn, - onChanged: null, // disabled for now + ListTile( + leading: Icon( + appState.isLoggedIn ? Icons.person : Icons.login, + color: appState.isLoggedIn ? Colors.green : null, + ), + title: Text(appState.isLoggedIn + ? 'Logged in as ${appState.username}' + : 'Log in to OpenStreetMap'), + onTap: () async { + if (appState.isLoggedIn) { + await appState.logout(); + } else { + await appState.login(); + } + }, ), + if (appState.isLoggedIn) + ListTile( + leading: const Icon(Icons.cloud_upload), + title: const Text('Test upload'), + onTap: () => ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Upload will run soon...')), + ), + ), const Divider(), const Text('Camera Profiles', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), @@ -30,6 +49,11 @@ class SettingsScreen extends StatelessWidget { onChanged: (v) => appState.toggleProfile(p, v), ), ), + const Divider(), + ListTile( + leading: const Icon(Icons.sync), + title: Text('Pending uploads: ${appState.pendingCount}'), + ), ], ), ); diff --git a/lib/services/auth_service.dart b/lib/services/auth_service.dart new file mode 100644 index 0000000..2949e14 --- /dev/null +++ b/lib/services/auth_service.dart @@ -0,0 +1,74 @@ +import 'dart:convert'; +import 'dart:developer'; + +import 'package:oauth2_client/oauth2_client.dart'; +import 'package:oauth2_client/oauth2_helper.dart'; +import 'package:http/http.dart' as http; + +/// Handles PKCE OAuth login with OpenStreetMap. +class AuthService { + static const String _clientId = 'HNbRD_Twxf0_lpkm-BmMB7-zb-v63VLdf_bVlNyU9qs'; + static const _redirect = 'flockmap://auth'; + + late final OAuth2Helper _helper; + String? _displayName; + + AuthService() { + final client = OAuth2Client( + authorizeUrl: 'https://www.openstreetmap.org/oauth2/authorize', + tokenUrl: 'https://www.openstreetmap.org/oauth2/token', + redirectUri: _redirect, + customUriScheme: 'flockmap', + ); + + _helper = OAuth2Helper( + client, + clientId: _clientId, + scopes: ['write_api'], + enablePKCE: true, + ); + } + + Future isLoggedIn() async => + (await _helper.getTokenFromStorage())?.isExpired() == false; + + String? get displayName => _displayName; + + Future login() async { + try { + final token = await _helper.getToken(); + if (token?.accessToken == null) { + log('OAuth error: token null or missing accessToken'); + return null; + } + _displayName = await _fetchUsername(token!.accessToken!); + return _displayName; + } catch (e) { + log('OAuth login failed: $e'); + rethrow; + } + } + + Future logout() async { + await _helper.removeAllTokens(); + _displayName = null; + } + + Future getAccessToken() async => + (await _helper.getTokenFromStorage())?.accessToken; + + /* ───────── helper ───────── */ + + Future _fetchUsername(String accessToken) async { + final resp = await http.get( + Uri.parse('https://api.openstreetmap.org/api/0.6/user/details.json'), + headers: {'Authorization': 'Bearer $accessToken'}, + ); + if (resp.statusCode != 200) { + log('fetchUsername response ${resp.statusCode}: ${resp.body}'); + return null; + } + return jsonDecode(resp.body)['user']?['display_name']; + } +} + diff --git a/lib/services/uploader.dart b/lib/services/uploader.dart new file mode 100644 index 0000000..624f865 --- /dev/null +++ b/lib/services/uploader.dart @@ -0,0 +1,66 @@ +import 'dart:async'; +import 'package:http/http.dart' as http; + +import '../models/pending_upload.dart'; + +class Uploader { + Uploader(this.accessToken, this.onSuccess); + + final String accessToken; + final void Function() onSuccess; + + Future upload(PendingUpload p) async { + try { + // 1. open changeset + final csXml = ''' + + + + + + '''; + final csResp = await _post('/api/0.6/changeset/create', csXml); + if (csResp.statusCode != 200) return false; + final csId = csResp.body; + + // 2. create node + final nodeXml = ''' + + + + + + + + '''; + final nodeResp = await _put('/api/0.6/node/create', nodeXml); + if (nodeResp.statusCode != 200) return false; + + // 3. close changeset + await _put('/api/0.6/changeset/$csId/close', ''); + + onSuccess(); + return true; + } catch (_) { + return false; + } + } + + Future _post(String path, String body) => http.post( + Uri.https('api.openstreetmap.org', path), + headers: _headers, + body: body, + ); + + Future _put(String path, String body) => http.put( + Uri.https('api.openstreetmap.org', path), + headers: _headers, + body: body, + ); + + Map get _headers => { + 'Authorization': 'Bearer $accessToken', + 'Content-Type': 'text/xml', + }; +} + diff --git a/lib/widgets/add_camera_sheet.dart b/lib/widgets/add_camera_sheet.dart index e441fcf..845df6d 100644 --- a/lib/widgets/add_camera_sheet.dart +++ b/lib/widgets/add_camera_sheet.dart @@ -5,19 +5,26 @@ import '../app_state.dart'; import '../models/camera_profile.dart'; class AddCameraSheet extends StatelessWidget { - const AddCameraSheet({super.key}); + const AddCameraSheet({super.key, required this.session}); + + final AddCameraSession session; @override Widget build(BuildContext context) { final appState = context.watch(); - final session = appState.session; - // If the session was cleared before this frame, bail safely. - if (session == null) { - return const SizedBox.shrink(); + void _commit() { + appState.commitSession(); + Navigator.pop(context); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Camera queued for upload')), + ); } - final profiles = appState.profiles; + void _cancel() { + appState.cancelSession(); + Navigator.pop(context); + } return Padding( padding: @@ -39,7 +46,7 @@ class AddCameraSheet extends StatelessWidget { title: const Text('Profile'), trailing: DropdownButton( value: session.profile, - items: profiles + items: appState.profiles .map((p) => DropdownMenuItem(value: p, child: Text(p.name))) .toList(), onChanged: (p) => @@ -64,14 +71,14 @@ class AddCameraSheet extends StatelessWidget { children: [ Expanded( child: OutlinedButton( - onPressed: () => Navigator.pop(context, false), + onPressed: _cancel, child: const Text('Cancel'), ), ), const SizedBox(width: 12), Expanded( child: ElevatedButton( - onPressed: () => Navigator.pop(context, true), + onPressed: _commit, child: const Text('Submit'), ), ), diff --git a/lib/widgets/map_view.dart b/lib/widgets/map_view.dart index 17a8d11..fb7b569 100644 --- a/lib/widgets/map_view.dart +++ b/lib/widgets/map_view.dart @@ -173,13 +173,13 @@ class _MapViewState extends State { // Attribution overlay Positioned( - bottom: 8, - right: 8, + bottom: 20, + left: 10, child: Container( color: Colors.white70, padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), child: const Text( - '© OpenStreetMap contributors', + '© OpenStreetMap and contributors', style: TextStyle(fontSize: 11), ), ), diff --git a/pubspec.lock b/pubspec.lock index a2cc1d3..af05978 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -9,14 +9,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.13.0" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" - url: "https://pub.dev" - source: hosted - version: "2.1.2" characters: dependency: transitive description: @@ -49,14 +41,30 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.6" - fake_async: + desktop_webview_window: dependency: transitive description: - name: fake_async - sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + name: desktop_webview_window + sha256: "57cf20d81689d5cbb1adfd0017e96b669398a669d927906073b0e42fc64111c0" url: "https://pub.dev" source: hosted - version: "1.3.3" + version: "0.2.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" fixnum: dependency: transitive description: @@ -78,11 +86,70 @@ packages: url: "https://pub.dev" source: hosted version: "6.2.1" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" + flutter_secure_storage: + dependency: "direct main" + description: + name: flutter_secure_storage + sha256: f7eceb0bc6f4fd0441e29d43cab9ac2a1c5ffd7ea7b64075136b718c46954874 + url: "https://pub.dev" + source: hosted + version: "10.0.0-beta.4" + flutter_secure_storage_darwin: + dependency: transitive + description: + name: flutter_secure_storage_darwin + sha256: f226f2a572bed96bc6542198ebaec227150786e34311d455a7e2d3d06d951845 + url: "https://pub.dev" + source: hosted + version: "0.1.0" + flutter_secure_storage_linux: + dependency: transitive + description: + name: flutter_secure_storage_linux + sha256: "9b4b73127e857cd3117d43a70fa3dddadb6e0b253be62e6a6ab85caa0742182c" + url: "https://pub.dev" + source: hosted + version: "2.0.1" + flutter_secure_storage_platform_interface: + dependency: transitive + description: + name: flutter_secure_storage_platform_interface + sha256: "8ceea1223bee3c6ac1a22dabd8feefc550e4729b3675de4b5900f55afcb435d6" + url: "https://pub.dev" + source: hosted + version: "2.0.1" + flutter_secure_storage_web: + dependency: transitive + description: + name: flutter_secure_storage_web + sha256: "4c3f233e739545c6cb09286eeec1cc4744138372b985113acc904f7263bef517" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + flutter_secure_storage_windows: + dependency: transitive + description: + name: flutter_secure_storage_windows + sha256: ff32af20f70a8d0e59b2938fc92de35b54a74671041c814275afd80e27df9f21 + url: "https://pub.dev" + source: hosted + version: "4.0.0" + flutter_web_auth_2: + dependency: "direct main" + description: + name: flutter_web_auth_2 + sha256: "2483d1fd3c45fe1262446e8d5f5490f01b864f2e7868ffe05b4727e263cc0182" + url: "https://pub.dev" + source: hosted + version: "5.0.0-alpha.3" + flutter_web_auth_2_platform_interface: + dependency: transitive + description: + name: flutter_web_auth_2_platform_interface + sha256: "45927587ebb2364cd273675ec95f6f67b81725754b416cef2b65cdc63fd3e853" + url: "https://pub.dev" + source: hosted + version: "5.0.0-alpha.0" flutter_web_plugins: dependency: transitive description: flutter @@ -168,30 +235,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.9.1" - leak_tracker: - dependency: transitive - description: - name: leak_tracker - sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" - url: "https://pub.dev" - source: hosted - version: "10.0.9" - leak_tracker_flutter_testing: - dependency: transitive - description: - name: leak_tracker_flutter_testing - sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 - url: "https://pub.dev" - source: hosted - version: "3.0.9" - leak_tracker_testing: - dependency: transitive - description: - name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" - url: "https://pub.dev" - source: hosted - version: "3.0.1" lists: dependency: transitive description: @@ -208,14 +251,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.6.0" - matcher: - dependency: transitive - description: - name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 - url: "https://pub.dev" - source: hosted - version: "0.12.17" material_color_utilities: dependency: transitive description: @@ -248,6 +283,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + oauth2_client: + dependency: "direct main" + description: + name: oauth2_client + sha256: d6a146049f36ef2da32bdc7a7a9e5671a0e66ea596d8f70a26de4cddfcab4d2e + url: "https://pub.dev" + source: hosted + version: "4.2.0" path: dependency: transitive description: @@ -256,6 +299,62 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 + url: "https://pub.dev" + source: hosted + version: "2.2.17" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" plugin_platform_interface: dependency: transitive description: @@ -288,6 +387,70 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.5" + random_string: + dependency: transitive + description: + name: random_string + sha256: "03b52435aae8cbdd1056cf91bfc5bf845e9706724dd35ae2e99fa14a1ef79d02" + url: "https://pub.dev" + source: hosted + version: "2.3.1" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" + url: "https://pub.dev" + source: hosted + version: "2.5.3" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "20cbd561f743a342c76c151d6ddb93a9ce6005751e7aa458baad3858bfbfb6ac" + url: "https://pub.dev" + source: hosted + version: "2.4.10" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" + url: "https://pub.dev" + source: hosted + version: "2.5.4" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" sky_engine: dependency: transitive description: flutter @@ -309,22 +472,6 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" - stack_trace: - dependency: transitive - description: - name: stack_trace - sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" - url: "https://pub.dev" - source: hosted - version: "1.12.1" - stream_channel: - dependency: transitive - description: - name: stream_channel - sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" - url: "https://pub.dev" - source: hosted - version: "2.1.4" string_scanner: dependency: transitive description: @@ -341,14 +488,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.2" - test_api: - dependency: transitive - description: - name: test_api - sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd - url: "https://pub.dev" - source: hosted - version: "0.7.4" typed_data: dependency: transitive description: @@ -365,6 +504,70 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.1" + url_launcher: + dependency: transitive + description: + name: url_launcher + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 + url: "https://pub.dev" + source: hosted + version: "6.3.2" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "8582d7f6fe14d2652b4c45c9b6c14c0b678c2af2d083a11b604caeba51930d79" + url: "https://pub.dev" + source: hosted + version: "6.3.16" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb" + url: "https://pub.dev" + source: hosted + version: "6.3.3" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" + url: "https://pub.dev" + source: hosted + version: "3.2.2" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" + url: "https://pub.dev" + source: hosted + version: "3.1.4" uuid: dependency: transitive description: @@ -381,14 +584,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" - vm_service: - dependency: transitive - description: - name: vm_service - sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 - url: "https://pub.dev" - source: hosted - version: "15.0.0" web: dependency: transitive description: @@ -397,6 +592,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + win32: + dependency: transitive + description: + name: win32 + sha256: "66814138c3562338d05613a6e368ed8cfb237ad6d64a9e9334be3f309acfca03" + url: "https://pub.dev" + source: hosted + version: "5.14.0" + window_to_front: + dependency: transitive + description: + name: window_to_front + sha256: "7aef379752b7190c10479e12b5fd7c0b9d92adc96817d9e96c59937929512aee" + url: "https://pub.dev" + source: hosted + version: "0.0.3" wkt_parser: dependency: transitive description: @@ -405,6 +616,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" sdks: - dart: ">=3.7.0-0 <4.0.0" - flutter: ">=3.18.0-18.0.pre.54" + dart: ">=3.8.0 <4.0.0" + flutter: ">=3.27.0" diff --git a/pubspec.yaml b/pubspec.yaml index 19d74e1..a16f956 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,25 +1,29 @@ name: flock_map_app description: Simple OSM camera‑mapping client publish_to: "none" -version: 0.2.1 +version: 0.5.0 environment: - sdk: ">=3.3.0 <4.0.0" + sdk: ">=3.5.0 <4.0.0" # oauth2_client 4.x needs Dart 3.5+ dependencies: flutter: sdk: flutter + # UI & Map provider: ^6.1.2 - flutter_map: ^6.1.0 + flutter_map: ^6.2.1 latlong2: ^0.9.0 geolocator: ^10.1.0 http: ^1.2.1 -dev_dependencies: - flutter_test: - sdk: flutter + # Auth, storage, prefs + oauth2_client: ^4.2.0 + flutter_web_auth_2: 5.0.0-alpha.3 + flutter_secure_storage: 10.0.0-beta.4 + + # Persistence + shared_preferences: ^2.2.2 flutter: uses-material-design: true -