mirror of
https://github.com/FoggedLens/deflock-app.git
synced 2026-02-12 16:52:51 +00:00
@@ -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
|
||||
|
||||
@@ -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 = "../.."
|
||||
}
|
||||
|
||||
|
||||
@@ -1,43 +1,67 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<!-- Location permissions for blue‑dot 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 backward‑compat) -->
|
||||
<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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
// ---------- 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<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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 built‑in for now
|
||||
attempts: j['attempts'] ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 non‑null 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
74
lib/services/auth_service.dart
Normal file
74
lib/services/auth_service.dart
Normal 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'];
|
||||
}
|
||||
}
|
||||
|
||||
66
lib/services/uploader.dart
Normal file
66
lib/services/uploader.dart
Normal 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',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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'),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
|
||||
385
pubspec.lock
385
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"
|
||||
|
||||
18
pubspec.yaml
18
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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user