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
-