Popup message before submitting first node

This commit is contained in:
stopflock
2025-11-22 14:56:05 -06:00
parent 7ff04851f4
commit 961465ebb5
15 changed files with 301 additions and 2 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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": "下载",

View File

@@ -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<String, dynamic>? _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<bool> hasSeenSubmissionGuide() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getBool(_hasSeenSubmissionGuideKey) ?? false;
}
/// Mark that user has seen the submission guide popup
Future<void> markSubmissionGuideSeen() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(_hasSeenSubmissionGuideKey, true);
}
/// Check if app version has changed since last launch
Future<bool> hasVersionChanged() async {
final prefs = await SharedPreferences.getInstance();

View File

@@ -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<void>(
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);

View File

@@ -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<void>(
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,

View File

@@ -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<SubmissionGuideDialog> createState() => _SubmissionGuideDialogState();
}
class _SubmissionGuideDialogState extends State<SubmissionGuideDialog> {
bool _dontShowAgain = false;
Future<void> _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,
),
),
),
);
}
}

View File

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