Skip to content

Commit 036ac8c

Browse files
authored
Merge pull request #166 from FoggedLens/feature/node-deeplink
Feature/node deeplink
2 parents b00e44e + 191b5cc commit 036ac8c

4 files changed

Lines changed: 112 additions & 0 deletions

File tree

android/app/src/main/AndroidManifest.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
<category android:name="android.intent.category.DEFAULT"/>
4343
<category android:name="android.intent.category.BROWSABLE"/>
4444
<data android:scheme="deflockapp" android:host="profiles"/>
45+
<data android:scheme="deflockapp" android:host="node"/>
4546
</intent-filter>
4647
</activity>
4748

lib/screens/home_screen.dart

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import '../models/osm_node.dart';
1919
import '../models/suspected_location.dart';
2020
import '../models/search_result.dart';
2121
import '../services/changelog_service.dart';
22+
import '../services/deep_link_service.dart';
2223
import 'coordinators/sheet_coordinator.dart';
2324
import 'coordinators/navigation_coordinator.dart';
2425
import 'coordinators/map_interaction_handler.dart';
@@ -57,10 +58,12 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
5758
_sheetCoordinator = SheetCoordinator();
5859
_navigationCoordinator = NavigationCoordinator();
5960
_mapInteractionHandler = MapInteractionHandler();
61+
DeepLinkService().onNodeDeepLink = _handleNodeDeepLink;
6062
}
6163

6264
@override
6365
void dispose() {
66+
DeepLinkService().onNodeDeepLink = null;
6467
_mapController.dispose();
6568
super.dispose();
6669
}
@@ -286,6 +289,25 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
286289
);
287290
}
288291

292+
void _handleNodeDeepLink(OsmNode node) {
293+
try {
294+
_mapController.animateTo(
295+
dest: node.coord,
296+
zoom: 16.0,
297+
duration: const Duration(milliseconds: 600),
298+
curve: Curves.easeOut,
299+
);
300+
} catch (e) {
301+
debugPrint('[HomeScreen] Could not animate to deep link node: $e');
302+
}
303+
304+
Future.delayed(const Duration(milliseconds: 700), () {
305+
if (mounted) {
306+
openNodeTagSheet(node);
307+
}
308+
});
309+
}
310+
289311
void openNodeTagSheet(OsmNode node) {
290312
// Handle the map interaction (centering and follow-me disable)
291313
_mapInteractionHandler.handleNodeTap(

lib/services/deep_link_service.dart

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
import 'dart:async';
2+
import 'dart:convert';
23
import 'package:app_links/app_links.dart';
34
import 'package:flutter/material.dart';
5+
import 'package:latlong2/latlong.dart';
46

57
import '../models/node_profile.dart';
68
import '../models/operator_profile.dart';
9+
import '../models/osm_node.dart';
10+
import 'http_client.dart';
711
import 'profile_import_service.dart';
812
import 'operator_profile_import_service.dart';
913
import '../screens/profile_editor.dart';
@@ -16,6 +20,9 @@ class DeepLinkService {
1620

1721
late AppLinks _appLinks;
1822
StreamSubscription<Uri>? _linkSubscription;
23+
24+
/// Callback for HomeScreen to receive node deep links
25+
void Function(OsmNode node)? onNodeDeepLink;
1926

2027
/// Initialize deep link handling (sets up stream listener only)
2128
Future<void> init() async {
@@ -45,6 +52,9 @@ class DeepLinkService {
4552
case 'profiles':
4653
_handleProfilesLink(uri);
4754
break;
55+
case 'node':
56+
_handleNodeLink(uri);
57+
break;
4858
case 'auth':
4959
// OAuth links are handled by flutter_web_auth_2
5060
debugPrint('[DeepLinkService] OAuth link handled by flutter_web_auth_2');
@@ -71,6 +81,59 @@ class DeepLinkService {
7181
}
7282
}
7383

84+
/// Handle node deep link: `deflockapp://node?id=<nodeId>`
85+
Future<void> _handleNodeLink(Uri uri) async {
86+
final idStr = uri.queryParameters['id'];
87+
final nodeId = int.tryParse(idStr ?? '');
88+
if (nodeId == null) {
89+
_showError('Invalid node link: missing or invalid ID');
90+
return;
91+
}
92+
93+
final node = await _fetchNodeById(nodeId);
94+
if (node == null) {
95+
_showError('Node $nodeId not found');
96+
return;
97+
}
98+
99+
if (onNodeDeepLink != null) {
100+
onNodeDeepLink!(node);
101+
} else {
102+
debugPrint('[DeepLinkService] No node deep link handler registered');
103+
}
104+
}
105+
106+
/// Fetch an OSM node by ID from the OpenStreetMap API
107+
Future<OsmNode?> _fetchNodeById(int nodeId) async {
108+
try {
109+
final url = Uri.parse('https://api.openstreetmap.org/api/0.6/node/$nodeId.json');
110+
final client = UserAgentClient();
111+
final response = await client.get(url);
112+
if (response.statusCode != 200) return null;
113+
114+
final json = jsonDecode(response.body);
115+
final elements = json['elements'] as List?;
116+
if (elements == null || elements.isEmpty) return null;
117+
118+
final e = elements[0];
119+
final tags = <String, String>{};
120+
if (e['tags'] != null) {
121+
(e['tags'] as Map<String, dynamic>).forEach((k, v) {
122+
tags[k] = v.toString();
123+
});
124+
}
125+
126+
return OsmNode(
127+
id: e['id'] as int,
128+
coord: LatLng((e['lat'] as num).toDouble(), (e['lon'] as num).toDouble()),
129+
tags: tags,
130+
);
131+
} catch (e) {
132+
debugPrint('[DeepLinkService] Failed to fetch node $nodeId: $e');
133+
return null;
134+
}
135+
}
136+
74137
/// Handle profile-related deep links
75138
void _handleProfilesLink(Uri uri) {
76139
final segments = uri.pathSegments;

test/deep_link_service_test.dart

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import 'package:flutter_test/flutter_test.dart';
2+
3+
void main() {
4+
group('Node deep link URL parsing', () {
5+
test('extracts node ID from valid link', () {
6+
final uri = Uri.parse('deflockapp://node?id=1234567890');
7+
expect(uri.host, 'node');
8+
expect(uri.queryParameters['id'], '1234567890');
9+
});
10+
11+
test('returns null for missing id param', () {
12+
final uri = Uri.parse('deflockapp://node');
13+
expect(uri.queryParameters['id'], isNull);
14+
});
15+
16+
test('returns null for empty id param', () {
17+
final uri = Uri.parse('deflockapp://node?id=');
18+
expect(uri.queryParameters['id'], '');
19+
});
20+
21+
test('returns null for non-numeric id', () {
22+
final uri = Uri.parse('deflockapp://node?id=abc');
23+
expect(int.tryParse(uri.queryParameters['id'] ?? ''), isNull);
24+
});
25+
});
26+
}

0 commit comments

Comments
 (0)