mirror of
https://github.com/FoggedLens/deflock-app.git
synced 2026-02-12 16:52:51 +00:00
Popup message before submitting first node
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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...",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "下载",
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
155
lib/widgets/submission_guide_dialog.dart
Normal file
155
lib/widgets/submission_guide_dialog.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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+
|
||||
|
||||
Reference in New Issue
Block a user