mirror of
https://github.com/FoggedLens/deflock-app.git
synced 2026-02-12 16:52:51 +00:00
submissions still not working, but oauth implemented and add camera sheet working.
This commit is contained in:
@@ -7,8 +7,8 @@
|
||||
<application
|
||||
android:name="${applicationName}"
|
||||
android:label="flock_map_app"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:enableOnBackInvokedCallback="true">
|
||||
<!-- Main Flutter activity -->
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
|
||||
@@ -83,9 +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;
|
||||
|
||||
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() {
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,56 +1,54 @@
|
||||
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 OAuth 2 PKCE login to OpenStreetMap and exposes
|
||||
/// the stored access token & display name.
|
||||
///
|
||||
/// ─ Requirements ─
|
||||
/// • Register an OAuth app at
|
||||
/// https://www.openstreetmap.org/oauth2/applications
|
||||
/// ‑ Redirect URI: flockmap://auth
|
||||
/// • Put that client ID below (replace 'flockmap').
|
||||
/// Handles PKCE OAuth login with OpenStreetMap.
|
||||
class AuthService {
|
||||
static const _clientId = 'lzEr2zjBGZ2TvJWr3QGxNcKxigp-mQ6pRWIUhI_Bqx8';
|
||||
/// Paste the **client ID** shown on the OSM OAuth2 application page
|
||||
/// (it can be alphanumeric like ‘lzEr2zjBGZ2…’).
|
||||
static const String _clientId = 'lzEr2zjBGZ2TvJWr3QGxNcKxigp-mQ6pRWIUhI_Bqx8';
|
||||
static const _redirect = 'flockmap://auth';
|
||||
|
||||
late final OAuth2Helper _helper;
|
||||
|
||||
String? _displayName; // cached after login
|
||||
String? get displayName => _displayName;
|
||||
String? _displayName;
|
||||
|
||||
AuthService() {
|
||||
final client = OAuth2Client(
|
||||
authorizeUrl: 'https://www.openstreetmap.org/oauth2/authorize',
|
||||
tokenUrl: 'https://www.openstreetmap.org/oauth2/token',
|
||||
redirectUri: _redirect,
|
||||
customUriScheme: 'flockmap', // matches redirect scheme
|
||||
customUriScheme: 'flockmap',
|
||||
);
|
||||
|
||||
_helper = OAuth2Helper(
|
||||
client,
|
||||
clientId: _clientId,
|
||||
scopes: ['write_api'],
|
||||
enablePKCE: true, // PKCE flow
|
||||
// No custom token store needed: oauth2_client will
|
||||
// auto‑use flutter_secure_storage when present.
|
||||
enablePKCE: true,
|
||||
);
|
||||
}
|
||||
|
||||
/* ───────── Public helpers ───────── */
|
||||
|
||||
/// Returns `true` if a non‑expired token is stored.
|
||||
Future<bool> isLoggedIn() async =>
|
||||
(await _helper.getTokenFromStorage())?.isExpired() == false;
|
||||
|
||||
/// Launches browser login if necessary; caches display name.
|
||||
String? get displayName => _displayName;
|
||||
|
||||
Future<String?> login() async {
|
||||
final token = await _helper.getToken();
|
||||
if (token?.accessToken == null) return null;
|
||||
_displayName = await _fetchUsername(token!.accessToken!);
|
||||
return _displayName;
|
||||
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 {
|
||||
@@ -58,18 +56,20 @@ class AuthService {
|
||||
_displayName = null;
|
||||
}
|
||||
|
||||
/// Safely fetch current access token (or null).
|
||||
Future<String?> getAccessToken() async =>
|
||||
(await _helper.getTokenFromStorage())?.accessToken;
|
||||
|
||||
/* ───────── Internal ───────── */
|
||||
/* ───────── 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) return null;
|
||||
if (resp.statusCode != 200) {
|
||||
log('fetchUsername response ${resp.statusCode}: ${resp.body}');
|
||||
return null;
|
||||
}
|
||||
return jsonDecode(resp.body)['user']?['display_name'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user