Merge pull request #1 from stopflock/stage5-submissions

Milestone
This commit is contained in:
stopflock
2025-07-19 16:27:22 -05:00
committed by GitHub
14 changed files with 737 additions and 191 deletions

View File

@@ -2,8 +2,13 @@
A minimal Flutter scaffold for mapping and tagging Flockstyle 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

View File

@@ -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 outofbox.
signingConfig = signingConfigs.getByName("debug")
}
}
}
flutter {
// Path up to the Flutter project directory
source = "../.."
}

View File

@@ -1,43 +1,67 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Location permissions for bluedot positioning -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<application
android:label="flock_map_app"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
android:label="flock_map_app"
android:icon="@mipmap/ic_launcher"
android:enableOnBackInvokedCallback="true">
<!-- Main Flutter activity -->
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:hardwareAccelerated="true"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<!-- The theme behind the splash while Flutter initializes -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" />
<!-- Launcher intent -->
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<!-- OPTIONAL: handle flockmap://auth redirect right here
(kept for backwardcompat) -->
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="flockmap" android:host="auth"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<!-- flutter_web_auth_2 callback activity (V2 embedding) -->
<activity
android:name="com.linusu.flutter_web_auth_2.CallbackActivity"
android:exported="true"
android:launchMode="singleTask">
<intent-filter android:label="flutter_web_auth_2">
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<!-- flockmap://auth -->
<data android:scheme="flockmap" android:host="auth"/>
</intent-filter>
</activity>
<!-- Flutter plugin registration flag -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<!-- Required so ProcessTextPlugin can query other apps -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
@@ -45,3 +69,4 @@
</intent>
</queries>
</manifest>

View File

@@ -47,5 +47,23 @@
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<!-- OAuth2 redirect handler -->
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>None</string>
<key>CFBundleURLSchemes</key>
<array>
<string>flockmap</string>
</array>
</dict>
</array>
<!-- (Optional) allow opening the system browser and returning -->
<key>LSApplicationQueriesSchemes</key>
<array>
<string>https</string>
</array>
</dict>
</plist>

View File

@@ -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<CameraProfile> _profiles;
late final Set<CameraProfile> _enabled;
List<CameraProfile> get profiles => List.unmodifiable(_profiles);
bool isEnabled(CameraProfile p) => _enabled.contains(p);
late final List<CameraProfile> _profiles = [CameraProfile.alpr()];
final Set<CameraProfile> _enabled = {};
void toggleProfile(CameraProfile p, bool enable) {
enable ? _enabled.add(p) : _enabled.remove(p);
notifyListeners();
}
List<CameraProfile> get enabledProfiles => _profiles
.where((p) => _enabled.contains(p))
.toList(growable: false);
// ---------- Add-camera session ----------
AddCameraSession? _session;
AddCameraSession? get session => _session;
final List<PendingUpload> _queue = [];
Timer? _uploadTimer;
bool get isLoggedIn => _username != null;
String get username => _username ?? '';
// ---------- Init ----------
Future<void> _init() async {
_enabled.addAll(_profiles);
await _loadQueue();
if (await _auth.isLoggedIn()) {
_username = await _auth.login();
}
_startUploader();
notifyListeners();
}
// ---------- Auth ----------
Future<void> login() async {
_username = await _auth.login();
notifyListeners();
}
Future<void> logout() async {
await _auth.logout();
_username = null;
notifyListeners();
}
// ---------- Profiles ----------
List<CameraProfile> get profiles => List.unmodifiable(_profiles);
bool isEnabled(CameraProfile p) => _enabled.contains(p);
List<CameraProfile> get enabledProfiles =>
_profiles.where(isEnabled).toList(growable: false);
void toggleProfile(CameraProfile p, bool e) {
e ? _enabled.add(p) : _enabled.remove(p);
notifyListeners();
}
// ---------- Addcamera 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<PendingUpload> _queue = [];
List<PendingUpload> 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<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)));
}
// ---------- 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;
}

View File

@@ -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<String, dynamic> toJson() => {
'lat': coord.latitude,
'lon': coord.longitude,
'dir': direction,
'profile': profile.name,
'attempts': attempts,
};
factory PendingUpload.fromJson(Map<String, dynamic> j) => PendingUpload(
coord: LatLng(j['lat'], j['lon']),
direction: j['dir'],
profile: CameraProfile.alpr(), // only builtin for now
attempts: j['attempts'] ?? 0,
);
}

View File

@@ -13,34 +13,25 @@ class HomeScreen extends StatefulWidget {
}
class _HomeScreenState extends State<HomeScreen> {
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
bool _followMe = true;
Future<void> _startAddCamera(BuildContext context) async {
void _openAddCameraSheet() {
final appState = context.read<AppState>();
appState.startAddSession();
final session = appState.session!; // guaranteed nonnull now
final submitted = await showModalBottomSheet<bool>(
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<AppState>();
return Scaffold(
key: _scaffoldKey,
appBar: AppBar(
title: const Text('Flock Map'),
actions: [
@@ -61,11 +52,13 @@ class _HomeScreenState extends State<HomeScreen> {
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,
);
}
}

View File

@@ -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
? 'Loggedin as ${appState.username}'
: 'Login 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}'),
),
],
),
);

View File

@@ -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<bool> isLoggedIn() async =>
(await _helper.getTokenFromStorage())?.isExpired() == false;
String? get displayName => _displayName;
Future<String?> 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<void> logout() async {
await _helper.removeAllTokens();
_displayName = null;
}
Future<String?> getAccessToken() async =>
(await _helper.getTokenFromStorage())?.accessToken;
/* ───────── helper ───────── */
Future<String?> _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'];
}
}

View File

@@ -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<bool> upload(PendingUpload p) async {
try {
// 1. open changeset
final csXml = '''
<osm>
<changeset>
<tag k="created_by" v="FlockMap 0.5"/>
<tag k="comment" v="Add surveillance camera"/>
</changeset>
</osm>''';
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 = '''
<osm>
<node changeset="$csId" lat="${p.coord.latitude}" lon="${p.coord.longitude}">
<tag k="man_made" v="surveillance"/>
<tag k="surveillance:type" v="ALPR"/>
<tag k="camera:type" v="fixed"/>
<tag k="direction" v="${p.direction.round()}"/>
</node>
</osm>''';
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<http.Response> _post(String path, String body) => http.post(
Uri.https('api.openstreetmap.org', path),
headers: _headers,
body: body,
);
Future<http.Response> _put(String path, String body) => http.put(
Uri.https('api.openstreetmap.org', path),
headers: _headers,
body: body,
);
Map<String, String> get _headers => {
'Authorization': 'Bearer $accessToken',
'Content-Type': 'text/xml',
};
}

View File

@@ -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<AppState>();
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<CameraProfile>(
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'),
),
),

View File

@@ -173,13 +173,13 @@ class _MapViewState extends State<MapView> {
// 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),
),
),

View File

@@ -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"

View File

@@ -1,25 +1,29 @@
name: flock_map_app
description: Simple OSM cameramapping 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