Compare commits

..
Author SHA1 Message Date
stopflock 999a918062 doesnt really work, probably abandon, saving because sunk cost 2025-11-21 12:49:35 -06:00
41 changed files with 585 additions and 536 deletions

No files matched your search

+5 -2
View File
@@ -98,21 +98,24 @@ cp lib/keys.dart.example lib/keys.dart
## Roadmap ## Roadmap
### Needed Bugfixes ### Needed Bugfixes
- Update node cache to reflect cleared queue entries
- Decide what to do for extracting nodes attached to a way/relation: - Decide what to do for extracting nodes attached to a way/relation:
- Auto extract (how?) - Auto extract (how?)
- Leave it alone (wrong answer unless user chooses intentionally) - Leave it alone (wrong answer unless user chooses intentionally)
- Manual cleanup (cognitive load for users) - Manual cleanup (cognitive load for users)
- Delete the old one (also wrong answer unless user chooses intentionally) - Delete the old one (also wrong answer unless user chooses intentionally)
- Give multiple of these options?? - Give multiple of these options??
- Two nodes too close together warning
- Nav start+end too close together error (warning + disable submit button?) - Nav start+end too close together error (warning + disable submit button?)
- Improve/retune tile fetching backoff/retry - Improve/retune tile fetching backoff/retry
- Disable deletes on nodes belonging to ways/relations
- Support FOV range notation: 0-360, 90-270, 10-45;90-125
- Add some builtin satellite tile provider - Add some builtin satellite tile provider
- Link to "my changes" on osm (username edit history)
- Are offline areas preferred for fast loading even when online? Check working. - Are offline areas preferred for fast loading even when online? Check working.
- Fix network indicator - only done when fetch queue is empty! - Fix network indicator - only done when fetch queue is empty!
### Current Development ### Current Development
- Persistent cache for MY submissions: assume submissions worked, cache,clean up when we see that node appear in overpass/OSM results or when older than 24h - Persistent cache for MY submissions: clean up when we see that node appear in overpass/OSM results or when older than 24h
- Dropdown on "refine tags" page to select acceptable options for camera:mount= (is this a boolean property of a profile?) - Dropdown on "refine tags" page to select acceptable options for camera:mount= (is this a boolean property of a profile?)
- Tutorial / info guide before submitting first node, info and links before creating first profile - Tutorial / info guide before submitting first node, info and links before creating first profile
- Option to pull in profiles from NSI (man_made=surveillance only?) - Option to pull in profiles from NSI (man_made=surveillance only?)
+26 -10
View File
@@ -1,24 +1,30 @@
{ {
"1.4.3": {
"content": [
"• NEW: Proximity warning when placing nodes too close together - prevents accidental duplicate submissions"
]
},
"1.4.2": { "1.4.2": {
"content": [ "content": [
"• NEW: Dedicated 'Upload Queue' page - queue items are now shown in a proper list view instead of a popup", "• NEW: Dedicated 'Upload Queue' page - queue items are now shown in a proper list view instead of a popup",
"• NEW: 'Clear Upload Queue' button is always visible at the top of queue page, greyed out when empty",
"• NEW: 'OpenStreetMap Account' page for managing OSM login and account settings", "• NEW: 'OpenStreetMap Account' page for managing OSM login and account settings",
"• NEW: 'View My Edits on OSM' button takes you directly to your edit history on OpenStreetMap" "• NEW: 'View My Edits on OSM' button takes you directly to your edit history on OpenStreetMap",
"• IMPROVED: Settings page organization with dedicated pages for upload management and OSM account",
"• IMPROVED: Better empty queue state with helpful messaging",
"• UX: Cleaner settings page layout with auth and queue sections moved to their own dedicated pages",
"• UX: Added informational content about OpenStreetMap on the account page"
] ]
}, },
"1.4.1": { "1.4.1": {
"content": [ "content": [
"• NEW: 'Extract node from way/relation' option for constrained nodes (currently disabled while we decide what that means)" "• NEW: 'Extract node from way/relation' option for constrained nodes",
"• When editing nodes that are part of ways or relations, you can now check 'Extract node from way' to create a new node with the same tags at a new location",
"• This preserves the original node in its way/relation while creating an independent copy that can be moved freely",
"• Useful for cases where surveillance equipment has been relocated but the original node must remain for mapping accuracy",
"• Extraction creates a separate OSM changeset and node, leaving the original node untouched"
] ]
}, },
"1.4.0": { "1.4.0": {
"content": [ "content": [
"• IMPROVED: Advanced editing options now only show apps available on your platform (iOS/Android)", "• IMPROVED: Advanced editing options now only show apps available on your platform (iOS/Android)",
"• IMPROVED: When an OSM editor app isn't installed, automatically redirect to the appropriate app store",
"• IMPROVED: Better error handling for external editor launches with app store fallback",
"• Supported editors: Vespucci (Android), StreetComplete (Android), EveryDoor (both), Go Map!! (iOS)", "• Supported editors: Vespucci (Android), StreetComplete (Android), EveryDoor (both), Go Map!! (iOS)",
"• Web editors (iD, RapiD) remain available on all platforms as before" "• Web editors (iD, RapiD) remain available on all platforms as before"
] ]
@@ -27,17 +33,27 @@
"content": [ "content": [
"• NEW: 'Pause Upload Queue' toggle in Offline Settings - stops uploads while keeping live data access", "• NEW: 'Pause Upload Queue' toggle in Offline Settings - stops uploads while keeping live data access",
"• Useful for metered connections or when you want to batch uploads later", "• Useful for metered connections or when you want to batch uploads later",
"• FIXED: Sheets now resize when rotating between orientations" "• Upload queue is now disabled if either full offline mode OR pause queue processing is enabled",
"• FIXED: Sheet buttons now remain visible when rotating from portrait to landscape mode",
"• FIXED: Sheets now properly resize when rotating between orientations without requiring user interaction",
"• IMPROVED: Tag list height adapts automatically for landscape orientation to prevent covering map",
"• IMPROVED: Sheets with few tags now shrink to appropriate size rather than maintaining fixed height",
"• IMPROVED: More reliable sheet layout using proper flexible height constraints",
"• CLEANED: Fixed minor code formatting inconsistencies"
] ]
}, },
"1.3.3": { "1.3.3": {
"content": [ "content": [
"• UX: Edits re-enabled. Only nodes which are part of ways/relations cannot be moved",
"• NEW: Added builtin surveillance device profiles for Rekor and Axis Communications ALPR cameras", "• NEW: Added builtin surveillance device profiles for Rekor and Axis Communications ALPR cameras",
"• Both profiles include proper OSM tags for manufacturer identification and require direction setting",
"• NEW: Advanced editing options - access iD Editor, RapiD, Vespucci, StreetComplete, and other OSM editors", "• NEW: Advanced editing options - access iD Editor, RapiD, Vespucci, StreetComplete, and other OSM editors",
"• NEW: 'View on OSM' links to see nodes directly on OpenStreetMap website", "• NEW: 'View on OSM' links to see nodes directly on OpenStreetMap website",
"• UX: Constrained nodes (part of ways/relations) cannot be moved to prevent data corruption",
"• UX: Auto-clickable URLs in all tag values - any URL becomes a tappable link", "• UX: Auto-clickable URLs in all tag values - any URL becomes a tappable link",
"• UX: Tag lists now scroll with max height to keep buttons and map visible" "• UX: Tag lists now scroll with max height to keep buttons and map visible",
"• UX: Improved button layout on mobile with two rows for better accessibility",
"• UX: Localized network status messages in all supported languages",
"• FIXED: Duplicate changelog service calls eliminated"
] ]
}, },
"1.3.2": { "1.3.2": {
-2
View File
@@ -17,8 +17,6 @@ import 'services/changelog_service.dart';
import 'services/operator_profile_service.dart'; import 'services/operator_profile_service.dart';
import 'services/profile_service.dart'; import 'services/profile_service.dart';
import 'widgets/camera_provider_with_cache.dart'; import 'widgets/camera_provider_with_cache.dart';
import 'widgets/proximity_warning_dialog.dart';
import 'dev_config.dart';
import 'state/auth_state.dart'; import 'state/auth_state.dart';
import 'state/navigation_state.dart'; import 'state/navigation_state.dart';
import 'state/operator_profile_state.dart'; import 'state/operator_profile_state.dart';
+116 -49
View File
@@ -2,36 +2,125 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
/// Developer/build-time configuration for global/non-user-tunable constants. /// Developer/build-time configuration for global/non-user-tunable constants.
/// Single source of truth with typed maps for settings auto-generation.
// Fallback tile storage estimate (KB per tile), used when no preview tile data is available // Typed configuration maps - single definition of each constant
const double kFallbackTileEstimateKb = 25.0; const Map<String, bool> _boolConfig = {
'kEnableDevelopmentModes': true,
'kEnableNodeEdits': true,
'kEnableNodeExtraction': false,
};
// Preview tile coordinates for tile provider previews and size estimates const Map<String, int> _intConfig = {
const int kPreviewTileZoom = 18; 'kPreviewTileZoom': 18,
const int kPreviewTileY = 101300; 'kPreviewTileY': 101300,
const int kPreviewTileX = 41904; 'kPreviewTileX': 41904,
'kNodeMinZoomLevel': 10,
'kOsmApiMinZoomLevel': 13,
'kPreFetchZoomLevel': 10,
'kMaxPreFetchSplitDepth': 3,
'kDataRefreshIntervalSeconds': 60,
'kProximityAlertDefaultDistance': 400,
'kProximityAlertMinDistance': 50,
'kProximityAlertMaxDistance': 1600,
'kTileFetchMaxAttempts': 16,
'kTileFetchInitialDelayMs': 500,
'kTileFetchMaxDelayMs': 10000,
'kTileFetchRandomJitterMs': 250,
'kMaxUserDownloadZoomSpan': 7,
'kMaxReasonableTileCount': 20000,
'kAbsoluteMaxTileCount': 50000,
'kAbsoluteMaxZoom': 23,
};
// Direction cone for map view const Map<String, double> _doubleConfig = {
const double kDirectionConeHalfAngle = 35.0; // degrees 'kFallbackTileEstimateKb': 25.0,
const double kDirectionConeBaseLength = 5; // multiplier 'kDirectionConeHalfAngle': 35.0,
const Color kDirectionConeColor = Color(0xD0767474); // FOV cone color 'kDirectionConeBaseLength': 5.0,
const double kDirectionConeOpacity = 0.5; // Fill opacity for FOV cones 'kDirectionConeOpacity': 0.5,
// Base values for thickness - use helper functions below for pixel-ratio scaling '_kDirectionConeBorderWidthBase': 1.6,
const double _kDirectionConeBorderWidthBase = 1.6; 'kBottomButtonBarOffset': 4.0,
'kButtonBarHeight': 60.0,
'kAttributionSpacingAboveButtonBar': 10.0,
'kZoomIndicatorSpacingAboveButtonBar': 40.0,
'kScaleBarSpacingAboveButtonBar': 70.0,
'kZoomControlsSpacingAboveButtonBar': 20.0,
'kPreFetchAreaExpansionMultiplier': 3.0,
'kMinSpeedForRotationMps': 1.0,
'kMaxTagListHeightRatioPortrait': 0.3,
'kMaxTagListHeightRatioLandscape': 0.2,
'kNodeDoubleTapZoomDelta': 1.0,
'kScrollWheelVelocity': 0.01,
'kPinchZoomThreshold': 0.2,
'kPinchMoveThreshold': 30.0,
'kRotationThreshold': 6.0,
'kNodeIconDiameter': 18.0,
'_kNodeRingThicknessBase': 2.5,
'kNodeDotOpacity': 0.3,
'kDirectionButtonMinWidth': 22.0,
'kDirectionButtonMinHeight': 32.0,
'kTileFetchBackoffMultiplier': 1.5,
};
// Bottom button bar positioning const Map<String, String> _stringConfig = {
const double kBottomButtonBarOffset = 4.0; // Distance from screen bottom (above safe area) 'kClientName': 'DeFlock', // Read-only in settings
const double kButtonBarHeight = 60.0; // Button height (48) + padding (12) 'kSuspectedLocationsCsvUrl': 'https://stopflock.com/app/flock_utilities_mini_latest.csv',
};
// Map overlay spacing relative to button bar top const Map<String, Color> _colorConfig = {
const double kAttributionSpacingAboveButtonBar = 10.0; // Attribution above button bar top 'kDirectionConeColor': Color(0xD0767474),
const double kZoomIndicatorSpacingAboveButtonBar = 40.0; // Zoom indicator above button bar top 'kNodeRingColorReal': Color(0xFF3036F0),
const double kScaleBarSpacingAboveButtonBar = 70.0; // Scale bar above button bar top 'kNodeRingColorMock': Color(0xD0FFFFFF),
const double kZoomControlsSpacingAboveButtonBar = 20.0; // Zoom controls above button bar top 'kNodeRingColorPending': Color(0xD09C27B0),
'kNodeRingColorEditing': Color(0xD0FF9800),
'kNodeRingColorPendingEdit': Color(0xD0757575),
'kNodeRingColorPendingDeletion': Color(0xC0F44336),
};
const Map<String, Duration> _durationConfig = {
'kMarkerTapTimeout': Duration(milliseconds: 250),
'kDebounceCameraRefresh': Duration(milliseconds: 500),
'kFollowMeAnimationDuration': Duration(milliseconds: 600),
'kProximityAlertCooldown': Duration(minutes: 10),
};
// Dynamic accessor class
class _DevConfig {
@override
dynamic noSuchMethod(Invocation invocation) {
final name = invocation.memberName.toString().replaceAll('Symbol("', '').replaceAll('")', '');
// Check each typed map
if (_boolConfig.containsKey(name)) return _boolConfig[name];
if (_intConfig.containsKey(name)) return _intConfig[name];
if (_doubleConfig.containsKey(name)) return _doubleConfig[name];
if (_stringConfig.containsKey(name)) return _stringConfig[name];
if (_colorConfig.containsKey(name)) return _colorConfig[name];
if (_durationConfig.containsKey(name)) return _durationConfig[name];
throw NoSuchMethodError.withInvocation(this, invocation);
}
}
// Global accessor
final dynamic dev = _DevConfig();
// For settings page - combine all maps
Map<String, dynamic> get devConfigForSettings => {
..._boolConfig,
..._intConfig,
..._doubleConfig,
..._stringConfig,
..._colorConfig,
..._durationConfig,
};
// Computed constants
bool get kEnableNavigationFeatures => dev.kEnableDevelopmentModes;
// Helper to calculate bottom position relative to button bar // Helper to calculate bottom position relative to button bar
double bottomPositionFromButtonBar(double spacingAboveButtonBar, double safeAreaBottom) { double bottomPositionFromButtonBar(double spacingAboveButtonBar, double safeAreaBottom) {
return safeAreaBottom + kBottomButtonBarOffset + kButtonBarHeight + spacingAboveButtonBar; return safeAreaBottom + dev.kBottomButtonBarOffset + dev.kButtonBarHeight + spacingAboveButtonBar;
} }
// Helper to get left positioning that accounts for safe area (for landscape mode) // Helper to get left positioning that accounts for safe area (for landscape mode)
@@ -49,28 +138,9 @@ double topPositionWithSafeArea(double baseTop, EdgeInsets safeArea) {
return baseTop + safeArea.top; return baseTop + safeArea.top;
} }
// Client name for OSM uploads ("created_by" tag)
const String kClientName = 'DeFlock';
// Note: Version is now dynamically retrieved from VersionService
// Suspected locations CSV URL
const String kSuspectedLocationsCsvUrl = 'https://stopflock.com/app/flock_utilities_mini_latest.csv';
// Development/testing features - set to false for production builds
const bool kEnableDevelopmentModes = false; // Set to false to hide sandbox/simulate modes and force production mode
// Navigation features - set to false to hide navigation UI elements while in development
const bool kEnableNavigationFeatures = kEnableDevelopmentModes; // Hide navigation until fully implemented
// Node editing features - set to false to temporarily disable editing
const bool kEnableNodeEdits = true; // Set to false to temporarily disable node editing
// Node extraction features - set to false to hide extract functionality for constrained nodes
const bool kEnableNodeExtraction = false; // Set to true to enable extract from way/relation feature (WIP)
/// Navigation availability: only dev builds, and only when online /// Navigation availability: only dev builds, and only when online
bool enableNavigationFeatures({required bool offlineMode}) { bool enableNavigationFeatures({required bool offlineMode}) {
if (!kEnableDevelopmentModes) { if (!dev.kEnableDevelopmentModes) {
return false; // Release builds: never allow navigation return false; // Release builds: never allow navigation
} else { } else {
return !offlineMode; // Dev builds: only when online return !offlineMode; // Dev builds: only when online
@@ -112,9 +182,6 @@ const int kProximityAlertMinDistance = 50; // meters
const int kProximityAlertMaxDistance = 1600; // meters const int kProximityAlertMaxDistance = 1600; // meters
const Duration kProximityAlertCooldown = Duration(minutes: 10); // Cooldown between alerts for same node const Duration kProximityAlertCooldown = Duration(minutes: 10); // Cooldown between alerts for same node
// Node proximity warning configuration (for new/edited nodes that are too close to existing ones)
const double kNodeProximityWarningDistance = 15.0; // meters - distance threshold to show warning
// Map interaction configuration // Map interaction configuration
const double kNodeDoubleTapZoomDelta = 1.0; // How much to zoom in when double-tapping nodes (was 1.0) const double kNodeDoubleTapZoomDelta = 1.0; // How much to zoom in when double-tapping nodes (was 1.0)
const double kScrollWheelVelocity = 0.01; // Mouse scroll wheel zoom speed (default 0.005) const double kScrollWheelVelocity = 0.01; // Mouse scroll wheel zoom speed (default 0.005)
@@ -154,11 +221,11 @@ const double kDirectionButtonMinHeight = 32.0;
// Helper functions for pixel-ratio scaling // Helper functions for pixel-ratio scaling
double getDirectionConeBorderWidth(BuildContext context) { double getDirectionConeBorderWidth(BuildContext context) {
// return _kDirectionConeBorderWidthBase * MediaQuery.of(context).devicePixelRatio; // return dev._kDirectionConeBorderWidthBase * MediaQuery.of(context).devicePixelRatio;
return _kDirectionConeBorderWidthBase; return dev._kDirectionConeBorderWidthBase;
} }
double getNodeRingThickness(BuildContext context) { double getNodeRingThickness(BuildContext context) {
// return _kNodeRingThicknessBase * MediaQuery.of(context).devicePixelRatio; // return dev._kNodeRingThicknessBase * MediaQuery.of(context).devicePixelRatio;
return _kNodeRingThicknessBase; return dev._kNodeRingThicknessBase;
} }
-18
View File
@@ -21,24 +21,6 @@
"advanced": "Erweitert", "advanced": "Erweitert",
"useAdvancedEditor": "Erweiterten Editor verwenden" "useAdvancedEditor": "Erweiterten Editor verwenden"
}, },
"proximityWarning": {
"title": "Knoten sehr nah an vorhandenem Gerät",
"message": "Dieser Knoten ist nur {} Meter von einem vorhandenen Überwachungsgerät entfernt.",
"suggestion": "Wenn mehrere Geräte am selben Mast sind, verwenden Sie bitte mehrere Richtungen auf einem einzigen Knoten, anstatt separate Knoten zu erstellen.",
"nearbyNodes": "Nahegelegene Gerät(e) gefunden ({}):",
"nodeInfo": "Knoten #{} - {}",
"andMore": "...und {} weitere",
"goBack": "Zurück",
"submitAnyway": "Trotzdem senden",
"nodeType": {
"alpr": "ALPR/ANPR Kamera",
"publicCamera": "Öffentliche Überwachungskamera",
"camera": "Überwachungskamera",
"amenity": "{}",
"device": "{} Gerät",
"unknown": "Unbekanntes Gerät"
}
},
"followMe": { "followMe": {
"off": "Verfolgung aktivieren", "off": "Verfolgung aktivieren",
"follow": "Verfolgung aktivieren (Rotation)", "follow": "Verfolgung aktivieren (Rotation)",
-18
View File
@@ -39,24 +39,6 @@
"advanced": "Advanced", "advanced": "Advanced",
"useAdvancedEditor": "Use Advanced Editor" "useAdvancedEditor": "Use Advanced Editor"
}, },
"proximityWarning": {
"title": "Node Very Close to Existing Device",
"message": "This node is only {} meters from an existing surveillance device.",
"suggestion": "If multiple devices are on the same pole, please use multiple directions on a single node instead of creating separate nodes.",
"nearbyNodes": "Nearby device(s) found ({}):",
"nodeInfo": "Node #{} - {}",
"andMore": "...and {} more",
"goBack": "Go Back",
"submitAnyway": "Submit Anyway",
"nodeType": {
"alpr": "ALPR/ANPR Camera",
"publicCamera": "Public Surveillance Camera",
"camera": "Surveillance Camera",
"amenity": "{}",
"device": "{} Device",
"unknown": "Unknown Device"
}
},
"followMe": { "followMe": {
"off": "Enable follow-me", "off": "Enable follow-me",
"follow": "Enable follow-me (rotating)", "follow": "Enable follow-me (rotating)",
-18
View File
@@ -39,24 +39,6 @@
"advanced": "Avanzado", "advanced": "Avanzado",
"useAdvancedEditor": "Usar Editor Avanzado" "useAdvancedEditor": "Usar Editor Avanzado"
}, },
"proximityWarning": {
"title": "Nodo Muy Cerca de Dispositivo Existente",
"message": "Este nodo está a solo {} metros de un dispositivo de vigilancia existente.",
"suggestion": "Si hay múltiples dispositivos en el mismo poste, use múltiples direcciones en un solo nodo en lugar de crear nodos separados.",
"nearbyNodes": "Dispositivo(s) cercano(s) encontrado(s) ({}):",
"nodeInfo": "Nodo #{} - {}",
"andMore": "...y {} más",
"goBack": "Volver",
"submitAnyway": "Enviar de Todas Formas",
"nodeType": {
"alpr": "Cámara ALPR/ANPR",
"publicCamera": "Cámara de Vigilancia Pública",
"camera": "Cámara de Vigilancia",
"amenity": "{}",
"device": "Dispositivo {}",
"unknown": "Dispositivo Desconocido"
}
},
"followMe": { "followMe": {
"off": "Activar seguimiento", "off": "Activar seguimiento",
"follow": "Activar seguimiento (rotación)", "follow": "Activar seguimiento (rotación)",
-18
View File
@@ -39,24 +39,6 @@
"advanced": "Avancé", "advanced": "Avancé",
"useAdvancedEditor": "Utiliser l'Éditeur Avancé" "useAdvancedEditor": "Utiliser l'Éditeur Avancé"
}, },
"proximityWarning": {
"title": "Nœud Très Proche d'un Dispositif Existant",
"message": "Ce nœud n'est qu'à {} mètres d'un dispositif de surveillance existant.",
"suggestion": "Si plusieurs dispositifs se trouvent sur le même poteau, veuillez utiliser plusieurs directions sur un seul nœud au lieu de créer des nœuds séparés.",
"nearbyNodes": "Dispositif(s) proche(s) trouvé(s) ({}) :",
"nodeInfo": "Nœud #{} - {}",
"andMore": "...et {} de plus",
"goBack": "Retour",
"submitAnyway": "Soumettre Quand Même",
"nodeType": {
"alpr": "Caméra ALPR/ANPR",
"publicCamera": "Caméra de Surveillance Publique",
"camera": "Caméra de Surveillance",
"amenity": "{}",
"device": "Dispositif {}",
"unknown": "Dispositif Inconnu"
}
},
"followMe": { "followMe": {
"off": "Activer le suivi", "off": "Activer le suivi",
"follow": "Activer le suivi (rotation)", "follow": "Activer le suivi (rotation)",
-18
View File
@@ -39,24 +39,6 @@
"advanced": "Avanzato", "advanced": "Avanzato",
"useAdvancedEditor": "Usa Editor Avanzato" "useAdvancedEditor": "Usa Editor Avanzato"
}, },
"proximityWarning": {
"title": "Nodo Molto Vicino a Dispositivo Esistente",
"message": "Questo nodo è a soli {} metri da un dispositivo di sorveglianza esistente.",
"suggestion": "Se ci sono più dispositivi sullo stesso palo, utilizzare più direzioni su un singolo nodo invece di creare nodi separati.",
"nearbyNodes": "Dispositivo/i vicino/i trovato/i ({}):",
"nodeInfo": "Nodo #{} - {}",
"andMore": "...e altri {}",
"goBack": "Torna Indietro",
"submitAnyway": "Invia Comunque",
"nodeType": {
"alpr": "Telecamera ALPR/ANPR",
"publicCamera": "Telecamera di Sorveglianza Pubblica",
"camera": "Telecamera di Sorveglianza",
"amenity": "{}",
"device": "Dispositivo {}",
"unknown": "Dispositivo Sconosciuto"
}
},
"followMe": { "followMe": {
"off": "Attiva seguimi", "off": "Attiva seguimi",
"follow": "Attiva seguimi (rotazione)", "follow": "Attiva seguimi (rotazione)",
-18
View File
@@ -39,24 +39,6 @@
"advanced": "Avançado", "advanced": "Avançado",
"useAdvancedEditor": "Usar Editor Avançado" "useAdvancedEditor": "Usar Editor Avançado"
}, },
"proximityWarning": {
"title": "Nó Muito Próximo de Dispositivo Existente",
"message": "Este nó está a apenas {} metros de um dispositivo de vigilância existente.",
"suggestion": "Se vários dispositivos estão no mesmo poste, use várias direções em um único nó em vez de criar nós separados.",
"nearbyNodes": "Dispositivo(s) próximo(s) encontrado(s) ({}):",
"nodeInfo": "Nó #{} - {}",
"andMore": "...e mais {}",
"goBack": "Voltar",
"submitAnyway": "Enviar Mesmo Assim",
"nodeType": {
"alpr": "Câmera ALPR/ANPR",
"publicCamera": "Câmera de Vigilância Pública",
"camera": "Câmera de Vigilância",
"amenity": "{}",
"device": "Dispositivo {}",
"unknown": "Dispositivo Desconhecido"
}
},
"followMe": { "followMe": {
"off": "Ativar seguir-me", "off": "Ativar seguir-me",
"follow": "Ativar seguir-me (rotação)", "follow": "Ativar seguir-me (rotação)",
-18
View File
@@ -39,24 +39,6 @@
"advanced": "高级", "advanced": "高级",
"useAdvancedEditor": "使用高级编辑器" "useAdvancedEditor": "使用高级编辑器"
}, },
"proximityWarning": {
"title": "节点过于靠近现有设备",
"message": "此节点距离现有监控设备仅 {} 米。",
"suggestion": "如果同一根杆上有多个设备,请在单个节点上使用多个方向,而不是创建单独的节点。",
"nearbyNodes": "发现附近设备 ({}):",
"nodeInfo": "节点 #{} - {}",
"andMore": "...还有 {} 个",
"goBack": "返回",
"submitAnyway": "仍然提交",
"nodeType": {
"alpr": "ALPR/ANPR 摄像头",
"publicCamera": "公共监控摄像头",
"camera": "监控摄像头",
"amenity": "{}",
"device": "{} 设备",
"unknown": "未知设备"
}
},
"followMe": { "followMe": {
"off": "启用跟随模式", "off": "启用跟随模式",
"follow": "启用跟随模式(旋转)", "follow": "启用跟随模式(旋转)",
+2
View File
@@ -8,6 +8,7 @@ import 'screens/profiles_settings_screen.dart';
import 'screens/navigation_settings_screen.dart'; import 'screens/navigation_settings_screen.dart';
import 'screens/offline_settings_screen.dart'; import 'screens/offline_settings_screen.dart';
import 'screens/advanced_settings_screen.dart'; import 'screens/advanced_settings_screen.dart';
import 'screens/developer_settings_screen.dart';
import 'screens/language_settings_screen.dart'; import 'screens/language_settings_screen.dart';
import 'screens/about_screen.dart'; import 'screens/about_screen.dart';
import 'screens/release_notes_screen.dart'; import 'screens/release_notes_screen.dart';
@@ -77,6 +78,7 @@ class DeFlockApp extends StatelessWidget {
'/settings/navigation': (context) => const NavigationSettingsScreen(), '/settings/navigation': (context) => const NavigationSettingsScreen(),
'/settings/offline': (context) => const OfflineSettingsScreen(), '/settings/offline': (context) => const OfflineSettingsScreen(),
'/settings/advanced': (context) => const AdvancedSettingsScreen(), '/settings/advanced': (context) => const AdvancedSettingsScreen(),
'/settings/developer': (context) => const DeveloperSettingsScreen(),
'/settings/language': (context) => const LanguageSettingsScreen(), '/settings/language': (context) => const LanguageSettingsScreen(),
'/settings/about': (context) => const AboutScreen(), '/settings/about': (context) => const AboutScreen(),
'/settings/release-notes': (context) => const ReleaseNotesScreen(), '/settings/release-notes': (context) => const ReleaseNotesScreen(),
+127
View File
@@ -0,0 +1,127 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../dev_config.dart';
class DeveloperSettingsScreen extends StatefulWidget {
const DeveloperSettingsScreen({super.key});
@override
State<DeveloperSettingsScreen> createState() => _DeveloperSettingsScreenState();
}
class _DeveloperSettingsScreenState extends State<DeveloperSettingsScreen> {
final Map<String, TextEditingController> _controllers = {};
final Map<String, dynamic> _overrides = {};
@override
void initState() {
super.initState();
_initializeControllers();
}
@override
void dispose() {
for (final controller in _controllers.values) {
controller.dispose();
}
super.dispose();
}
void _initializeControllers() {
for (final entry in devConfigForSettings.entries) {
if (entry.value is String) {
_controllers[entry.key] = TextEditingController(text: entry.value);
} else if (entry.value is int) {
_controllers[entry.key] = TextEditingController(text: entry.value.toString());
} else if (entry.value is double) {
_controllers[entry.key] = TextEditingController(text: entry.value.toString());
} else if (entry.value is Color) {
final color = entry.value as Color;
final hex = color.value.toRadixString(16).padLeft(8, '0').toUpperCase();
_controllers[entry.key] = TextEditingController(text: hex);
} else if (entry.value is Duration) {
final duration = entry.value as Duration;
_controllers[entry.key] = TextEditingController(text: duration.inMilliseconds.toString());
}
}
}
void _saveAndRestart() {
// For now, just show a dialog - actual restart would require platform channels
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Restart Required'),
content: const Text('Changes saved. Please restart the app to apply new settings.'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('OK'),
),
],
),
);
}
Widget _buildSettingWidget(String key, dynamic defaultValue) {
if (key == 'kClientName') {
// Special read-only case
return ListTile(
title: Text(key),
subtitle: Text(defaultValue.toString()),
trailing: const Text('READ ONLY'),
);
}
if (defaultValue is bool) {
return SwitchListTile(
title: Text(key),
value: _overrides[key] ?? defaultValue,
onChanged: (value) {
setState(() {
_overrides[key] = value;
});
},
);
} else if (defaultValue is int || defaultValue is double || defaultValue is String ||
defaultValue is Color || defaultValue is Duration) {
return ListTile(
title: Text(key),
subtitle: TextField(
controller: _controllers[key],
keyboardType: defaultValue is int || defaultValue is double
? const TextInputType.numberWithOptions(signed: true, decimal: true)
: TextInputType.text,
textInputAction: TextInputAction.done,
onChanged: (value) {
// Store the string value for now - actual parsing would happen on save
_overrides[key] = value;
},
),
);
}
return const SizedBox.shrink();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Developer Settings'),
actions: [
TextButton(
onPressed: _saveAndRestart,
child: const Text('SAVE', style: TextStyle(color: Colors.white)),
),
],
),
body: ListView(
padding: const EdgeInsets.all(16),
children: devConfigForSettings.entries
.map((entry) => _buildSettingWidget(entry.key, entry.value))
.toList(),
),
);
}
}
+2 -2
View File
@@ -713,7 +713,7 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
final safeArea = MediaQuery.of(context).padding; final safeArea = MediaQuery.of(context).padding;
return Padding( return Padding(
padding: EdgeInsets.only( padding: EdgeInsets.only(
bottom: safeArea.bottom + kBottomButtonBarOffset, bottom: safeArea.bottom + dev.kBottomButtonBarOffset,
left: leftPositionWithSafeArea(8, safeArea), left: leftPositionWithSafeArea(8, safeArea),
right: rightPositionWithSafeArea(8, safeArea), right: rightPositionWithSafeArea(8, safeArea),
), ),
@@ -731,7 +731,7 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
) )
], ],
), ),
margin: EdgeInsets.only(bottom: kBottomButtonBarOffset), margin: EdgeInsets.only(bottom: dev.kBottomButtonBarOffset),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
child: Row( child: Row(
children: [ children: [
+1 -1
View File
@@ -117,7 +117,7 @@ class OSMAccountScreen extends StatelessWidget {
const SizedBox(height: 16), const SizedBox(height: 16),
// Upload Mode Section (only show in development builds) // Upload Mode Section (only show in development builds)
if (kEnableDevelopmentModes) ...[ if (dev.kEnableDevelopmentModes) ...[
Card( Card(
child: const Padding( child: const Padding(
padding: EdgeInsets.all(16.0), padding: EdgeInsets.all(16.0),
+40 -7
View File
@@ -1,11 +1,41 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../services/localization_service.dart'; import '../services/localization_service.dart';
import '../services/version_service.dart'; import '../services/version_service.dart';
import '../dev_config.dart'; import '../dev_config.dart';
class SettingsScreen extends StatelessWidget { class SettingsScreen extends StatefulWidget {
const SettingsScreen({super.key}); const SettingsScreen({super.key});
@override
State<SettingsScreen> createState() => _SettingsScreenState();
}
class _SettingsScreenState extends State<SettingsScreen> {
int _versionTapCount = 0;
Timer? _tapTimer;
@override
void dispose() {
_tapTimer?.cancel();
super.dispose();
}
void _onVersionTap() {
_tapTimer?.cancel();
_versionTapCount++;
if (_versionTapCount >= 10) {
Navigator.pushNamed(context, '/settings/developer');
_versionTapCount = 0;
return;
}
_tapTimer = Timer(const Duration(milliseconds: 400), () {
_versionTapCount = 0;
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final locService = LocalizationService.instance; final locService = LocalizationService.instance;
@@ -100,15 +130,18 @@ class SettingsScreen extends StatelessWidget {
), ),
const Divider(), const Divider(),
// Version display // Version display with secret tap counter
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Text( child: GestureDetector(
'Version: ${VersionService().version}', onTap: _onVersionTap,
style: Theme.of(context).textTheme.bodySmall?.copyWith( child: Text(
color: Theme.of(context).textTheme.bodySmall?.color?.withOpacity(0.6), 'Version: ${VersionService().version}',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).textTheme.bodySmall?.color?.withOpacity(0.6),
),
textAlign: TextAlign.center,
), ),
textAlign: TextAlign.center,
), ),
), ),
], ],
+4 -4
View File
@@ -345,7 +345,7 @@ class _TileTypeDialogState extends State<_TileTypeDialog> {
if (value?.trim().isEmpty == true) return locService.t('tileTypeEditor.maxZoomRequired'); if (value?.trim().isEmpty == true) return locService.t('tileTypeEditor.maxZoomRequired');
final zoom = int.tryParse(value!); final zoom = int.tryParse(value!);
if (zoom == null) return locService.t('tileTypeEditor.maxZoomInvalid'); if (zoom == null) return locService.t('tileTypeEditor.maxZoomInvalid');
if (zoom < 1 || zoom > kAbsoluteMaxZoom) return locService.t('tileTypeEditor.maxZoomRange', params: ['1', kAbsoluteMaxZoom.toString()]); if (zoom < 1 || zoom > dev.kAbsoluteMaxZoom) return locService.t('tileTypeEditor.maxZoomRange', params: ['1', kAbsoluteMaxZoom.toString()]);
return null; return null;
}, },
), ),
@@ -405,9 +405,9 @@ class _TileTypeDialogState extends State<_TileTypeDialog> {
try { try {
// Use a sample tile from configured preview location // Use a sample tile from configured preview location
final url = _urlController.text final url = _urlController.text
.replaceAll('{z}', kPreviewTileZoom.toString()) .replaceAll('{z}', dev.kPreviewTileZoom.toString())
.replaceAll('{x}', kPreviewTileX.toString()) .replaceAll('{x}', dev.kPreviewTileX.toString())
.replaceAll('{y}', kPreviewTileY.toString()); .replaceAll('{y}', dev.kPreviewTileY.toString());
final response = await http.get(Uri.parse(url)); final response = await http.get(Uri.parse(url));
@@ -44,7 +44,7 @@ Future<List<OsmNode>> _fetchOverpassNodesWithSplitting({
}) async { }) async {
if (profiles.isEmpty) return []; if (profiles.isEmpty) return [];
const int maxSplitDepth = kMaxPreFetchSplitDepth; // Maximum times we'll split (4^3 = 64 max sub-areas) final int maxSplitDepth = dev.dev.kMaxPreFetchSplitDepth; // Maximum times we'll split (4^3 = 64 max sub-areas)
try { try {
return await _fetchSingleOverpassQuery( return await _fetchSingleOverpassQuery(
@@ -36,14 +36,14 @@ void clearRemoteTileQueueSelective(LatLngBounds currentBounds) {
/// Uses: initialDelay * (multiplier ^ (attempt - 1)) + randomJitter, capped at maxDelay /// Uses: initialDelay * (multiplier ^ (attempt - 1)) + randomJitter, capped at maxDelay
int _calculateRetryDelay(int attempt, Random random) { int _calculateRetryDelay(int attempt, Random random) {
// Calculate exponential backoff: initialDelay * (multiplier ^ (attempt - 1)) // Calculate exponential backoff: initialDelay * (multiplier ^ (attempt - 1))
final baseDelay = (kTileFetchInitialDelayMs * final baseDelay = (dev.kTileFetchInitialDelayMs *
pow(kTileFetchBackoffMultiplier, attempt - 1)).round(); pow(dev.kTileFetchBackoffMultiplier, attempt - 1)).round();
// Add random jitter to avoid thundering herd // Add random jitter to avoid thundering herd
final jitter = random.nextInt(kTileFetchRandomJitterMs + 1); final jitter = random.nextInt(dev.kTileFetchRandomJitterMs + 1);
// Apply max delay cap // Apply max delay cap
return (baseDelay + jitter).clamp(0, kTileFetchMaxDelayMs); return (baseDelay + jitter).clamp(0, dev.kTileFetchMaxDelayMs);
} }
/// Convert tile coordinates to lat/lng bounds for spatial filtering /// Convert tile coordinates to lat/lng bounds for spatial filtering
@@ -101,7 +101,7 @@ Future<List<int>> fetchRemoteTile({
required int y, required int y,
required String url, required String url,
}) async { }) async {
const int maxAttempts = kTileFetchMaxAttempts; final int maxAttempts = dev.dev.kTileFetchMaxAttempts;
int attempt = 0; int attempt = 0;
final random = Random(); final random = Random();
final hostInfo = Uri.parse(url).host; // For logging final hostInfo = Uri.parse(url).host; // For logging
-30
View File
@@ -2,8 +2,6 @@ import 'package:latlong2/latlong.dart';
import '../models/osm_node.dart'; import '../models/osm_node.dart';
import 'package:flutter_map/flutter_map.dart' show LatLngBounds; import 'package:flutter_map/flutter_map.dart' show LatLngBounds;
const Distance _distance = Distance();
class NodeCache { class NodeCache {
// Singleton instance // Singleton instance
static final NodeCache instance = NodeCache._internal(); static final NodeCache instance = NodeCache._internal();
@@ -105,34 +103,6 @@ class NodeCache {
(coord1.longitude - coord2.longitude).abs() < tolerance; (coord1.longitude - coord2.longitude).abs() < tolerance;
} }
/// Find nodes within the specified distance (in meters) of the given coordinate
/// Excludes nodes with the excludeNodeId (useful when checking proximity for edited nodes)
List<OsmNode> findNodesWithinDistance(LatLng coord, double distanceMeters, {int? excludeNodeId}) {
final nearbyNodes = <OsmNode>[];
for (final node in _nodes.values) {
// Skip the excluded node (typically the node being edited)
if (excludeNodeId != null && node.id == excludeNodeId) {
continue;
}
// Skip temporary nodes (negative IDs) with pending upload/edit/deletion markers
if (node.id < 0 && (
node.tags.containsKey('_pending_upload') ||
node.tags.containsKey('_pending_edit') ||
node.tags.containsKey('_pending_deletion'))) {
continue;
}
final distance = _distance.as(LengthUnit.Meter, coord, node.coord);
if (distance <= distanceMeters) {
nearbyNodes.add(node);
}
}
return nearbyNodes;
}
/// Utility: point-in-bounds for coordinates /// Utility: point-in-bounds for coordinates
bool _inBounds(LatLng coord, LatLngBounds bounds) { bool _inBounds(LatLng coord, LatLngBounds bounds) {
return coord.latitude >= bounds.southWest.latitude && return coord.latitude >= bounds.southWest.latitude &&
+4 -4
View File
@@ -30,8 +30,8 @@ class PrefetchAreaService {
Timer? _debounceTimer; Timer? _debounceTimer;
// Configuration from dev_config // Configuration from dev_config
static const double _areaExpansionMultiplier = kPreFetchAreaExpansionMultiplier; static final double _areaExpansionMultiplier = dev.dev.kPreFetchAreaExpansionMultiplier;
static const int _preFetchZoomLevel = kPreFetchZoomLevel; static final int _preFetchZoomLevel = dev.dev.kPreFetchZoomLevel;
/// Check if the given bounds are fully within the current pre-fetched area. /// Check if the given bounds are fully within the current pre-fetched area.
bool isWithinPreFetchedArea(LatLngBounds bounds, List<NodeProfile> profiles, UploadMode uploadMode) { bool isWithinPreFetchedArea(LatLngBounds bounds, List<NodeProfile> profiles, UploadMode uploadMode) {
@@ -58,7 +58,7 @@ class PrefetchAreaService {
/// Check if cached data is stale (older than configured refresh interval). /// Check if cached data is stale (older than configured refresh interval).
bool isDataStale() { bool isDataStale() {
if (_lastFetchTime == null) return true; if (_lastFetchTime == null) return true;
return DateTime.now().difference(_lastFetchTime!).inSeconds > kDataRefreshIntervalSeconds; return DateTime.now().difference(_lastFetchTime!).inSeconds > dev.kDataRefreshIntervalSeconds;
} }
/// Request pre-fetch for the given view bounds if not already covered or if data is stale. /// Request pre-fetch for the given view bounds if not already covered or if data is stale.
@@ -84,7 +84,7 @@ class PrefetchAreaService {
} }
if (isStale) { if (isStale) {
debugPrint('[PrefetchAreaService] Data is stale (>${kDataRefreshIntervalSeconds}s), refreshing'); debugPrint('[PrefetchAreaService] Data is stale (>${dev.kDataRefreshIntervalSeconds}s), refreshing');
} else { } else {
debugPrint('[PrefetchAreaService] Current view outside pre-fetched area, fetching larger area'); debugPrint('[PrefetchAreaService] Current view outside pre-fetched area, fetching larger area');
} }
+1 -1
View File
@@ -28,7 +28,7 @@ class ProximityAlertService {
// Simple in-memory tracking of recent alerts to prevent spam // Simple in-memory tracking of recent alerts to prevent spam
final List<RecentAlert> _recentAlerts = []; final List<RecentAlert> _recentAlerts = [];
static const Duration _alertCooldown = kProximityAlertCooldown; static final Duration _alertCooldown = dev.dev.kProximityAlertCooldown;
// Callback for showing in-app visual alerts // Callback for showing in-app visual alerts
VoidCallback? _onVisualAlert; VoidCallback? _onVisualAlert;
+2 -2
View File
@@ -102,10 +102,10 @@ class SuspectedLocationService {
/// Fetch data from the CSV URL /// Fetch data from the CSV URL
Future<bool> _fetchData() async { Future<bool> _fetchData() async {
try { try {
debugPrint('[SuspectedLocationService] Fetching CSV data from $kSuspectedLocationsCsvUrl'); debugPrint('[SuspectedLocationService] Fetching CSV data from $dev.kSuspectedLocationsCsvUrl');
final response = await http.get( final response = await http.get(
Uri.parse(kSuspectedLocationsCsvUrl), Uri.parse(dev.kSuspectedLocationsCsvUrl),
headers: { headers: {
'User-Agent': 'DeFlock/1.0 (OSM surveillance mapping app)', 'User-Agent': 'DeFlock/1.0 (OSM surveillance mapping app)',
}, },
+1 -1
View File
@@ -61,7 +61,7 @@ class TilePreviewService {
static Future<Uint8List?> _fetchPreviewForTileType(TileType tileType, String? apiKey) async { static Future<Uint8List?> _fetchPreviewForTileType(TileType tileType, String? apiKey) async {
try { try {
final url = tileType.getTileUrl(kPreviewTileZoom, kPreviewTileX, kPreviewTileY, apiKey: apiKey); final url = tileType.getTileUrl(dev.kPreviewTileZoom, dev.kPreviewTileX, dev.kPreviewTileY, apiKey: apiKey);
final response = await http.get(Uri.parse(url)).timeout(_timeout); final response = await http.get(Uri.parse(url)).timeout(_timeout);
+1 -1
View File
@@ -44,7 +44,7 @@ class Uploader {
final csXml = ''' final csXml = '''
<osm> <osm>
<changeset> <changeset>
<tag k="created_by" v="$kClientName ${VersionService().version}"/> <tag k="created_by" v="$dev.kClientName ${VersionService().version}"/>
<tag k="comment" v="$action $profileName surveillance node"/> <tag k="comment" v="$action $profileName surveillance node"/>
</changeset> </changeset>
</osm>'''; </osm>''';
+7 -7
View File
@@ -33,10 +33,10 @@ class SettingsState extends ChangeNotifier {
bool _offlineMode = false; bool _offlineMode = false;
bool _pauseQueueProcessing = false; bool _pauseQueueProcessing = false;
int _maxCameras = 250; int _maxCameras = 250;
UploadMode _uploadMode = kEnableDevelopmentModes ? UploadMode.simulate : UploadMode.production; UploadMode _uploadMode = dev.kEnableDevelopmentModes ? UploadMode.simulate : UploadMode.production;
FollowMeMode _followMeMode = FollowMeMode.follow; FollowMeMode _followMeMode = FollowMeMode.follow;
bool _proximityAlertsEnabled = false; bool _proximityAlertsEnabled = false;
int _proximityAlertDistance = kProximityAlertDefaultDistance; int _proximityAlertDistance = dev.kProximityAlertDefaultDistance;
bool _networkStatusIndicatorEnabled = true; bool _networkStatusIndicatorEnabled = true;
int _suspectedLocationMinDistance = 100; // meters int _suspectedLocationMinDistance = 100; // meters
List<TileProvider> _tileProviders = []; List<TileProvider> _tileProviders = [];
@@ -105,7 +105,7 @@ class SettingsState extends ChangeNotifier {
// Load proximity alerts settings // Load proximity alerts settings
_proximityAlertsEnabled = prefs.getBool(_proximityAlertsEnabledPrefsKey) ?? false; _proximityAlertsEnabled = prefs.getBool(_proximityAlertsEnabledPrefsKey) ?? false;
_proximityAlertDistance = prefs.getInt(_proximityAlertDistancePrefsKey) ?? kProximityAlertDefaultDistance; _proximityAlertDistance = prefs.getInt(_proximityAlertDistancePrefsKey) ?? dev.kProximityAlertDefaultDistance;
// Load network status indicator setting // Load network status indicator setting
_networkStatusIndicatorEnabled = prefs.getBool(_networkStatusIndicatorEnabledPrefsKey) ?? true; _networkStatusIndicatorEnabled = prefs.getBool(_networkStatusIndicatorEnabledPrefsKey) ?? true;
@@ -128,7 +128,7 @@ class SettingsState extends ChangeNotifier {
} }
// In production builds, force production mode if development modes are disabled // In production builds, force production mode if development modes are disabled
if (!kEnableDevelopmentModes && _uploadMode != UploadMode.production) { if (!dev.kEnableDevelopmentModes && _uploadMode != UploadMode.production) {
debugPrint('SettingsState: Development modes disabled, forcing production mode'); debugPrint('SettingsState: Development modes disabled, forcing production mode');
_uploadMode = UploadMode.production; _uploadMode = UploadMode.production;
await prefs.setInt(_uploadModePrefsKey, _uploadMode.index); await prefs.setInt(_uploadModePrefsKey, _uploadMode.index);
@@ -236,7 +236,7 @@ class SettingsState extends ChangeNotifier {
Future<void> setUploadMode(UploadMode mode) async { Future<void> setUploadMode(UploadMode mode) async {
// In production builds, only allow production mode // In production builds, only allow production mode
if (!kEnableDevelopmentModes && mode != UploadMode.production) { if (!dev.kEnableDevelopmentModes && mode != UploadMode.production) {
debugPrint('SettingsState: Development modes disabled, forcing production mode'); debugPrint('SettingsState: Development modes disabled, forcing production mode');
mode = UploadMode.production; mode = UploadMode.production;
} }
@@ -323,8 +323,8 @@ class SettingsState extends ChangeNotifier {
/// Set proximity alert distance in meters /// Set proximity alert distance in meters
Future<void> setProximityAlertDistance(int distance) async { Future<void> setProximityAlertDistance(int distance) async {
if (distance < kProximityAlertMinDistance) distance = kProximityAlertMinDistance; if (distance < dev.kProximityAlertMinDistance) distance = dev.kProximityAlertMinDistance;
if (distance > kProximityAlertMaxDistance) distance = kProximityAlertMaxDistance; if (distance > dev.kProximityAlertMaxDistance) distance = dev.kProximityAlertMaxDistance;
if (_proximityAlertDistance != distance) { if (_proximityAlertDistance != distance) {
_proximityAlertDistance = distance; _proximityAlertDistance = distance;
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
+8 -49
View File
@@ -6,58 +6,13 @@ import '../dev_config.dart';
import '../models/node_profile.dart'; import '../models/node_profile.dart';
import '../models/operator_profile.dart'; import '../models/operator_profile.dart';
import '../services/localization_service.dart'; import '../services/localization_service.dart';
import '../services/node_cache.dart';
import 'refine_tags_sheet.dart'; import 'refine_tags_sheet.dart';
import 'proximity_warning_dialog.dart';
class AddNodeSheet extends StatelessWidget { class AddNodeSheet extends StatelessWidget {
const AddNodeSheet({super.key, required this.session}); const AddNodeSheet({super.key, required this.session});
final AddNodeSession session; final AddNodeSession session;
void _checkProximityAndCommit(BuildContext context, AppState appState, LocalizationService locService) {
// Only check proximity if we have a target location
if (session.target == null) {
_commitWithoutCheck(context, appState, locService);
return;
}
// Check for nearby nodes within the configured distance
final nearbyNodes = NodeCache.instance.findNodesWithinDistance(
session.target!,
kNodeProximityWarningDistance,
);
if (nearbyNodes.isNotEmpty) {
// Show proximity warning dialog
showDialog<void>(
context: context,
builder: (context) => ProximityWarningDialog(
nearbyNodes: nearbyNodes,
distance: kNodeProximityWarningDistance,
onGoBack: () {
Navigator.of(context).pop(); // Close dialog
},
onSubmitAnyway: () {
Navigator.of(context).pop(); // Close dialog
_commitWithoutCheck(context, appState, locService);
},
),
);
} else {
// No nearby nodes, proceed with commit
_commitWithoutCheck(context, appState, locService);
}
}
void _commitWithoutCheck(BuildContext context, AppState appState, LocalizationService locService) {
appState.commitSession();
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(locService.t('node.queuedForUpload'))),
);
}
Widget _buildDirectionControls(BuildContext context, AppState appState, AddNodeSession session, LocalizationService locService) { Widget _buildDirectionControls(BuildContext context, AppState appState, AddNodeSession session, LocalizationService locService) {
final requiresDirection = session.profile != null && session.profile!.requiresDirection; final requiresDirection = session.profile != null && session.profile!.requiresDirection;
@@ -126,7 +81,7 @@ class AddNodeSheet extends StatelessWidget {
: null, : null,
tooltip: requiresDirection ? 'Remove current direction' : 'Direction not required for this profile', tooltip: requiresDirection ? 'Remove current direction' : 'Direction not required for this profile',
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
constraints: const BoxConstraints(minWidth: kDirectionButtonMinWidth, minHeight: kDirectionButtonMinHeight), constraints: BoxConstraints(minWidth: dev.kDirectionButtonMinWidth, minHeight: dev.kDirectionButtonMinHeight),
), ),
// Add button // Add button
IconButton( IconButton(
@@ -140,7 +95,7 @@ class AddNodeSheet extends StatelessWidget {
? (session.directions.length >= 8 ? 'Maximum 8 directions allowed' : 'Add new direction') ? (session.directions.length >= 8 ? 'Maximum 8 directions allowed' : 'Add new direction')
: 'Direction not required for this profile', : 'Direction not required for this profile',
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
constraints: const BoxConstraints(minWidth: kDirectionButtonMinWidth, minHeight: kDirectionButtonMinHeight), constraints: BoxConstraints(minWidth: dev.kDirectionButtonMinWidth, minHeight: dev.kDirectionButtonMinHeight),
), ),
// Cycle button // Cycle button
IconButton( IconButton(
@@ -154,7 +109,7 @@ class AddNodeSheet extends StatelessWidget {
: null, : null,
tooltip: requiresDirection ? 'Cycle through directions' : 'Direction not required for this profile', tooltip: requiresDirection ? 'Cycle through directions' : 'Direction not required for this profile',
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
constraints: const BoxConstraints(minWidth: kDirectionButtonMinWidth, minHeight: kDirectionButtonMinHeight), constraints: BoxConstraints(minWidth: dev.kDirectionButtonMinWidth, minHeight: dev.kDirectionButtonMinHeight),
), ),
], ],
), ),
@@ -189,7 +144,11 @@ class AddNodeSheet extends StatelessWidget {
final appState = context.watch<AppState>(); final appState = context.watch<AppState>();
void _commit() { void _commit() {
_checkProximityAndCommit(context, appState, locService); appState.commitSession();
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(locService.t('node.queuedForUpload'))),
);
} }
void _cancel() { void _cancel() {
+9 -9
View File
@@ -19,28 +19,28 @@ class CameraIcon extends StatelessWidget {
Color get _ringColor { Color get _ringColor {
switch (type) { switch (type) {
case CameraIconType.real: case CameraIconType.real:
return kNodeRingColorReal; return dev.kNodeRingColorReal;
case CameraIconType.mock: case CameraIconType.mock:
return kNodeRingColorMock; return dev.kNodeRingColorMock;
case CameraIconType.pending: case CameraIconType.pending:
return kNodeRingColorPending; return dev.kNodeRingColorPending;
case CameraIconType.editing: case CameraIconType.editing:
return kNodeRingColorEditing; return dev.kNodeRingColorEditing;
case CameraIconType.pendingEdit: case CameraIconType.pendingEdit:
return kNodeRingColorPendingEdit; return dev.kNodeRingColorPendingEdit;
case CameraIconType.pendingDeletion: case CameraIconType.pendingDeletion:
return kNodeRingColorPendingDeletion; return dev.kNodeRingColorPendingDeletion;
} }
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
width: kNodeIconDiameter, width: dev.kNodeIconDiameter,
height: kNodeIconDiameter, height: dev.kNodeIconDiameter,
decoration: BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle, shape: BoxShape.circle,
color: _ringColor.withOpacity(kNodeDotOpacity), color: _ringColor.withOpacity(dev.kNodeDotOpacity),
border: Border.all( border: Border.all(
color: _ringColor, color: _ringColor,
width: getNodeRingThickness(context), width: getNodeRingThickness(context),
+10 -10
View File
@@ -76,14 +76,14 @@ class _DownloadAreaDialogState extends State<DownloadAreaDialog> {
/// Calculate the maximum zoom level that keeps tile count under the absolute limit /// Calculate the maximum zoom level that keeps tile count under the absolute limit
int _calculateMaxZoomForTileLimit(LatLngBounds bounds, int minZoom) { int _calculateMaxZoomForTileLimit(LatLngBounds bounds, int minZoom) {
for (int zoom = minZoom; zoom <= kAbsoluteMaxZoom; zoom++) { for (int zoom = minZoom; zoom <= dev.kAbsoluteMaxZoom; zoom++) {
final tileCount = computeTileList(bounds, minZoom, zoom).length; final tileCount = computeTileList(bounds, minZoom, zoom).length;
if (tileCount > kAbsoluteMaxTileCount) { if (tileCount > dev.kAbsoluteMaxTileCount) {
// Return the previous zoom level that was still under the absolute limit // Return the previous zoom level that was still under the absolute limit
return math.max(minZoom, zoom - 1); return math.max(minZoom, zoom - 1);
} }
} }
return kAbsoluteMaxZoom; return dev.kAbsoluteMaxZoom;
} }
/// Get tile size estimate in KB, using preview tile data if available, otherwise fallback to constant /// Get tile size estimate in KB, using preview tile data if available, otherwise fallback to constant
@@ -98,7 +98,7 @@ class _DownloadAreaDialogState extends State<DownloadAreaDialog> {
return previewSizeKb; return previewSizeKb;
} else { } else {
// Fall back to configured estimate // Fall back to configured estimate
return kFallbackTileEstimateKb; return dev.kFallbackTileEstimateKb;
} }
} }
@@ -176,7 +176,7 @@ class _DownloadAreaDialogState extends State<DownloadAreaDialog> {
child: Container( child: Container(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
decoration: BoxDecoration( decoration: BoxDecoration(
color: _tileCount! > kMaxReasonableTileCount color: _tileCount! > dev.kMaxReasonableTileCount
? Colors.orange.withOpacity(0.1) ? Colors.orange.withOpacity(0.1)
: Colors.green.withOpacity(0.1), : Colors.green.withOpacity(0.1),
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
@@ -185,12 +185,12 @@ class _DownloadAreaDialogState extends State<DownloadAreaDialog> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
_tileCount! > kMaxReasonableTileCount _tileCount! > dev.kMaxReasonableTileCount
? 'Above recommended limit (Z${_maxPossibleZoom})' ? 'Above recommended limit (Z${_maxPossibleZoom})'
: locService.t('download.maxRecommendedZoom', params: [_maxPossibleZoom.toString()]), : locService.t('download.maxRecommendedZoom', params: [_maxPossibleZoom.toString()]),
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: _tileCount! > kMaxReasonableTileCount color: _tileCount! > dev.kMaxReasonableTileCount
? Colors.orange[700] ? Colors.orange[700]
: Colors.green[700], : Colors.green[700],
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
@@ -198,12 +198,12 @@ class _DownloadAreaDialogState extends State<DownloadAreaDialog> {
), ),
const SizedBox(height: 2), const SizedBox(height: 2),
Text( Text(
_tileCount! > kMaxReasonableTileCount _tileCount! > dev.kMaxReasonableTileCount
? 'Current selection exceeds ${kMaxReasonableTileCount} recommended tile limit but is within ${kAbsoluteMaxTileCount} absolute limit' ? 'Current selection exceeds ${dev.kMaxReasonableTileCount} recommended tile limit but is within ${dev.kAbsoluteMaxTileCount} absolute limit'
: locService.t('download.withinTileLimit', params: [kMaxReasonableTileCount.toString()]), : locService.t('download.withinTileLimit', params: [kMaxReasonableTileCount.toString()]),
style: TextStyle( style: TextStyle(
fontSize: 11, fontSize: 11,
color: _tileCount! > kMaxReasonableTileCount color: _tileCount! > dev.kMaxReasonableTileCount
? Colors.orange[600] ? Colors.orange[600]
: Colors.green[600], : Colors.green[600],
), ),
+12 -48
View File
@@ -6,55 +6,15 @@ import '../dev_config.dart';
import '../models/node_profile.dart'; import '../models/node_profile.dart';
import '../models/operator_profile.dart'; import '../models/operator_profile.dart';
import '../services/localization_service.dart'; import '../services/localization_service.dart';
import '../services/node_cache.dart';
import '../state/settings_state.dart'; import '../state/settings_state.dart';
import 'refine_tags_sheet.dart'; import 'refine_tags_sheet.dart';
import 'advanced_edit_options_sheet.dart'; import 'advanced_edit_options_sheet.dart';
import 'proximity_warning_dialog.dart';
class EditNodeSheet extends StatelessWidget { class EditNodeSheet extends StatelessWidget {
const EditNodeSheet({super.key, required this.session}); const EditNodeSheet({super.key, required this.session});
final EditNodeSession session; final EditNodeSession session;
void _checkProximityAndCommit(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,
kNodeProximityWarningDistance,
excludeNodeId: session.originalNode.id,
);
if (nearbyNodes.isNotEmpty) {
// Show proximity warning dialog
showDialog<void>(
context: context,
builder: (context) => ProximityWarningDialog(
nearbyNodes: nearbyNodes,
distance: kNodeProximityWarningDistance,
onGoBack: () {
Navigator.of(context).pop(); // Close dialog
},
onSubmitAnyway: () {
Navigator.of(context).pop(); // Close dialog
_commitWithoutCheck(context, appState, locService);
},
),
);
} else {
// No nearby nodes, proceed with commit
_commitWithoutCheck(context, appState, locService);
}
}
void _commitWithoutCheck(BuildContext context, AppState appState, LocalizationService locService) {
appState.commitEditSession();
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(locService.t('node.editQueuedForUpload'))),
);
}
Widget _buildDirectionControls(BuildContext context, AppState appState, EditNodeSession session, LocalizationService locService) { Widget _buildDirectionControls(BuildContext context, AppState appState, EditNodeSession session, LocalizationService locService) {
final requiresDirection = session.profile != null && session.profile!.requiresDirection; final requiresDirection = session.profile != null && session.profile!.requiresDirection;
@@ -123,7 +83,7 @@ class EditNodeSheet extends StatelessWidget {
: null, : null,
tooltip: requiresDirection ? 'Remove current direction' : 'Direction not required for this profile', tooltip: requiresDirection ? 'Remove current direction' : 'Direction not required for this profile',
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
constraints: const BoxConstraints(minWidth: kDirectionButtonMinWidth, minHeight: kDirectionButtonMinHeight), constraints: BoxConstraints(minWidth: dev.kDirectionButtonMinWidth, minHeight: dev.kDirectionButtonMinHeight),
), ),
// Add button // Add button
IconButton( IconButton(
@@ -137,7 +97,7 @@ class EditNodeSheet extends StatelessWidget {
? (session.directions.length >= 8 ? 'Maximum 8 directions allowed' : 'Add new direction') ? (session.directions.length >= 8 ? 'Maximum 8 directions allowed' : 'Add new direction')
: 'Direction not required for this profile', : 'Direction not required for this profile',
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
constraints: const BoxConstraints(minWidth: kDirectionButtonMinWidth, minHeight: kDirectionButtonMinHeight), constraints: BoxConstraints(minWidth: dev.kDirectionButtonMinWidth, minHeight: dev.kDirectionButtonMinHeight),
), ),
// Cycle button // Cycle button
IconButton( IconButton(
@@ -151,7 +111,7 @@ class EditNodeSheet extends StatelessWidget {
: null, : null,
tooltip: requiresDirection ? 'Cycle through directions' : 'Direction not required for this profile', tooltip: requiresDirection ? 'Cycle through directions' : 'Direction not required for this profile',
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
constraints: const BoxConstraints(minWidth: kDirectionButtonMinWidth, minHeight: kDirectionButtonMinHeight), constraints: BoxConstraints(minWidth: dev.kDirectionButtonMinWidth, minHeight: dev.kDirectionButtonMinHeight),
), ),
], ],
), ),
@@ -186,7 +146,11 @@ class EditNodeSheet extends StatelessWidget {
final appState = context.watch<AppState>(); final appState = context.watch<AppState>();
void _commit() { void _commit() {
_checkProximityAndCommit(context, appState, locService); appState.commitEditSession();
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(locService.t('node.editQueuedForUpload'))),
);
} }
void _cancel() { void _cancel() {
@@ -196,7 +160,7 @@ class EditNodeSheet extends StatelessWidget {
final submittableProfiles = appState.enabledProfiles.where((p) => p.isSubmittable).toList(); final submittableProfiles = appState.enabledProfiles.where((p) => p.isSubmittable).toList();
final isSandboxMode = appState.uploadMode == UploadMode.sandbox; final isSandboxMode = appState.uploadMode == UploadMode.sandbox;
final allowSubmit = kEnableNodeEdits && final allowSubmit = dev.kEnableNodeEdits &&
appState.isLoggedIn && appState.isLoggedIn &&
submittableProfiles.isNotEmpty && submittableProfiles.isNotEmpty &&
session.profile != null && session.profile != null &&
@@ -256,7 +220,7 @@ class EditNodeSheet extends StatelessWidget {
child: Column( child: Column(
children: [ children: [
// Extract from way checkbox (only show if enabled in dev config) // Extract from way checkbox (only show if enabled in dev config)
if (kEnableNodeExtraction) ...[ if (dev.kEnableNodeExtraction) ...[
CheckboxListTile( CheckboxListTile(
title: Text(locService.t('editNode.extractFromWay')), title: Text(locService.t('editNode.extractFromWay')),
subtitle: Text(locService.t('editNode.extractFromWaySubtitle')), subtitle: Text(locService.t('editNode.extractFromWaySubtitle')),
@@ -270,7 +234,7 @@ class EditNodeSheet extends StatelessWidget {
const SizedBox(height: 8), const SizedBox(height: 8),
], ],
// Constraint info message (only show if extract is not checked or not enabled) // Constraint info message (only show if extract is not checked or not enabled)
if (!kEnableNodeExtraction || !session.extractFromWay) ...[ if (!dev.kEnableNodeExtraction || !session.extractFromWay) ...[
Row( Row(
children: [ children: [
const Icon(Icons.info_outline, size: 20), const Icon(Icons.info_outline, size: 20),
@@ -302,7 +266,7 @@ class EditNodeSheet extends StatelessWidget {
), ),
), ),
if (!kEnableNodeEdits) if (!dev.kEnableNodeEdits)
Padding( Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 12), padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
child: Row( child: Row(
+4 -4
View File
@@ -28,7 +28,7 @@ class CameraMapMarker extends StatefulWidget {
class _CameraMapMarkerState extends State<CameraMapMarker> { class _CameraMapMarkerState extends State<CameraMapMarker> {
Timer? _tapTimer; Timer? _tapTimer;
// From dev_config.dart for build-time parameters // From dev_config.dart for build-time parameters
static const Duration tapTimeout = kMarkerTapTimeout; static final Duration tapTimeout = dev.dev.kMarkerTapTimeout;
void _onTap() { void _onTap() {
_tapTimer = Timer(tapTimeout, () { _tapTimer = Timer(tapTimeout, () {
@@ -49,7 +49,7 @@ class _CameraMapMarkerState extends State<CameraMapMarker> {
void _onDoubleTap() { void _onDoubleTap() {
_tapTimer?.cancel(); _tapTimer?.cancel();
widget.mapController.move(widget.node.coord, widget.mapController.camera.zoom + kNodeDoubleTapZoomDelta); widget.mapController.move(widget.node.coord, widget.mapController.camera.zoom + dev.kNodeDoubleTapZoomDelta);
} }
@override @override
@@ -108,8 +108,8 @@ class CameraMarkersBuilder {
return Marker( return Marker(
point: n.coord, point: n.coord,
width: kNodeIconDiameter, width: dev.kNodeIconDiameter,
height: kNodeIconDiameter, height: dev.kNodeIconDiameter,
child: Opacity( child: Opacity(
opacity: shouldDimNode ? 0.5 : 1.0, opacity: shouldDimNode ? 0.5 : 1.0,
child: CameraMapMarker( child: CameraMapMarker(
@@ -72,12 +72,12 @@ class CameraRefreshController {
} }
final zoom = controller.mapController.camera.zoom; final zoom = controller.mapController.camera.zoom;
if (zoom < kNodeMinZoomLevel) { if (zoom < dev.kNodeMinZoomLevel) {
// Show a snackbar-style bubble warning // Show a snackbar-style bubble warning
if (context.mounted) { if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text('Nodes not drawn below zoom level $kNodeMinZoomLevel'), content: Text('Nodes not drawn below zoom level $dev.kNodeMinZoomLevel'),
duration: const Duration(seconds: 2), duration: const Duration(seconds: 2),
), ),
); );
+7 -7
View File
@@ -112,11 +112,11 @@ class DirectionConesBuilder {
bool isSession = false, bool isSession = false,
bool isActiveDirection = true, bool isActiveDirection = true,
}) { }) {
final halfAngle = kDirectionConeHalfAngle; final halfAngle = dev.kDirectionConeHalfAngle;
// Calculate pixel-based radii // Calculate pixel-based radii
final outerRadiusPx = kNodeIconDiameter + (kNodeIconDiameter * kDirectionConeBaseLength); final outerRadiusPx = dev.kNodeIconDiameter + (dev.kNodeIconDiameter * dev.kDirectionConeBaseLength);
final innerRadiusPx = kNodeIconDiameter + (2 * getNodeRingThickness(context)); final innerRadiusPx = dev.kNodeIconDiameter + (2 * getNodeRingThickness(context));
// Convert pixels to coordinate distances with zoom scaling // Convert pixels to coordinate distances with zoom scaling
final pixelToCoordinate = 0.00001 * math.pow(2, 15 - zoom); final pixelToCoordinate = 0.00001 * math.pow(2, 15 - zoom);
@@ -150,15 +150,15 @@ class DirectionConesBuilder {
} }
// Adjust opacity based on direction state // Adjust opacity based on direction state
double opacity = kDirectionConeOpacity; double opacity = dev.kDirectionConeOpacity;
if (isSession && !isActiveDirection) { if (isSession && !isActiveDirection) {
opacity = kDirectionConeOpacity * 0.4; // Reduced opacity for inactive session directions opacity = dev.kDirectionConeOpacity * 0.4; // Reduced opacity for inactive session directions
} }
return Polygon( return Polygon(
points: points, points: points,
color: kDirectionConeColor.withOpacity(opacity), color: dev.kDirectionConeColor.withOpacity(opacity),
borderColor: kDirectionConeColor, borderColor: dev.kDirectionConeColor,
borderStrokeWidth: getDirectionConeBorderWidth(context), borderStrokeWidth: getDirectionConeBorderWidth(context),
); );
} }
+5 -5
View File
@@ -60,7 +60,7 @@ class GpsController {
controller.animateTo( controller.animateTo(
dest: _currentLatLng!, dest: _currentLatLng!,
zoom: controller.mapController.camera.zoom, zoom: controller.mapController.camera.zoom,
duration: kFollowMeAnimationDuration, duration: dev.kFollowMeAnimationDuration,
curve: Curves.easeOut, curve: Curves.easeOut,
); );
onMapMovedProgrammatically?.call(); onMapMovedProgrammatically?.call();
@@ -70,7 +70,7 @@ class GpsController {
dest: _currentLatLng!, dest: _currentLatLng!,
zoom: controller.mapController.camera.zoom, zoom: controller.mapController.camera.zoom,
rotation: 0.0, rotation: 0.0,
duration: kFollowMeAnimationDuration, duration: dev.kFollowMeAnimationDuration,
curve: Curves.easeOut, curve: Curves.easeOut,
); );
onMapMovedProgrammatically?.call(); onMapMovedProgrammatically?.call();
@@ -123,7 +123,7 @@ class GpsController {
dest: latLng, dest: latLng,
zoom: controller.mapController.camera.zoom, zoom: controller.mapController.camera.zoom,
rotation: controller.mapController.camera.rotation, rotation: controller.mapController.camera.rotation,
duration: kFollowMeAnimationDuration, duration: dev.kFollowMeAnimationDuration,
curve: Curves.easeOut, curve: Curves.easeOut,
); );
@@ -135,14 +135,14 @@ class GpsController {
final speed = position.speed; // Speed in m/s final speed = position.speed; // Speed in m/s
// Only apply rotation if moving fast enough to avoid wild spinning when stationary // Only apply rotation if moving fast enough to avoid wild spinning when stationary
final shouldRotate = !speed.isNaN && speed >= kMinSpeedForRotationMps && !heading.isNaN; final shouldRotate = !speed.isNaN && speed >= dev.kMinSpeedForRotationMps && !heading.isNaN;
final rotation = shouldRotate ? -heading : controller.mapController.camera.rotation; final rotation = shouldRotate ? -heading : controller.mapController.camera.rotation;
controller.animateTo( controller.animateTo(
dest: latLng, dest: latLng,
zoom: controller.mapController.camera.zoom, zoom: controller.mapController.camera.zoom,
rotation: rotation, rotation: rotation,
duration: kFollowMeAnimationDuration, duration: dev.kFollowMeAnimationDuration,
curve: Curves.easeOut, curve: Curves.easeOut,
); );
+3 -3
View File
@@ -94,7 +94,7 @@ class MapOverlays extends StatelessWidget {
// Zoom indicator, positioned relative to button bar with left safe area // Zoom indicator, positioned relative to button bar with left safe area
Positioned( Positioned(
left: leftPositionWithSafeArea(10, safeArea), left: leftPositionWithSafeArea(10, safeArea),
bottom: bottomPositionFromButtonBar(kZoomIndicatorSpacingAboveButtonBar, safeArea.bottom), bottom: bottomPositionFromButtonBar(dev.kZoomIndicatorSpacingAboveButtonBar, safeArea.bottom),
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2), padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2),
decoration: BoxDecoration( decoration: BoxDecoration(
@@ -125,7 +125,7 @@ class MapOverlays extends StatelessWidget {
// Attribution overlay, positioned relative to button bar with left safe area // Attribution overlay, positioned relative to button bar with left safe area
if (attribution != null) if (attribution != null)
Positioned( Positioned(
bottom: bottomPositionFromButtonBar(kAttributionSpacingAboveButtonBar, safeArea.bottom), bottom: bottomPositionFromButtonBar(dev.kAttributionSpacingAboveButtonBar, safeArea.bottom),
left: leftPositionWithSafeArea(10, safeArea), left: leftPositionWithSafeArea(10, safeArea),
child: GestureDetector( child: GestureDetector(
onTap: () => _showAttributionDialog(context, attribution!), onTap: () => _showAttributionDialog(context, attribution!),
@@ -151,7 +151,7 @@ class MapOverlays extends StatelessWidget {
// Zoom and layer controls (bottom-right), positioned relative to button bar with right safe area // Zoom and layer controls (bottom-right), positioned relative to button bar with right safe area
Positioned( Positioned(
bottom: bottomPositionFromButtonBar(kZoomControlsSpacingAboveButtonBar, safeArea.bottom), bottom: bottomPositionFromButtonBar(dev.kZoomControlsSpacingAboveButtonBar, safeArea.bottom),
right: rightPositionWithSafeArea(16, safeArea), right: rightPositionWithSafeArea(16, safeArea),
child: Consumer<AppState>( child: Consumer<AppState>(
builder: (context, appState, child) { builder: (context, appState, child) {
@@ -28,7 +28,7 @@ class SuspectedLocationMapMarker extends StatefulWidget {
class _SuspectedLocationMapMarkerState extends State<SuspectedLocationMapMarker> { class _SuspectedLocationMapMarkerState extends State<SuspectedLocationMapMarker> {
Timer? _tapTimer; Timer? _tapTimer;
// From dev_config.dart for build-time parameters // From dev_config.dart for build-time parameters
static const Duration tapTimeout = kMarkerTapTimeout; static final Duration tapTimeout = dev.dev.kMarkerTapTimeout;
void _onTap() { void _onTap() {
_tapTimer = Timer(tapTimeout, () { _tapTimer = Timer(tapTimeout, () {
@@ -47,7 +47,7 @@ class _SuspectedLocationMapMarkerState extends State<SuspectedLocationMapMarker>
void _onDoubleTap() { void _onDoubleTap() {
_tapTimer?.cancel(); _tapTimer?.cancel();
widget.mapController.move(widget.location.centroid, widget.mapController.camera.zoom + kNodeDoubleTapZoomDelta); widget.mapController.move(widget.location.centroid, widget.mapController.camera.zoom + dev.kNodeDoubleTapZoomDelta);
} }
@override @override
+15 -15
View File
@@ -60,7 +60,7 @@ class MapView extends StatefulWidget {
class MapViewState extends State<MapView> { class MapViewState extends State<MapView> {
late final AnimatedMapController _controller; late final AnimatedMapController _controller;
final Debouncer _cameraDebounce = Debouncer(kDebounceCameraRefresh); final Debouncer _cameraDebounce = Debouncer(dev.kDebounceCameraRefresh);
final Debouncer _tileDebounce = Debouncer(const Duration(milliseconds: 150)); final Debouncer _tileDebounce = Debouncer(const Duration(milliseconds: 150));
final Debouncer _mapPositionDebounce = Debouncer(const Duration(milliseconds: 1000)); final Debouncer _mapPositionDebounce = Debouncer(const Duration(milliseconds: 1000));
final Debouncer _constrainedNodeSnapBack = Debouncer(const Duration(milliseconds: 100)); final Debouncer _constrainedNodeSnapBack = Debouncer(const Duration(milliseconds: 100));
@@ -240,9 +240,9 @@ class MapViewState extends State<MapView> {
// OSM API (sandbox mode) needs higher zoom level due to bbox size limits // OSM API (sandbox mode) needs higher zoom level due to bbox size limits
if (uploadMode == UploadMode.sandbox) { if (uploadMode == UploadMode.sandbox) {
return kOsmApiMinZoomLevel; return dev.kOsmApiMinZoomLevel;
} else { } else {
return kNodeMinZoomLevel; return dev.kNodeMinZoomLevel;
} }
} }
@@ -268,17 +268,17 @@ class MapViewState extends State<MapView> {
// Check if we're editing a constrained node that's not being extracted // Check if we're editing a constrained node that's not being extracted
if (editSession?.originalNode.isConstrained == true && editSession?.extractFromWay != true) { if (editSession?.originalNode.isConstrained == true && editSession?.extractFromWay != true) {
// Constrained node (not extracting): only allow pinch zoom and rotation, disable ALL panning // Constrained node (not extracting): only allow pinch zoom and rotation, disable ALL panning
return const InteractionOptions( return InteractionOptions(
enableMultiFingerGestureRace: true, enableMultiFingerGestureRace: true,
flags: InteractiveFlag.pinchZoom | InteractiveFlag.rotate, flags: InteractiveFlag.pinchZoom | InteractiveFlag.rotate,
scrollWheelVelocity: kScrollWheelVelocity, scrollWheelVelocity: dev.kScrollWheelVelocity,
pinchZoomThreshold: kPinchZoomThreshold, pinchZoomThreshold: dev.kPinchZoomThreshold,
pinchMoveThreshold: kPinchMoveThreshold, pinchMoveThreshold: dev.kPinchMoveThreshold,
); );
} }
// Normal case: all interactions allowed with gesture race to prevent accidental rotation during zoom // Normal case: all interactions allowed with gesture race to prevent accidental rotation during zoom
return const InteractionOptions( return InteractionOptions(
enableMultiFingerGestureRace: true, enableMultiFingerGestureRace: true,
flags: InteractiveFlag.doubleTapDragZoom | flags: InteractiveFlag.doubleTapDragZoom |
InteractiveFlag.doubleTapZoom | InteractiveFlag.doubleTapZoom |
@@ -287,9 +287,9 @@ class MapViewState extends State<MapView> {
InteractiveFlag.pinchZoom | InteractiveFlag.pinchZoom |
InteractiveFlag.rotate | InteractiveFlag.rotate |
InteractiveFlag.scrollWheelZoom, InteractiveFlag.scrollWheelZoom,
scrollWheelVelocity: kScrollWheelVelocity, scrollWheelVelocity: dev.kScrollWheelVelocity,
pinchZoomThreshold: kPinchZoomThreshold, pinchZoomThreshold: dev.kPinchZoomThreshold,
pinchMoveThreshold: kPinchMoveThreshold, pinchMoveThreshold: dev.kPinchMoveThreshold,
); );
} }
@@ -506,8 +506,8 @@ class MapViewState extends State<MapView> {
centerMarkers.add( centerMarkers.add(
Marker( Marker(
point: center, point: center,
width: kNodeIconDiameter, width: dev.kNodeIconDiameter,
height: kNodeIconDiameter, height: dev.kNodeIconDiameter,
child: CameraIcon( child: CameraIcon(
type: editSession != null ? CameraIconType.editing : CameraIconType.mock, type: editSession != null ? CameraIconType.editing : CameraIconType.mock,
), ),
@@ -691,7 +691,7 @@ class MapViewState extends State<MapView> {
alignment: Alignment.bottomLeft, alignment: Alignment.bottomLeft,
padding: EdgeInsets.only( padding: EdgeInsets.only(
left: leftPositionWithSafeArea(8, safeArea), left: leftPositionWithSafeArea(8, safeArea),
bottom: bottomPositionFromButtonBar(kScaleBarSpacingAboveButtonBar, safeArea.bottom) bottom: bottomPositionFromButtonBar(dev.kScaleBarSpacingAboveButtonBar, safeArea.bottom)
), ),
textStyle: TextStyle(color: Colors.black, fontWeight: FontWeight.bold), textStyle: TextStyle(color: Colors.black, fontWeight: FontWeight.bold),
lineColor: Colors.black, lineColor: Colors.black,
@@ -753,7 +753,7 @@ class MapViewState extends State<MapView> {
if (originalCoord != null) { if (originalCoord != null) {
lines.add(Polyline( lines.add(Polyline(
points: [originalCoord, node.coord], points: [originalCoord, node.coord],
color: kNodeRingColorPending, color: dev.kNodeRingColorPending,
strokeWidth: 3.0, strokeWidth: 3.0,
)); ));
} }
-126
View File
@@ -1,126 +0,0 @@
import 'package:flutter/material.dart';
import 'package:latlong2/latlong.dart';
import '../models/osm_node.dart';
import '../services/localization_service.dart';
class ProximityWarningDialog extends StatelessWidget {
final List<OsmNode> nearbyNodes;
final double distance;
final VoidCallback onGoBack;
final VoidCallback onSubmitAnyway;
const ProximityWarningDialog({
super.key,
required this.nearbyNodes,
required this.distance,
required this.onGoBack,
required this.onSubmitAnyway,
});
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: LocalizationService.instance,
builder: (context, child) {
final locService = LocalizationService.instance;
return AlertDialog(
icon: const Icon(
Icons.warning_amber_rounded,
color: Colors.orange,
size: 32,
),
title: Text(locService.t('proximityWarning.title')),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
locService.t('proximityWarning.message',
params: [distance.toStringAsFixed(1)]),
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 16),
Text(
locService.t('proximityWarning.suggestion'),
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontStyle: FontStyle.italic,
),
),
const SizedBox(height: 16),
Text(
locService.t('proximityWarning.nearbyNodes',
params: [nearbyNodes.length.toString()]),
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
...nearbyNodes.take(3).map((node) => Padding(
padding: const EdgeInsets.only(left: 8.0, bottom: 4.0),
child: Text(
'• ${locService.t('proximityWarning.nodeInfo', params: [
node.id.toString(),
_getNodeTypeDescription(node, locService),
])}',
style: Theme.of(context).textTheme.bodySmall,
),
)),
if (nearbyNodes.length > 3)
Padding(
padding: const EdgeInsets.only(left: 8.0, top: 4.0),
child: Text(
locService.t('proximityWarning.andMore',
params: [(nearbyNodes.length - 3).toString()]),
style: Theme.of(context).textTheme.bodySmall?.copyWith(
fontStyle: FontStyle.italic,
),
),
),
],
),
actions: [
TextButton(
onPressed: onGoBack,
child: Text(locService.t('proximityWarning.goBack')),
),
ElevatedButton(
onPressed: onSubmitAnyway,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange,
foregroundColor: Colors.white,
),
child: Text(locService.t('proximityWarning.submitAnyway')),
),
],
);
},
);
}
String _getNodeTypeDescription(OsmNode node, LocalizationService locService) {
// Try to get a meaningful description from the node's tags
final manMade = node.tags['man_made'];
final amenity = node.tags['amenity'];
final surveillance = node.tags['surveillance'];
final surveillanceType = node.tags['surveillance:type'];
final manufacturer = node.tags['manufacturer'];
if (manMade == 'surveillance') {
if (surveillanceType == 'ALPR' || surveillanceType == 'ANPR') {
return locService.t('proximityWarning.nodeType.alpr');
} else if (surveillance == 'public') {
return locService.t('proximityWarning.nodeType.publicCamera');
} else {
return locService.t('proximityWarning.nodeType.camera');
}
} else if (amenity != null) {
return locService.t('proximityWarning.nodeType.amenity', params: [amenity]);
} else if (manufacturer != null) {
return locService.t('proximityWarning.nodeType.device', params: [manufacturer]);
} else {
return locService.t('proximityWarning.nodeType.unknown');
}
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
name: deflockapp name: deflockapp
description: Map public surveillance infrastructure with OpenStreetMap description: Map public surveillance infrastructure with OpenStreetMap
publish_to: "none" publish_to: "none"
version: 1.4.3+14 # The thing after the + is the version code, incremented with each release version: 1.4.1+14 # The thing after the + is the version code, incremented with each release
environment: environment:
sdk: ">=3.5.0 <4.0.0" # oauth2_client 4.x needs Dart 3.5+ sdk: ">=3.5.0 <4.0.0" # oauth2_client 4.x needs Dart 3.5+
+150
View File
@@ -0,0 +1,150 @@
#!/usr/bin/env python3
import os
import re
# All constants to replace
CONSTANTS = [
"kFallbackTileEstimateKb", "kPreviewTileZoom", "kPreviewTileY", "kPreviewTileX",
"kDirectionConeHalfAngle", "kDirectionConeBaseLength", "kDirectionConeColor", "kDirectionConeOpacity",
"kBottomButtonBarOffset", "kButtonBarHeight", "kAttributionSpacingAboveButtonBar",
"kZoomIndicatorSpacingAboveButtonBar", "kScaleBarSpacingAboveButtonBar", "kZoomControlsSpacingAboveButtonBar",
"kClientName", "kSuspectedLocationsCsvUrl", "kEnableDevelopmentModes", "kEnableNodeEdits", "kEnableNodeExtraction",
"kNodeMinZoomLevel", "kOsmApiMinZoomLevel", "kMarkerTapTimeout", "kDebounceCameraRefresh",
"kPreFetchAreaExpansionMultiplier", "kPreFetchZoomLevel", "kMaxPreFetchSplitDepth", "kDataRefreshIntervalSeconds",
"kFollowMeAnimationDuration", "kMinSpeedForRotationMps", "kProximityAlertDefaultDistance",
"kProximityAlertMinDistance", "kProximityAlertMaxDistance", "kProximityAlertCooldown",
"kNodeDoubleTapZoomDelta", "kScrollWheelVelocity", "kPinchZoomThreshold", "kPinchMoveThreshold", "kRotationThreshold",
"kTileFetchMaxAttempts", "kTileFetchInitialDelayMs", "kTileFetchBackoffMultiplier", "kTileFetchMaxDelayMs",
"kTileFetchRandomJitterMs", "kMaxUserDownloadZoomSpan", "kMaxReasonableTileCount", "kAbsoluteMaxTileCount",
"kAbsoluteMaxZoom", "kNodeIconDiameter", "kNodeDotOpacity", "kNodeRingColorReal", "kNodeRingColorMock",
"kNodeRingColorPending", "kNodeRingColorEditing", "kNodeRingColorPendingEdit", "kNodeRingColorPendingDeletion",
"kDirectionButtonMinWidth", "kDirectionButtonMinHeight"
]
def find_dart_files():
"""Find all .dart files except dev_config.dart"""
dart_files = []
for root, dirs, files in os.walk('.'):
for file in files:
if file.endswith('.dart'):
path = os.path.join(root, file)
if 'dev_config.dart' not in path:
dart_files.append(path)
return dart_files
def process_file(filepath):
"""Process a single dart file"""
print(f" 📝 Processing {filepath}")
try:
with open(filepath, 'r', encoding='utf-8') as f:
content = f.read()
except Exception as e:
print(f" ❌ Error reading file: {e}")
return False
original_content = content
changes_made = []
# Process each constant
for constant in CONSTANTS:
content, changed = process_constant_in_content(content, constant)
if changed:
changes_made.append(constant)
# Only write if something actually changed
if content != original_content:
try:
with open(filepath, 'w', encoding='utf-8') as f:
f.write(content)
print(f" ✅ Updated: {', '.join(changes_made)}")
return True
except Exception as e:
print(f" ❌ Error writing file: {e}")
return False
else:
print(f" ⏭️ No changes needed")
return False
def process_constant_in_content(content, constant):
"""Process a single constant in file content, handling const issues"""
original_content = content
# Skip if already using dev.constant (idempotent)
if f"dev.{constant}" in content:
return content, False
# Skip if constant not found at all
if constant not in content:
return content, False
print(f" 🔄 Replacing {constant}")
# Pattern 1: const Type variable = kConstant;
# Change to: final Type variable = dev.kConstant;
pattern1 = rf'\bconst\s+(\w+)\s+(\w+)\s*=\s*{re.escape(constant)}\s*;'
replacement1 = rf'final \1 \2 = dev.{constant};'
content = re.sub(pattern1, replacement1, content)
# Pattern 2: static const Type variable = kConstant;
# Change to: static final Type variable = dev.kConstant;
pattern2 = rf'\bstatic\s+const\s+(\w+)\s+(\w+)\s*=\s*{re.escape(constant)}\s*;'
replacement2 = rf'static final \1 \2 = dev.{constant};'
content = re.sub(pattern2, replacement2, content)
# Pattern 3: const ConstructorName(...kConstant...)
# We need to be careful here - find const constructors that contain our constant
# and remove the const keyword
# This is tricky to do perfectly with regex, so let's do a simple approach:
# If we find "const SomeConstructor(" followed by our constant somewhere before the matching ")"
# we'll remove the const keyword from the constructor
# Find all const constructor calls that contain our constant
const_constructor_pattern = r'\bconst\s+(\w+)\s*\([^)]*' + re.escape(constant) + r'[^)]*\)'
matches = list(re.finditer(const_constructor_pattern, content))
# Replace const with just the constructor name for each match
for match in reversed(matches): # Reverse to maintain positions
full_match = match.group(0)
constructor_name = match.group(1)
# Remove 'const ' from the beginning
replacement = full_match.replace(f'const {constructor_name}', constructor_name, 1)
content = content[:match.start()] + replacement + content[match.end():]
# Pattern 4: Simple replacements - any remaining instances of kConstant
# Use word boundaries to avoid partial matches, but avoid already replaced dev.kConstant
pattern4 = rf'\b{re.escape(constant)}\b(?![\w.])' # Negative lookahead to avoid partial matches
replacement4 = f'dev.{constant}'
content = re.sub(pattern4, replacement4, content)
return content, content != original_content
def main():
print("🚀 Starting dev_config reference update...")
print("🔍 Finding Dart files...")
dart_files = find_dart_files()
print(f"📁 Found {len(dart_files)} Dart files to process")
if not dart_files:
print("❌ No Dart files found!")
return
updated_files = 0
for filepath in dart_files:
if process_file(filepath):
updated_files += 1
print(f"\n✨ Finished! Updated {updated_files} out of {len(dart_files)} files")
print("💡 Next steps:")
print(" 1. flutter analyze (check for syntax errors)")
print(" 2. flutter pub get (refresh dependencies)")
print(" 3. flutter run (test the app)")
if updated_files > 0:
print("⚠️ If you see compilation errors, the script can be run again safely")
if __name__ == '__main__':
main()
+12
View File
@@ -0,0 +1,12 @@
#!/bin/bash
# Super simple test - just replace one constant first
echo "🔄 Testing with kClientName..."
find . -name "*.dart" -not -path "./lib/dev_config.dart" -exec grep -l "kClientName" {} \;
echo "Found files with kClientName. Now replacing..."
find . -name "*.dart" -not -path "./lib/dev_config.dart" -exec sed -i '' 's/kClientName/dev.kClientName/g' {} \;
echo "✅ Done with test. Check if lib/services/uploader.dart changed"