From 961465ebb5b46d04459a1a16f2ffdbd90b6ddc45 Mon Sep 17 00:00:00 2001 From: stopflock Date: Sat, 22 Nov 2025 14:56:05 -0600 Subject: [PATCH] Popup message before submitting first node --- DEVELOPER.md | 2 + README.md | 2 +- assets/changelog.json | 6 + lib/localizations/de.json | 11 ++ lib/localizations/en.json | 11 ++ lib/localizations/es.json | 11 ++ lib/localizations/fr.json | 11 ++ lib/localizations/it.json | 11 ++ lib/localizations/pt.json | 11 ++ lib/localizations/zh.json | 11 ++ lib/services/changelog_service.dart | 13 ++ lib/widgets/add_node_sheet.dart | 23 ++++ lib/widgets/edit_node_sheet.dart | 23 ++++ lib/widgets/submission_guide_dialog.dart | 155 +++++++++++++++++++++++ pubspec.yaml | 2 +- 15 files changed, 301 insertions(+), 2 deletions(-) create mode 100644 lib/widgets/submission_guide_dialog.dart diff --git a/DEVELOPER.md b/DEVELOPER.md index fd12fdf..499bce5 100644 --- a/DEVELOPER.md +++ b/DEVELOPER.md @@ -72,6 +72,7 @@ The app includes a comprehensive system for welcoming new users and notifying ex ### Components - **ChangelogService**: Manages version tracking and changelog loading - **WelcomeDialog**: First launch popup with privacy information and quick links +- **SubmissionGuideDialog**: One-time popup before first node submission with best practices - **ChangelogDialog**: Update notification popup for version changes - **ReleaseNotesScreen**: Settings page for viewing all changelog history @@ -96,6 +97,7 @@ Changelog content is stored in `assets/changelog.json`: ### User Experience Flow - **First Launch**: Welcome popup with "don't show again" option +- **First Submission**: Submission guide popup with best practices and resource links - **Version Updates**: Changelog popup (only if content exists, no "don't show again") - **Settings Access**: Complete changelog history available in Settings > About > Release Notes diff --git a/README.md b/README.md index f2b1eaf..5760328 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ A comprehensive Flutter app for mapping public surveillance infrastructure with 1. **Install** the app on iOS or Android - a welcome popup will guide you through key information 2. **Enable location** permissions 3. **Log into OpenStreetMap**: Choose upload mode and get OAuth2 credentials -4. **Add your first device**: Tap the "New Node" button, position the pin, set direction(s), select a profile, and tap submit +4. **Add your first device**: Tap the "New Node" button, position the pin, set direction(s), select a profile, and tap submit - a guidance popup will help you with best practices on your first submission 5. **Edit or delete devices**: Tap any device marker to view details, then use Edit or Delete buttons **New to OpenStreetMap?** Visit [deflock.me](https://deflock.me) for complete setup instructions and community guidelines. diff --git a/assets/changelog.json b/assets/changelog.json index 7623581..6ad1034 100644 --- a/assets/changelog.json +++ b/assets/changelog.json @@ -1,4 +1,10 @@ { + "1.5.0": { + "content": [ + "• NEW: First-submission guide popup - provides essential guidance and links before your first device submission", + "• IMPROVED: Better onboarding for new contributors with links to identification guides and OSM wiki resources" + ] + }, "1.4.6": { "content": [ "• IMPROVED: Tile fetching reliability - removed retry limits so visible tiles always load eventually", diff --git a/lib/localizations/de.json b/lib/localizations/de.json index 1bcf8b9..5eaeace 100644 --- a/lib/localizations/de.json +++ b/lib/localizations/de.json @@ -405,6 +405,17 @@ "dontShowAgain": "Diese Willkommensnachricht nicht mehr anzeigen", "getStarted": "Los geht's mit DeFlocking!" }, + "submissionGuide": { + "title": "Einreichungs-Richtlinien", + "description": "Bevor Sie Ihr erstes Überwachungsgerät einreichen, lesen Sie bitte diese wichtigen Richtlinien für qualitativ hochwertige Beiträge zu OpenStreetMap.", + "bestPractices": "• Nur Geräte erfassen, die Sie persönlich beobachtet haben\n• Zeit nehmen für genaue Identifikation von Typ und Hersteller\n• Präzise Positionierung - nah heranzoomen vor Markierung\n• Richtungsinformationen angeben, falls zutreffend\n• Tag-Auswahl vor dem Senden überprüfen", + "placementNote": "Denken Sie daran: Genaue, persönlich verifizierte Daten sind essentiell für die DeFlock-Community und das OpenStreetMap-Projekt.", + "moreInfo": "Für detaillierte Anleitungen zur Geräteerkennung und Kartierung:", + "identificationGuide": "ID-Leitfaden", + "osmWiki": "OSM Wiki", + "dontShowAgain": "Diese Anleitung nicht mehr anzeigen", + "gotIt": "Verstanden!" + }, "navigation": { "searchLocation": "Ort suchen", "searchPlaceholder": "Orte oder Koordinaten suchen...", diff --git a/lib/localizations/en.json b/lib/localizations/en.json index 4ec930e..a29502d 100644 --- a/lib/localizations/en.json +++ b/lib/localizations/en.json @@ -23,6 +23,17 @@ "dontShowAgain": "Don't show this welcome message again", "getStarted": "Let's Get DeFlocking!" }, + "submissionGuide": { + "title": "Submission Best Practices", + "description": "Before submitting your first surveillance device, please take a moment to review these important guidelines to ensure high-quality contributions to OpenStreetMap.", + "bestPractices": "• Only map devices you've personally observed firsthand\n• Take time to accurately identify the device type and manufacturer\n• Use precise positioning - zoom in close before placing the marker\n• Include direction information when applicable\n• Double-check your tag selections before submitting", + "placementNote": "Remember: Accurate, first-hand data is essential for the DeFlock community and OpenStreetMap project.", + "moreInfo": "For detailed guidance on device identification and mapping best practices:", + "identificationGuide": "ID Guide", + "osmWiki": "OSM Wiki", + "dontShowAgain": "Don't show this guide again", + "gotIt": "Got It!" + }, "actions": { "tagNode": "New Node", "download": "Download", diff --git a/lib/localizations/es.json b/lib/localizations/es.json index 3acaff6..ab0ae55 100644 --- a/lib/localizations/es.json +++ b/lib/localizations/es.json @@ -23,6 +23,17 @@ "dontShowAgain": "No mostrar este mensaje de bienvenida otra vez", "getStarted": "¡Comencemos con DeFlock!" }, + "submissionGuide": { + "title": "Mejores Prácticas de Envío", + "description": "Antes de enviar su primer dispositivo de vigilancia, tómese un momento para revisar estas pautas importantes para contribuciones de alta calidad a OpenStreetMap.", + "bestPractices": "• Solo mapee dispositivos que haya observado personalmente\n• Tómese tiempo para identificar con precisión el tipo y fabricante\n• Use posicionamiento preciso - acerque antes de colocar el marcador\n• Incluya información de dirección cuando sea aplicable\n• Verifique sus selecciones de etiquetas antes de enviar", + "placementNote": "Recuerde: Los datos precisos y de primera mano son esenciales para la comunidad DeFlock y el proyecto OpenStreetMap.", + "moreInfo": "Para orientación detallada sobre identificación de dispositivos y mejores prácticas de mapeo:", + "identificationGuide": "Guía de ID", + "osmWiki": "Wiki OSM", + "dontShowAgain": "No mostrar esta guía otra vez", + "gotIt": "¡Entendido!" + }, "actions": { "tagNode": "Nuevo Nodo", "download": "Descargar", diff --git a/lib/localizations/fr.json b/lib/localizations/fr.json index f4b63df..1e6f62e 100644 --- a/lib/localizations/fr.json +++ b/lib/localizations/fr.json @@ -23,6 +23,17 @@ "dontShowAgain": "Ne plus afficher ce message de bienvenue", "getStarted": "Commençons le DeFlock !" }, + "submissionGuide": { + "title": "Meilleures Pratiques de Soumission", + "description": "Avant de soumettre votre premier dispositif de surveillance, prenez un moment pour examiner ces directives importantes pour des contributions de haute qualité à OpenStreetMap.", + "bestPractices": "• Ne cartographiez que les dispositifs que vous avez observés personnellement\n• Prenez le temps d'identifier avec précision le type et le fabricant\n• Utilisez un positionnement précis - zoomez avant de placer le marqueur\n• Incluez les informations de direction quand c'est applicable\n• Vérifiez vos sélections d'étiquettes avant de soumettre", + "placementNote": "Rappelez-vous : Des données précises et de première main sont essentielles pour la communauté DeFlock et le projet OpenStreetMap.", + "moreInfo": "Pour des conseils détaillés sur l'identification des dispositifs et les meilleures pratiques de cartographie :", + "identificationGuide": "Guide ID", + "osmWiki": "Wiki OSM", + "dontShowAgain": "Ne plus afficher ce guide", + "gotIt": "Compris !" + }, "actions": { "tagNode": "Nouveau Nœud", "download": "Télécharger", diff --git a/lib/localizations/it.json b/lib/localizations/it.json index 0386666..8401107 100644 --- a/lib/localizations/it.json +++ b/lib/localizations/it.json @@ -23,6 +23,17 @@ "dontShowAgain": "Non mostrare più questo messaggio di benvenuto", "getStarted": "Iniziamo con DeFlock!" }, + "submissionGuide": { + "title": "Migliori Pratiche di Invio", + "description": "Prima di inviare il tuo primo dispositivo di sorveglianza, prenditi un momento per rivedere queste linee guida importanti per contributi di alta qualità a OpenStreetMap.", + "bestPractices": "• Mappa solo dispositivi che hai osservato personalmente\n• Prenditi tempo per identificare accuratamente tipo e produttore\n• Usa posizionamento preciso - ingrandisci prima di piazzare il marcatore\n• Includi informazioni sulla direzione quando applicabile\n• Controlla le tue selezioni di tag prima di inviare", + "placementNote": "Ricorda: Dati accurati e di prima mano sono essenziali per la comunità DeFlock e il progetto OpenStreetMap.", + "moreInfo": "Per una guida dettagliata sull'identificazione dei dispositivi e le migliori pratiche di mappatura:", + "identificationGuide": "Guida ID", + "osmWiki": "Wiki OSM", + "dontShowAgain": "Non mostrare più questa guida", + "gotIt": "Capito!" + }, "actions": { "tagNode": "Nuovo Nodo", "download": "Scarica", diff --git a/lib/localizations/pt.json b/lib/localizations/pt.json index 7c4e967..ac00603 100644 --- a/lib/localizations/pt.json +++ b/lib/localizations/pt.json @@ -23,6 +23,17 @@ "dontShowAgain": "Não mostrar esta mensagem de boas-vindas novamente", "getStarted": "Vamos começar com o DeFlock!" }, + "submissionGuide": { + "title": "Melhores Práticas de Submissão", + "description": "Antes de submeter seu primeiro dispositivo de vigilância, dedique um momento para revisar estas diretrizes importantes para contribuições de alta qualidade ao OpenStreetMap.", + "bestPractices": "• Mapear apenas dispositivos que você observou pessoalmente\n• Dedicar tempo para identificar com precisão tipo e fabricante\n• Usar posicionamento preciso - aproximar antes de colocar o marcador\n• Incluir informações de direção quando aplicável\n• Verificar suas seleções de tags antes de submeter", + "placementNote": "Lembre-se: Dados precisos e de primeira mão são essenciais para a comunidade DeFlock e o projeto OpenStreetMap.", + "moreInfo": "Para orientação detalhada sobre identificação de dispositivos e melhores práticas de mapeamento:", + "identificationGuide": "Guia de ID", + "osmWiki": "Wiki OSM", + "dontShowAgain": "Não mostrar este guia novamente", + "gotIt": "Entendi!" + }, "actions": { "tagNode": "Novo Nó", "download": "Baixar", diff --git a/lib/localizations/zh.json b/lib/localizations/zh.json index 0bedaf3..2145010 100644 --- a/lib/localizations/zh.json +++ b/lib/localizations/zh.json @@ -23,6 +23,17 @@ "dontShowAgain": "不再显示此欢迎消息", "getStarted": "开始使用 DeFlock!" }, + "submissionGuide": { + "title": "提交最佳实践", + "description": "在提交您的第一个监控设备之前,请花点时间查看这些重要指南,以确保对 OpenStreetMap 的高质量贡献。", + "bestPractices": "• 只映射您亲自观察到的设备\n• 花时间准确识别设备类型和制造商\n• 使用精确定位 - 放置标记前请放大\n• 在适用时包含方向信息\n• 提交前请检查您的标签选择", + "placementNote": "请记住:准确的第一手数据对 DeFlock 社区和 OpenStreetMap 项目至关重要。", + "moreInfo": "有关设备识别和映射最佳实践的详细指导:", + "identificationGuide": "识别指南", + "osmWiki": "OSM Wiki", + "dontShowAgain": "不再显示此指南", + "gotIt": "明白了!" + }, "actions": { "tagNode": "新建节点", "download": "下载", diff --git a/lib/services/changelog_service.dart b/lib/services/changelog_service.dart index cd5a74e..383460b 100644 --- a/lib/services/changelog_service.dart +++ b/lib/services/changelog_service.dart @@ -13,6 +13,7 @@ class ChangelogService { static const String _lastSeenVersionKey = 'last_seen_version'; static const String _hasSeenWelcomeKey = 'has_seen_welcome'; + static const String _hasSeenSubmissionGuideKey = 'has_seen_submission_guide'; Map? _changelogData; bool _initialized = false; @@ -67,6 +68,18 @@ class ChangelogService { await prefs.setBool(_hasSeenWelcomeKey, true); } + /// Check if user has seen the submission guide popup + Future hasSeenSubmissionGuide() async { + final prefs = await SharedPreferences.getInstance(); + return prefs.getBool(_hasSeenSubmissionGuideKey) ?? false; + } + + /// Mark that user has seen the submission guide popup + Future markSubmissionGuideSeen() async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool(_hasSeenSubmissionGuideKey, true); + } + /// Check if app version has changed since last launch Future hasVersionChanged() async { final prefs = await SharedPreferences.getInstance(); diff --git a/lib/widgets/add_node_sheet.dart b/lib/widgets/add_node_sheet.dart index 3a15673..c148c89 100644 --- a/lib/widgets/add_node_sheet.dart +++ b/lib/widgets/add_node_sheet.dart @@ -7,8 +7,10 @@ import '../models/node_profile.dart'; import '../models/operator_profile.dart'; import '../services/localization_service.dart'; import '../services/node_cache.dart'; +import '../services/changelog_service.dart'; import 'refine_tags_sheet.dart'; import 'proximity_warning_dialog.dart'; +import 'submission_guide_dialog.dart'; class AddNodeSheet extends StatelessWidget { const AddNodeSheet({super.key, required this.session}); @@ -16,6 +18,27 @@ class AddNodeSheet extends StatelessWidget { final AddNodeSession session; void _checkProximityAndCommit(BuildContext context, AppState appState, LocalizationService locService) { + _checkSubmissionGuideAndProceed(context, appState, locService); + } + + void _checkSubmissionGuideAndProceed(BuildContext context, AppState appState, LocalizationService locService) async { + // Check if user has seen the submission guide + final hasSeenGuide = await ChangelogService().hasSeenSubmissionGuide(); + + if (!hasSeenGuide) { + // Show submission guide dialog first + await showDialog( + context: context, + barrierDismissible: false, + builder: (context) => const SubmissionGuideDialog(), + ); + } + + // Now proceed with proximity check + _checkProximityOnly(context, appState, locService); + } + + void _checkProximityOnly(BuildContext context, AppState appState, LocalizationService locService) { // Only check proximity if we have a target location if (session.target == null) { _commitWithoutCheck(context, appState, locService); diff --git a/lib/widgets/edit_node_sheet.dart b/lib/widgets/edit_node_sheet.dart index 4a46cbc..123bb97 100644 --- a/lib/widgets/edit_node_sheet.dart +++ b/lib/widgets/edit_node_sheet.dart @@ -7,10 +7,12 @@ import '../models/node_profile.dart'; import '../models/operator_profile.dart'; import '../services/localization_service.dart'; import '../services/node_cache.dart'; +import '../services/changelog_service.dart'; import '../state/settings_state.dart'; import 'refine_tags_sheet.dart'; import 'advanced_edit_options_sheet.dart'; import 'proximity_warning_dialog.dart'; +import 'submission_guide_dialog.dart'; class EditNodeSheet extends StatelessWidget { const EditNodeSheet({super.key, required this.session}); @@ -18,6 +20,27 @@ class EditNodeSheet extends StatelessWidget { final EditNodeSession session; void _checkProximityAndCommit(BuildContext context, AppState appState, LocalizationService locService) { + _checkSubmissionGuideAndProceed(context, appState, locService); + } + + void _checkSubmissionGuideAndProceed(BuildContext context, AppState appState, LocalizationService locService) async { + // Check if user has seen the submission guide + final hasSeenGuide = await ChangelogService().hasSeenSubmissionGuide(); + + if (!hasSeenGuide) { + // Show submission guide dialog first + await showDialog( + context: context, + barrierDismissible: false, + builder: (context) => const SubmissionGuideDialog(), + ); + } + + // Now proceed with proximity check + _checkProximityOnly(context, appState, locService); + } + + void _checkProximityOnly(BuildContext context, AppState appState, LocalizationService locService) { // Check for nearby nodes within the configured distance, excluding the node being edited final nearbyNodes = NodeCache.instance.findNodesWithinDistance( session.target, diff --git a/lib/widgets/submission_guide_dialog.dart b/lib/widgets/submission_guide_dialog.dart new file mode 100644 index 0000000..8a07525 --- /dev/null +++ b/lib/widgets/submission_guide_dialog.dart @@ -0,0 +1,155 @@ +import 'package:flutter/material.dart'; +import 'package:url_launcher/url_launcher.dart'; +import '../services/changelog_service.dart'; +import '../services/localization_service.dart'; + +class SubmissionGuideDialog extends StatefulWidget { + const SubmissionGuideDialog({super.key}); + + @override + State createState() => _SubmissionGuideDialogState(); +} + +class _SubmissionGuideDialogState extends State { + bool _dontShowAgain = false; + + Future _launchUrl(String url) async { + final uri = Uri.parse(url); + if (await canLaunchUrl(uri)) { + await launchUrl(uri, mode: LaunchMode.externalApplication); + } + } + + void _onClose() async { + if (_dontShowAgain) { + await ChangelogService().markSubmissionGuideSeen(); + } + + if (mounted) { + Navigator.of(context).pop(); + } + } + + @override + Widget build(BuildContext context) { + final locService = LocalizationService.instance; + + return AnimatedBuilder( + animation: LocalizationService.instance, + builder: (context, child) => AlertDialog( + title: Text(locService.t('submissionGuide.title')), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Scrollable content + Flexible( + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + locService.t('submissionGuide.description'), + style: const TextStyle(fontSize: 14), + ), + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.blue.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.blue.withOpacity(0.3)), + ), + child: Text( + locService.t('submissionGuide.bestPractices'), + style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500), + ), + ), + const SizedBox(height: 12), + Text( + locService.t('submissionGuide.placementNote'), + style: const TextStyle(fontSize: 13, fontStyle: FontStyle.italic), + ), + const SizedBox(height: 16), + Text( + locService.t('submissionGuide.moreInfo'), + style: const TextStyle(fontSize: 13), + ), + const SizedBox(height: 16), + // Resource links row + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _buildLinkButton( + locService.t('submissionGuide.identificationGuide'), + 'https://deflock.me/identify' + ), + _buildLinkButton( + locService.t('submissionGuide.osmWiki'), + 'https://wiki.openstreetmap.org/wiki/Tag:man_made%3Dsurveillance' + ), + ], + ), + ], + ), + ), + ), + const SizedBox(height: 16), + // Always visible checkbox at the bottom + Row( + children: [ + Checkbox( + value: _dontShowAgain, + onChanged: (value) { + setState(() { + _dontShowAgain = value ?? false; + }); + }, + ), + Expanded( + child: Text( + locService.t('submissionGuide.dontShowAgain'), + style: const TextStyle(fontSize: 13), + ), + ), + ], + ), + ], + ), + actions: [ + TextButton( + onPressed: _onClose, + child: Text(locService.t('submissionGuide.gotIt')), + ), + ], + ), + ); + } + + Widget _buildLinkButton(String text, String url) { + return Flexible( + child: GestureDetector( + onTap: () => _launchUrl(url), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary.withOpacity(0.1), + borderRadius: BorderRadius.circular(4), + border: Border.all( + color: Theme.of(context).colorScheme.primary.withOpacity(0.3), + ), + ), + child: Text( + text, + style: TextStyle( + fontSize: 12, + color: Theme.of(context).colorScheme.primary, + ), + textAlign: TextAlign.center, + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index 24e8b74..55713d8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: deflockapp description: Map public surveillance infrastructure with OpenStreetMap publish_to: "none" -version: 1.4.6+17 # The thing after the + is the version code, incremented with each release +version: 1.5.0+18 # The thing after the + is the version code, incremented with each release environment: sdk: ">=3.5.0 <4.0.0" # oauth2_client 4.x needs Dart 3.5+