diff --git a/assets/changelog.json b/assets/changelog.json index 7c6e0e0..4e6ebdb 100644 --- a/assets/changelog.json +++ b/assets/changelog.json @@ -1,7 +1,10 @@ { + "1.2.8": { + "content": "• UX: Profile selection is now a required step to prevent accidental submission of default profile.\n• NEW: Note in welcome message about not submitting data you cannot vouch for personally (no street view etc)\n• NEW: Added default operator profiles for the most common private operators nationwide (Lowe's, Home Depot, et al)" + }, "1.2.7": { "content": "• NEW: Compass indicator shows map orientation; tap to spin north-up\n• Smart area caching: Loads 3x larger areas and refreshes data every 60 seconds for much faster browsing\n• Enhanced tile loading: Increased retry attempts with faster delays - tiles load much more reliably\n• Better network status: Simplified loading indicator logic\n• Instant node display: Surveillance devices now appear immediately when data finishes loading\n• Node limit alerts: Get notified when some nodes are not drawn" -}, + }, "1.2.4": { "content": "• New welcome popup for first-time users with essential privacy information\n• Automatic changelog display when app updates (like this one!)\n• Added Release Notes viewer in Settings > About\n• Enhanced user onboarding and transparency about data handling\n• Improved documentation for contributors" }, @@ -11,9 +14,6 @@ "1.2.2": { "content": "• New surveillance device profiles added\n• Improved tile loading performance\n• Fixed issue with GPS accuracy\n• Updated translations" }, - "1.2.1": { - "content": "" - }, "1.2.0": { "content": "• Major UI improvements\n• Added proximity alerts\n• Enhanced offline capabilities\n• New suspected locations feature" } diff --git a/lib/localizations/de.json b/lib/localizations/de.json index 24cc0f3..37545ec 100644 --- a/lib/localizations/de.json +++ b/lib/localizations/de.json @@ -72,6 +72,8 @@ }, "addNode": { "profile": "Profil", + "selectProfile": "Profil auswählen...", + "profileRequired": "Bitte wählen Sie ein Profil aus, um fortzufahren.", "direction": "Richtung {}°", "profileNoDirectionInfo": "Dieses Profil benötigt keine Richtung.", "mustBeLoggedIn": "Sie müssen angemeldet sein, um neue Knoten zu übertragen. Bitte melden Sie sich über die Einstellungen an.", @@ -83,6 +85,8 @@ "editNode": { "title": "Knoten #{} Bearbeiten", "profile": "Profil", + "selectProfile": "Profil auswählen...", + "profileRequired": "Bitte wählen Sie ein Profil aus, um fortzufahren.", "direction": "Richtung {}°", "profileNoDirectionInfo": "Dieses Profil benötigt keine Richtung.", "mustBeLoggedIn": "Sie müssen angemeldet sein, um Knoten zu bearbeiten. Bitte melden Sie sich über die Einstellungen an.", diff --git a/lib/localizations/en.json b/lib/localizations/en.json index 967e658..c8d368c 100644 --- a/lib/localizations/en.json +++ b/lib/localizations/en.json @@ -89,6 +89,8 @@ }, "addNode": { "profile": "Profile", + "selectProfile": "Select a profile...", + "profileRequired": "Please select a profile to continue.", "direction": "Direction {}°", "profileNoDirectionInfo": "This profile does not require a direction.", "mustBeLoggedIn": "You must be logged in to submit new nodes. Please log in via Settings.", @@ -100,6 +102,8 @@ "editNode": { "title": "Edit Node #{}", "profile": "Profile", + "selectProfile": "Select a profile...", + "profileRequired": "Please select a profile to continue.", "direction": "Direction {}°", "profileNoDirectionInfo": "This profile does not require a direction.", "mustBeLoggedIn": "You must be logged in to edit nodes. Please log in via Settings.", diff --git a/lib/localizations/es.json b/lib/localizations/es.json index 28f186d..cc0fcac 100644 --- a/lib/localizations/es.json +++ b/lib/localizations/es.json @@ -89,6 +89,8 @@ }, "addNode": { "profile": "Perfil", + "selectProfile": "Seleccionar un perfil...", + "profileRequired": "Por favor, seleccione un perfil para continuar.", "direction": "Dirección {}°", "profileNoDirectionInfo": "Este perfil no requiere una dirección.", "mustBeLoggedIn": "Debe estar conectado para enviar nuevos nodos. Por favor, inicie sesión a través de Configuración.", @@ -100,6 +102,8 @@ "editNode": { "title": "Editar Nodo #{}", "profile": "Perfil", + "selectProfile": "Seleccionar un perfil...", + "profileRequired": "Por favor, seleccione un perfil para continuar.", "direction": "Dirección {}°", "profileNoDirectionInfo": "Este perfil no requiere una dirección.", "mustBeLoggedIn": "Debe estar conectado para editar nodos. Por favor, inicie sesión a través de Configuración.", diff --git a/lib/localizations/fr.json b/lib/localizations/fr.json index 3f07d98..7f796e8 100644 --- a/lib/localizations/fr.json +++ b/lib/localizations/fr.json @@ -89,6 +89,8 @@ }, "addNode": { "profile": "Profil", + "selectProfile": "Sélectionner un profil...", + "profileRequired": "Veuillez sélectionner un profil pour continuer.", "direction": "Direction {}°", "profileNoDirectionInfo": "Ce profil ne nécessite pas de direction.", "mustBeLoggedIn": "Vous devez être connecté pour soumettre de nouveaux nœuds. Veuillez vous connecter via les Paramètres.", @@ -100,6 +102,8 @@ "editNode": { "title": "Modifier Nœud #{}", "profile": "Profil", + "selectProfile": "Sélectionner un profil...", + "profileRequired": "Veuillez sélectionner un profil pour continuer.", "direction": "Direction {}°", "profileNoDirectionInfo": "Ce profil ne nécessite pas de direction.", "mustBeLoggedIn": "Vous devez être connecté pour modifier les nœuds. Veuillez vous connecter via les Paramètres.", diff --git a/lib/localizations/it.json b/lib/localizations/it.json index 1b2e4c2..0c06203 100644 --- a/lib/localizations/it.json +++ b/lib/localizations/it.json @@ -89,6 +89,8 @@ }, "addNode": { "profile": "Profilo", + "selectProfile": "Seleziona un profilo...", + "profileRequired": "Per favore seleziona un profilo per continuare.", "direction": "Direzione {}°", "profileNoDirectionInfo": "Questo profilo non richiede una direzione.", "mustBeLoggedIn": "Devi essere loggato per inviare nuovi nodi. Per favore accedi tramite Impostazioni.", @@ -100,6 +102,8 @@ "editNode": { "title": "Modifica Nodo #{}", "profile": "Profilo", + "selectProfile": "Seleziona un profilo...", + "profileRequired": "Per favore seleziona un profilo per continuare.", "direction": "Direzione {}°", "profileNoDirectionInfo": "Questo profilo non richiede una direzione.", "mustBeLoggedIn": "Devi essere loggato per modificare i nodi. Per favore accedi tramite Impostazioni.", diff --git a/lib/localizations/pt.json b/lib/localizations/pt.json index 6620a1a..6664e5e 100644 --- a/lib/localizations/pt.json +++ b/lib/localizations/pt.json @@ -89,6 +89,8 @@ }, "addNode": { "profile": "Perfil", + "selectProfile": "Selecionar um perfil...", + "profileRequired": "Por favor, selecione um perfil para continuar.", "direction": "Direção {}°", "profileNoDirectionInfo": "Este perfil não requer uma direção.", "mustBeLoggedIn": "Você deve estar logado para enviar novos nós. Por favor, faça login via Configurações.", @@ -100,6 +102,8 @@ "editNode": { "title": "Editar Nó #{}", "profile": "Perfil", + "selectProfile": "Selecionar um perfil...", + "profileRequired": "Por favor, selecione um perfil para continuar.", "direction": "Direção {}°", "profileNoDirectionInfo": "Este perfil não requer uma direção.", "mustBeLoggedIn": "Você deve estar logado para editar nós. Por favor, faça login via Configurações.", diff --git a/lib/localizations/zh.json b/lib/localizations/zh.json index 282fbd9..321db8f 100644 --- a/lib/localizations/zh.json +++ b/lib/localizations/zh.json @@ -89,6 +89,8 @@ }, "addNode": { "profile": "配置文件", + "selectProfile": "选择配置文件...", + "profileRequired": "请选择配置文件以继续。", "direction": "方向 {}°", "profileNoDirectionInfo": "此配置文件不需要方向。", "mustBeLoggedIn": "您必须登录才能提交新节点。请通过设置登录。", @@ -100,6 +102,8 @@ "editNode": { "title": "编辑节点 #{}", "profile": "配置文件", + "selectProfile": "选择配置文件...", + "profileRequired": "请选择配置文件以继续。", "direction": "方向 {}°", "profileNoDirectionInfo": "此配置文件不需要方向。", "mustBeLoggedIn": "您必须登录才能编辑节点。请通过设置登录。", diff --git a/lib/state/session_state.dart b/lib/state/session_state.dart index 7f3d562..0f289a6 100644 --- a/lib/state/session_state.dart +++ b/lib/state/session_state.dart @@ -7,8 +7,8 @@ import '../models/osm_node.dart'; // ------------------ AddNodeSession ------------------ class AddNodeSession { - AddNodeSession({required this.profile, this.directionDegrees = 0}); - NodeProfile profile; + AddNodeSession({this.profile, this.directionDegrees = 0}); + NodeProfile? profile; OperatorProfile? operatorProfile; double directionDegrees; LatLng? target; @@ -18,13 +18,13 @@ class AddNodeSession { class EditNodeSession { EditNodeSession({ required this.originalNode, - required this.profile, + this.profile, required this.directionDegrees, required this.target, }); final OsmNode originalNode; // The original node being edited - NodeProfile profile; + NodeProfile? profile; OperatorProfile? operatorProfile; double directionDegrees; LatLng target; // Current position (can be dragged) @@ -39,11 +39,8 @@ class SessionState extends ChangeNotifier { EditNodeSession? get editSession => _editSession; void startAddSession(List enabledProfiles) { - final submittableProfiles = enabledProfiles.where((p) => p.isSubmittable).toList(); - final defaultProfile = submittableProfiles.isNotEmpty - ? submittableProfiles.first - : enabledProfiles.first; // Fallback to any enabled profile - _session = AddNodeSession(profile: defaultProfile); + // Start with no profile selected - force user to choose + _session = AddNodeSession(); _editSession = null; // Clear any edit session notifyListeners(); } @@ -52,11 +49,9 @@ class SessionState extends ChangeNotifier { final submittableProfiles = enabledProfiles.where((p) => p.isSubmittable).toList(); // Try to find a matching profile based on the node's tags - NodeProfile matchingProfile = submittableProfiles.isNotEmpty - ? submittableProfiles.first - : enabledProfiles.first; + NodeProfile? matchingProfile; - // Attempt to find a better match by comparing tags + // Attempt to find a match by comparing tags for (final profile in submittableProfiles) { if (_profileMatchesTags(profile, node.tags)) { matchingProfile = profile; @@ -64,6 +59,7 @@ class SessionState extends ChangeNotifier { } } + // Start with no profile selected if no match found - force user to choose _editSession = EditNodeSession( originalNode: node, profile: matchingProfile, @@ -151,7 +147,7 @@ class SessionState extends ChangeNotifier { } AddNodeSession? commitSession() { - if (_session?.target == null) return null; + if (_session?.target == null || _session?.profile == null) return null; final session = _session!; _session = null; @@ -160,7 +156,7 @@ class SessionState extends ChangeNotifier { } EditNodeSession? commitEditSession() { - if (_editSession == null) return null; + if (_editSession?.profile == null) return null; final session = _editSession!; _editSession = null; diff --git a/lib/state/upload_queue_state.dart b/lib/state/upload_queue_state.dart index 704164c..32f1aed 100644 --- a/lib/state/upload_queue_state.dart +++ b/lib/state/upload_queue_state.dart @@ -30,7 +30,7 @@ class UploadQueueState extends ChangeNotifier { final upload = PendingUpload( coord: session.target!, direction: session.directionDegrees, - profile: session.profile, + profile: session.profile!, // Safe to use ! because commitSession() checks for null operatorProfile: session.operatorProfile, uploadMode: uploadMode, operation: UploadOperation.create, @@ -64,7 +64,7 @@ class UploadQueueState extends ChangeNotifier { final upload = PendingUpload( coord: session.target, direction: session.directionDegrees, - profile: session.profile, + profile: session.profile!, // Safe to use ! because commitEditSession() checks for null operatorProfile: session.operatorProfile, uploadMode: uploadMode, operation: UploadOperation.modify, diff --git a/lib/widgets/add_node_sheet.dart b/lib/widgets/add_node_sheet.dart index f9eae80..2a992e6 100644 --- a/lib/widgets/add_node_sheet.dart +++ b/lib/widgets/add_node_sheet.dart @@ -34,7 +34,10 @@ class AddNodeSheet extends StatelessWidget { } final submittableProfiles = appState.enabledProfiles.where((p) => p.isSubmittable).toList(); - final allowSubmit = appState.isLoggedIn && submittableProfiles.isNotEmpty && session.profile.isSubmittable; + final allowSubmit = appState.isLoggedIn && + submittableProfiles.isNotEmpty && + session.profile != null && + session.profile!.isSubmittable; void _openRefineTags() async { final result = await Navigator.push( @@ -66,13 +69,13 @@ class AddNodeSheet extends StatelessWidget { const SizedBox(height: 16), ListTile( title: Text(locService.t('addNode.profile')), - trailing: DropdownButton( + trailing: DropdownButton( value: session.profile, + hint: Text(locService.t('addNode.selectProfile')), items: submittableProfiles .map((p) => DropdownMenuItem(value: p, child: Text(p.name))) .toList(), - onChanged: (p) => - appState.updateSession(profile: p ?? session.profile), + onChanged: (p) => appState.updateSession(profile: p), ), ), ListTile( @@ -83,12 +86,12 @@ class AddNodeSheet extends StatelessWidget { divisions: 359, value: session.directionDegrees, label: session.directionDegrees.round().toString(), - onChanged: session.profile.requiresDirection + onChanged: (session.profile != null && session.profile!.requiresDirection) ? (v) => appState.updateSession(directionDeg: v) - : null, // Disables slider when requiresDirection is false + : null, // Disabled when no profile selected or profile doesn't require direction ), ), - if (!session.profile.requiresDirection) + if (session.profile != null && !session.profile!.requiresDirection) Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 12), child: Row( @@ -136,7 +139,23 @@ class AddNodeSheet extends StatelessWidget { ], ), ) - else if (!session.profile.isSubmittable) + else if (session.profile == null) + Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 12), + child: Row( + children: [ + const Icon(Icons.info_outline, color: Colors.orange, size: 20), + const SizedBox(width: 6), + Expanded( + child: Text( + locService.t('addNode.profileRequired'), + style: const TextStyle(color: Colors.orange, fontSize: 13), + ), + ), + ], + ), + ) + else if (!session.profile!.isSubmittable) Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 12), child: Row( @@ -158,7 +177,7 @@ class AddNodeSheet extends StatelessWidget { child: SizedBox( width: double.infinity, child: OutlinedButton.icon( - onPressed: _openRefineTags, + onPressed: session.profile != null ? _openRefineTags : null, // Disabled when no profile selected icon: const Icon(Icons.tune), label: Text(session.operatorProfile != null ? locService.t('addNode.refineTagsWithProfile', params: [session.operatorProfile!.name]) diff --git a/lib/widgets/edit_node_sheet.dart b/lib/widgets/edit_node_sheet.dart index 132e732..4a3290a 100644 --- a/lib/widgets/edit_node_sheet.dart +++ b/lib/widgets/edit_node_sheet.dart @@ -36,7 +36,10 @@ class EditNodeSheet extends StatelessWidget { final submittableProfiles = appState.enabledProfiles.where((p) => p.isSubmittable).toList(); final isSandboxMode = appState.uploadMode == UploadMode.sandbox; - final allowSubmit = appState.isLoggedIn && submittableProfiles.isNotEmpty && session.profile.isSubmittable; + final allowSubmit = appState.isLoggedIn && + submittableProfiles.isNotEmpty && + session.profile != null && + session.profile!.isSubmittable; void _openRefineTags() async { final result = await Navigator.push( @@ -73,13 +76,13 @@ class EditNodeSheet extends StatelessWidget { const SizedBox(height: 16), ListTile( title: Text(locService.t('editNode.profile')), - trailing: DropdownButton( + trailing: DropdownButton( value: session.profile, + hint: Text(locService.t('editNode.selectProfile')), items: submittableProfiles .map((p) => DropdownMenuItem(value: p, child: Text(p.name))) .toList(), - onChanged: (p) => - appState.updateEditSession(profile: p ?? session.profile), + onChanged: (p) => appState.updateEditSession(profile: p), ), ), ListTile( @@ -90,12 +93,12 @@ class EditNodeSheet extends StatelessWidget { divisions: 359, value: session.directionDegrees, label: session.directionDegrees.round().toString(), - onChanged: session.profile.requiresDirection + onChanged: (session.profile != null && session.profile!.requiresDirection) ? (v) => appState.updateEditSession(directionDeg: v) - : null, // Disables slider when requiresDirection is false + : null, // Disabled when no profile selected or profile doesn't require direction ), ), - if (!session.profile.requiresDirection) + if (session.profile != null && !session.profile!.requiresDirection) Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 12), child: Row( @@ -143,7 +146,23 @@ class EditNodeSheet extends StatelessWidget { ], ), ) - else if (!session.profile.isSubmittable) + else if (session.profile == null) + Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 12), + child: Row( + children: [ + const Icon(Icons.info_outline, color: Colors.orange, size: 20), + const SizedBox(width: 6), + Expanded( + child: Text( + locService.t('editNode.profileRequired'), + style: const TextStyle(color: Colors.orange, fontSize: 13), + ), + ), + ], + ), + ) + else if (!session.profile!.isSubmittable) Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 12), child: Row( @@ -165,7 +184,7 @@ class EditNodeSheet extends StatelessWidget { child: SizedBox( width: double.infinity, child: OutlinedButton.icon( - onPressed: _openRefineTags, + onPressed: session.profile != null ? _openRefineTags : null, // Disabled when no profile selected icon: const Icon(Icons.tune), label: Text(session.operatorProfile != null ? locService.t('editNode.refineTagsWithProfile', params: [session.operatorProfile!.name]) diff --git a/lib/widgets/map/direction_cones.dart b/lib/widgets/map/direction_cones.dart index 2c58424..8b610cf 100644 --- a/lib/widgets/map/direction_cones.dart +++ b/lib/widgets/map/direction_cones.dart @@ -19,7 +19,7 @@ class DirectionConesBuilder { final overlays = []; // Add session cone if in add-camera mode and profile requires direction - if (session != null && session.target != null && session.profile.requiresDirection) { + if (session != null && session.target != null && session.profile?.requiresDirection == true) { overlays.add(_buildCone( session.target!, session.directionDegrees, @@ -30,7 +30,7 @@ class DirectionConesBuilder { } // Add edit session cone if in edit-camera mode and profile requires direction - if (editSession != null && editSession.profile.requiresDirection) { + if (editSession != null && editSession.profile?.requiresDirection == true) { overlays.add(_buildCone( editSession.target, editSession.directionDegrees,