diff --git a/README.md b/README.md index b3613cf..cf8281c 100644 --- a/README.md +++ b/README.md @@ -107,10 +107,8 @@ cp lib/keys.dart.example lib/keys.dart - Persistent cache for MY submissions: clean up when we see that node appear in overpass results or when older than 24h - Dropdown on "refine tags" page to select acceptable options for camera:mount= - Tutorial / info guide before submitting first node -- Link to OSM node in node_details_sheet - Link to "my changes" on osm (username edit history) - Option to "extract node from way" for nodes attached to a way to allow moving -- Option to "open in other editor" for advanced edits: StreetComplete/EveryDoor/Vespucci/GO!! Map/OSM.org(iD)/Rapid/Level0/OSMand/OrganicMaps/CoMaps ### On Pause - Suspected locations expansion to more regions diff --git a/assets/changelog.json b/assets/changelog.json index 1183816..ac9f0df 100644 --- a/assets/changelog.json +++ b/assets/changelog.json @@ -1,6 +1,6 @@ { "1.3.3": { - "content": "• NEW: Added builtin surveillance device profiles for Rekor and Axis Communications ALPR cameras" + "content": "• NEW: Added builtin surveillance device profiles for Rekor and Axis Communications ALPR cameras\n• NEW: Smart constraint system prevents moving nodes that are part of OSM ways/relations to protect data integrity\n• NEW: \"View on OSM\" button in node details opens the node page on OpenStreetMap.org\n• NEW: \"Advanced Edit\" options with links to iD Editor, RapiD, Vespucci, StreetComplete, Go Map!!, and EveryDoor\n• IMPROVED: Network status messages are now fully localized in all supported languages\n• IMPROVED: Map interaction controls for constrained nodes - zoom/rotate allowed, movement prevented\n• IMPROVED: Debounced snap-back system keeps constrained nodes centered smoothly\n• FIXED: Eliminated duplicate changelog service calls on app updates\n• FIXED: Network status now correctly says \"Node data slow\" instead of \"Camera data slow\"" }, "1.3.2": { "content": "• HOTFIX: Temporarily disabled node editing to prevent OSM database issues while a bug is resolved\n• UX: Fixed Android navigation bar covering settings page content" diff --git a/lib/widgets/advanced_edit_options_sheet.dart b/lib/widgets/advanced_edit_options_sheet.dart new file mode 100644 index 0000000..6f237aa --- /dev/null +++ b/lib/widgets/advanced_edit_options_sheet.dart @@ -0,0 +1,155 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:url_launcher/url_launcher.dart'; +import '../models/osm_node.dart'; +import '../services/localization_service.dart'; + +class AdvancedEditOptionsSheet extends StatelessWidget { + final OsmNode node; + + const AdvancedEditOptionsSheet({super.key, required this.node}); + + @override + Widget build(BuildContext context) { + final locService = LocalizationService.instance; + + return SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Advanced Editing Options', + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 8), + Text( + 'These editors offer more advanced features for complex edits.', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).textTheme.bodySmall?.color, + ), + ), + const SizedBox(height: 16), + + // Web Editors Section + Text( + 'Web Editors', + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 8), + _buildEditorTile( + context: context, + icon: Icons.public, + title: 'iD Editor', + subtitle: 'Full-featured web editor - always works', + onTap: () => _launchEditor(context, 'https://www.openstreetmap.org/edit?editor=id&node=${node.id}'), + ), + _buildEditorTile( + context: context, + icon: Icons.speed, + title: 'RapiD Editor', + subtitle: 'AI-assisted editing with Facebook data', + onTap: () => _launchEditor(context, 'https://rapideditor.org/edit#map=19/0/0&nodes=${node.id}'), + ), + + const SizedBox(height: 16), + + // Mobile Editors Section + Text( + 'Mobile Editors', + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 8), + + if (Platform.isAndroid) ...[ + _buildEditorTile( + context: context, + icon: Icons.android, + title: 'Vespucci', + subtitle: 'Advanced Android OSM editor', + onTap: () => _launchEditor(context, 'vespucci://edit?node=${node.id}'), + ), + _buildEditorTile( + context: context, + icon: Icons.place, + title: 'StreetComplete', + subtitle: 'Survey-based mapping app', + onTap: () => _launchEditor(context, 'streetcomplete://quest?node=${node.id}'), + ), + _buildEditorTile( + context: context, + icon: Icons.map, + title: 'EveryDoor', + subtitle: 'Fast POI editing', + onTap: () => _launchEditor(context, 'everydoor://edit?node=${node.id}'), + ), + ], + + if (Platform.isIOS) ...[ + _buildEditorTile( + context: context, + icon: Icons.phone_iphone, + title: 'Go Map!!', + subtitle: 'iOS OSM editor', + onTap: () => _launchEditor(context, 'gomaposm://edit?node=${node.id}'), + ), + ], + + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(locService.t('actions.close')), + ), + ], + ), + ], + ), + ), + ); + } + + Widget _buildEditorTile({ + required BuildContext context, + required IconData icon, + required String title, + required String subtitle, + required VoidCallback onTap, + }) { + return ListTile( + leading: Icon(icon, size: 24), + title: Text(title), + subtitle: Text(subtitle), + trailing: const Icon(Icons.launch, size: 18), + onTap: onTap, + contentPadding: const EdgeInsets.symmetric(horizontal: 0, vertical: 4), + ); + } + + void _launchEditor(BuildContext context, String url) async { + Navigator.pop(context); // Close the sheet first + + try { + final uri = Uri.parse(url); + if (await canLaunchUrl(uri)) { + await launchUrl(uri, mode: LaunchMode.externalApplication); + } else { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Could not open $url - app may not be installed')), + ); + } + } + } catch (e) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Could not open editor - app may not be installed')), + ); + } + } + } +} \ No newline at end of file diff --git a/lib/widgets/edit_node_sheet.dart b/lib/widgets/edit_node_sheet.dart index 3bef64c..83d2772 100644 --- a/lib/widgets/edit_node_sheet.dart +++ b/lib/widgets/edit_node_sheet.dart @@ -8,6 +8,7 @@ import '../models/operator_profile.dart'; import '../services/localization_service.dart'; import '../state/settings_state.dart'; import 'refine_tags_sheet.dart'; +import 'advanced_edit_options_sheet.dart'; class EditNodeSheet extends StatelessWidget { const EditNodeSheet({super.key, required this.session}); @@ -214,15 +215,33 @@ class EditNodeSheet extends StatelessWidget { if (session.originalNode.isConstrained) Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 12), - child: Row( + child: Column( children: [ - const Icon(Icons.info_outline, size: 20), - const SizedBox(width: 8), - Expanded( - child: Text( - locService.t('editNode.cannotMoveConstrainedNode'), - style: Theme.of(context).textTheme.bodyMedium, - ), + Row( + children: [ + const Icon(Icons.info_outline, size: 20), + const SizedBox(width: 8), + Expanded( + child: Text( + locService.t('editNode.cannotMoveConstrainedNode'), + style: Theme.of(context).textTheme.bodyMedium, + ), + ), + ], + ), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + OutlinedButton.icon( + onPressed: () => _openAdvancedEdit(context), + icon: const Icon(Icons.open_in_new, size: 16), + label: const Text('Use Advanced Editor'), + style: OutlinedButton.styleFrom( + minimumSize: const Size(0, 32), + ), + ), + ], ), ], ), @@ -349,4 +368,12 @@ class EditNodeSheet extends StatelessWidget { }, ); } + + void _openAdvancedEdit(BuildContext context) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (context) => AdvancedEditOptionsSheet(node: session.originalNode), + ); + } } \ No newline at end of file diff --git a/lib/widgets/node_tag_sheet.dart b/lib/widgets/node_tag_sheet.dart index 1763689..7e3ab48 100644 --- a/lib/widgets/node_tag_sheet.dart +++ b/lib/widgets/node_tag_sheet.dart @@ -1,8 +1,10 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:url_launcher/url_launcher.dart'; import '../models/osm_node.dart'; import '../app_state.dart'; import '../services/localization_service.dart'; +import 'advanced_edit_options_sheet.dart'; class NodeTagSheet extends StatelessWidget { final OsmNode node; @@ -67,6 +69,36 @@ class NodeTagSheet extends StatelessWidget { } } + void _viewOnOSM() async { + final url = 'https://www.openstreetmap.org/node/${node.id}'; + try { + final uri = Uri.parse(url); + if (await canLaunchUrl(uri)) { + await launchUrl(uri, mode: LaunchMode.externalApplication); + } else { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Could not open OSM website')), + ); + } + } + } catch (e) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Could not open OSM website')), + ); + } + } + } + + void _openAdvancedEdit() { + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (context) => AdvancedEditOptionsSheet(node: node), + ); + } + return SafeArea( child: Padding( padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20), @@ -108,6 +140,30 @@ class NodeTagSheet extends StatelessWidget { ), ), const SizedBox(height: 16), + // First row: View and Advanced buttons + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton.icon( + onPressed: () => _viewOnOSM(), + icon: const Icon(Icons.open_in_new, size: 16), + label: const Text('View on OSM'), + ), + const SizedBox(width: 8), + if (isEditable) ...[ + OutlinedButton.icon( + onPressed: _openAdvancedEdit, + icon: const Icon(Icons.open_in_new, size: 18), + label: const Text('Advanced'), + style: OutlinedButton.styleFrom( + minimumSize: const Size(0, 36), + ), + ), + ], + ], + ), + const SizedBox(height: 8), + // Second row: Edit, Delete, and Close buttons Row( mainAxisAlignment: MainAxisAlignment.end, children: [