require profile selection

This commit is contained in:
stopflock
2025-10-24 13:49:48 -05:00
parent c7f4164f12
commit 2a7004e5a2
13 changed files with 103 additions and 41 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -89,6 +89,8 @@
},
"addNode": {
"profile": "配置文件",
"selectProfile": "选择配置文件...",
"profileRequired": "请选择配置文件以继续。",
"direction": "方向 {}°",
"profileNoDirectionInfo": "此配置文件不需要方向。",
"mustBeLoggedIn": "您必须登录才能提交新节点。请通过设置登录。",
@@ -100,6 +102,8 @@
"editNode": {
"title": "编辑节点 #{}",
"profile": "配置文件",
"selectProfile": "选择配置文件...",
"profileRequired": "请选择配置文件以继续。",
"direction": "方向 {}°",
"profileNoDirectionInfo": "此配置文件不需要方向。",
"mustBeLoggedIn": "您必须登录才能编辑节点。请通过设置登录。",

View File

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

View File

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

View File

@@ -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<OperatorProfile?>(
@@ -66,13 +69,13 @@ class AddNodeSheet extends StatelessWidget {
const SizedBox(height: 16),
ListTile(
title: Text(locService.t('addNode.profile')),
trailing: DropdownButton<NodeProfile>(
trailing: DropdownButton<NodeProfile?>(
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])

View File

@@ -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<OperatorProfile?>(
@@ -73,13 +76,13 @@ class EditNodeSheet extends StatelessWidget {
const SizedBox(height: 16),
ListTile(
title: Text(locService.t('editNode.profile')),
trailing: DropdownButton<NodeProfile>(
trailing: DropdownButton<NodeProfile?>(
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])

View File

@@ -19,7 +19,7 @@ class DirectionConesBuilder {
final overlays = <Polygon>[];
// 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,