submissions still not working, but oauth implemented and add camera sheet working.

This commit is contained in:
stopflock
2025-07-19 15:59:05 -05:00
parent ef241688c3
commit 99d6ad8a44
6 changed files with 78 additions and 66 deletions

View File

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

View File

@@ -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() {

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

@@ -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 OAuth2 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 **clientID** 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
// autouse flutter_secure_storage when present.
enablePKCE: true,
);
}
/* ───────── Public helpers ───────── */
/// Returns `true` if a nonexpired 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'];
}
}

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),
),
),