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
### Needed Bugfixes
- Update node cache to reflect cleared queue entries
- Decide what to do for extracting nodes attached to a way/relation:
- Auto extract (how?)
- Leave it alone (wrong answer unless user chooses intentionally)
- Manual cleanup (cognitive load for users)
- Delete the old one (also wrong answer unless user chooses intentionally)
- Give multiple of these options??
- Two nodes too close together warning
- Nav start+end too close together error (warning + disable submit button?)
- 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
- Link to "my changes" on osm (username edit history)
- Are offline areas preferred for fast loading even when online? Check working.
- Fix network indicator - only done when fetch queue is empty!
### 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?)
- 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?)
+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": {
"content": [
"• 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: '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": {
"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": {
"content": [
"• 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)",
"• Web editors (iD, RapiD) remain available on all platforms as before"
]
@@ -27,17 +33,27 @@
"content": [
"• 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",
"• 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": {
"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",
"• 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: '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: 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": {
-2
View File
@@ -17,8 +17,6 @@ import 'services/changelog_service.dart';
import 'services/operator_profile_service.dart';
import 'services/profile_service.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/navigation_state.dart';
import 'state/operator_profile_state.dart';
+116 -49
View File
@@ -2,36 +2,125 @@
import 'package:flutter/material.dart';
/// 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
const double kFallbackTileEstimateKb = 25.0;
// Typed configuration maps - single definition of each constant
const Map<String, bool> _boolConfig = {
'kEnableDevelopmentModes': true,
'kEnableNodeEdits': true,
'kEnableNodeExtraction': false,
};
// Preview tile coordinates for tile provider previews and size estimates
const int kPreviewTileZoom = 18;
const int kPreviewTileY = 101300;
const int kPreviewTileX = 41904;
const Map<String, int> _intConfig = {
'kPreviewTileZoom': 18,
'kPreviewTileY': 101300,
'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 double kDirectionConeHalfAngle = 35.0; // degrees
const double kDirectionConeBaseLength = 5; // multiplier
const Color kDirectionConeColor = Color(0xD0767474); // FOV cone color
const double kDirectionConeOpacity = 0.5; // Fill opacity for FOV cones
// Base values for thickness - use helper functions below for pixel-ratio scaling
const double _kDirectionConeBorderWidthBase = 1.6;
const Map<String, double> _doubleConfig = {
'kFallbackTileEstimateKb': 25.0,
'kDirectionConeHalfAngle': 35.0,
'kDirectionConeBaseLength': 5.0,
'kDirectionConeOpacity': 0.5,
'_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 double kBottomButtonBarOffset = 4.0; // Distance from screen bottom (above safe area)
const double kButtonBarHeight = 60.0; // Button height (48) + padding (12)
const Map<String, String> _stringConfig = {
'kClientName': 'DeFlock', // Read-only in settings
'kSuspectedLocationsCsvUrl': 'https://stopflock.com/app/flock_utilities_mini_latest.csv',
};
// Map overlay spacing relative to button bar top
const double kAttributionSpacingAboveButtonBar = 10.0; // Attribution above button bar top
const double kZoomIndicatorSpacingAboveButtonBar = 40.0; // Zoom indicator above button bar top
const double kScaleBarSpacingAboveButtonBar = 70.0; // Scale bar above button bar top
const double kZoomControlsSpacingAboveButtonBar = 20.0; // Zoom controls above button bar top
const Map<String, Color> _colorConfig = {
'kDirectionConeColor': Color(0xD0767474),
'kNodeRingColorReal': Color(0xFF3036F0),
'kNodeRingColorMock': Color(0xD0FFFFFF),
'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
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)
@@ -49,28 +138,9 @@ double topPositionWithSafeArea(double baseTop, EdgeInsets safeArea) {
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
bool enableNavigationFeatures({required bool offlineMode}) {
if (!kEnableDevelopmentModes) {
if (!dev.kEnableDevelopmentModes) {
return false; // Release builds: never allow navigation
} else {
return !offlineMode; // Dev builds: only when online
@@ -112,9 +182,6 @@ const int kProximityAlertMinDistance = 50; // meters
const int kProximityAlertMaxDistance = 1600; // meters
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
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)
@@ -154,11 +221,11 @@ const double kDirectionButtonMinHeight = 32.0;
// Helper functions for pixel-ratio scaling
double getDirectionConeBorderWidth(BuildContext context) {
// return _kDirectionConeBorderWidthBase * MediaQuery.of(context).devicePixelRatio;
return _kDirectionConeBorderWidthBase;
// return dev._kDirectionConeBorderWidthBase * MediaQuery.of(context).devicePixelRatio;
return dev._kDirectionConeBorderWidthBase;
}
double getNodeRingThickness(BuildContext context) {
// return _kNodeRingThicknessBase * MediaQuery.of(context).devicePixelRatio;
return _kNodeRingThicknessBase;
// return dev._kNodeRingThicknessBase * MediaQuery.of(context).devicePixelRatio;
return dev._kNodeRingThicknessBase;
}
-18
View File
@@ -21,24 +21,6 @@
"advanced": "Erweitert",
"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": {
"off": "Verfolgung aktivieren",
"follow": "Verfolgung aktivieren (Rotation)",
-18
View File
@@ -39,24 +39,6 @@
"advanced": "Advanced",
"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": {
"off": "Enable follow-me",
"follow": "Enable follow-me (rotating)",
-18
View File
@@ -39,24 +39,6 @@
"advanced": "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": {
"off": "Activar seguimiento",
"follow": "Activar seguimiento (rotación)",
-18
View File
@@ -39,24 +39,6 @@
"advanced": "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": {
"off": "Activer le suivi",
"follow": "Activer le suivi (rotation)",
-18
View File
@@ -39,24 +39,6 @@
"advanced": "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": {
"off": "Attiva seguimi",
"follow": "Attiva seguimi (rotazione)",
-18
View File
@@ -39,24 +39,6 @@
"advanced": "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": {
"off": "Ativar seguir-me",
"follow": "Ativar seguir-me (rotação)",
-18
View File
@@ -39,24 +39,6 @@
"advanced": "高级",
"useAdvancedEditor": "使用高级编辑器"
},
"proximityWarning": {
"title": "节点过于靠近现有设备",
"message": "此节点距离现有监控设备仅 {} 米。",
"suggestion": "如果同一根杆上有多个设备,请在单个节点上使用多个方向,而不是创建单独的节点。",
"nearbyNodes": "发现附近设备 ({}):",
"nodeInfo": "节点 #{} - {}",
"andMore": "...还有 {} 个",
"goBack": "返回",
"submitAnyway": "仍然提交",
"nodeType": {
"alpr": "ALPR/ANPR 摄像头",
"publicCamera": "公共监控摄像头",
"camera": "监控摄像头",
"amenity": "{}",
"device": "{} 设备",
"unknown": "未知设备"
}
},
"followMe": {
"off": "启用跟随模式",
"follow": "启用跟随模式(旋转)",
+2
View File
@@ -8,6 +8,7 @@ import 'screens/profiles_settings_screen.dart';
import 'screens/navigation_settings_screen.dart';
import 'screens/offline_settings_screen.dart';
import 'screens/advanced_settings_screen.dart';
import 'screens/developer_settings_screen.dart';
import 'screens/language_settings_screen.dart';
import 'screens/about_screen.dart';
import 'screens/release_notes_screen.dart';
@@ -77,6 +78,7 @@ class DeFlockApp extends StatelessWidget {
'/settings/navigation': (context) => const NavigationSettingsScreen(),
'/settings/offline': (context) => const OfflineSettingsScreen(),
'/settings/advanced': (context) => const AdvancedSettingsScreen(),
'/settings/developer': (context) => const DeveloperSettingsScreen(),
'/settings/language': (context) => const LanguageSettingsScreen(),
'/settings/about': (context) => const AboutScreen(),
'/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;
return Padding(
padding: EdgeInsets.only(
bottom: safeArea.bottom + kBottomButtonBarOffset,
bottom: safeArea.bottom + dev.kBottomButtonBarOffset,
left: leftPositionWithSafeArea(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),
child: Row(
children: [
+1 -1
View File
@@ -117,7 +117,7 @@ class OSMAccountScreen extends StatelessWidget {
const SizedBox(height: 16),
// Upload Mode Section (only show in development builds)
if (kEnableDevelopmentModes) ...[
if (dev.kEnableDevelopmentModes) ...[
Card(
child: const Padding(
padding: EdgeInsets.all(16.0),
+40 -7
View File
@@ -1,11 +1,41 @@
import 'dart:async';
import 'package:flutter/material.dart';
import '../services/localization_service.dart';
import '../services/version_service.dart';
import '../dev_config.dart';
class SettingsScreen extends StatelessWidget {
class SettingsScreen extends StatefulWidget {
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
Widget build(BuildContext context) {
final locService = LocalizationService.instance;
@@ -100,15 +130,18 @@ class SettingsScreen extends StatelessWidget {
),
const Divider(),
// Version display
// Version display with secret tap counter
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Text(
'Version: ${VersionService().version}',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).textTheme.bodySmall?.color?.withOpacity(0.6),
child: GestureDetector(
onTap: _onVersionTap,
child: Text(
'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');
final zoom = int.tryParse(value!);
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;
},
),
@@ -405,9 +405,9 @@ class _TileTypeDialogState extends State<_TileTypeDialog> {
try {
// Use a sample tile from configured preview location
final url = _urlController.text
.replaceAll('{z}', kPreviewTileZoom.toString())
.replaceAll('{x}', kPreviewTileX.toString())
.replaceAll('{y}', kPreviewTileY.toString());
.replaceAll('{z}', dev.kPreviewTileZoom.toString())
.replaceAll('{x}', dev.kPreviewTileX.toString())
.replaceAll('{y}', dev.kPreviewTileY.toString());
final response = await http.get(Uri.parse(url));
@@ -44,7 +44,7 @@ Future<List<OsmNode>> _fetchOverpassNodesWithSplitting({
}) async {
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 {
return await _fetchSingleOverpassQuery(
@@ -36,14 +36,14 @@ void clearRemoteTileQueueSelective(LatLngBounds currentBounds) {
/// Uses: initialDelay * (multiplier ^ (attempt - 1)) + randomJitter, capped at maxDelay
int _calculateRetryDelay(int attempt, Random random) {
// Calculate exponential backoff: initialDelay * (multiplier ^ (attempt - 1))
final baseDelay = (kTileFetchInitialDelayMs *
pow(kTileFetchBackoffMultiplier, attempt - 1)).round();
final baseDelay = (dev.kTileFetchInitialDelayMs *
pow(dev.kTileFetchBackoffMultiplier, attempt - 1)).round();
// Add random jitter to avoid thundering herd
final jitter = random.nextInt(kTileFetchRandomJitterMs + 1);
final jitter = random.nextInt(dev.kTileFetchRandomJitterMs + 1);
// 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
@@ -101,7 +101,7 @@ Future<List<int>> fetchRemoteTile({
required int y,
required String url,
}) async {
const int maxAttempts = kTileFetchMaxAttempts;
final int maxAttempts = dev.dev.kTileFetchMaxAttempts;
int attempt = 0;
final random = Random();
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 'package:flutter_map/flutter_map.dart' show LatLngBounds;
const Distance _distance = Distance();
class NodeCache {
// Singleton instance
static final NodeCache instance = NodeCache._internal();
@@ -105,34 +103,6 @@ class NodeCache {
(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
bool _inBounds(LatLng coord, LatLngBounds bounds) {
return coord.latitude >= bounds.southWest.latitude &&
+4 -4
View File
@@ -30,8 +30,8 @@ class PrefetchAreaService {
Timer? _debounceTimer;
// Configuration from dev_config
static const double _areaExpansionMultiplier = kPreFetchAreaExpansionMultiplier;
static const int _preFetchZoomLevel = kPreFetchZoomLevel;
static final double _areaExpansionMultiplier = dev.dev.kPreFetchAreaExpansionMultiplier;
static final int _preFetchZoomLevel = dev.dev.kPreFetchZoomLevel;
/// Check if the given bounds are fully within the current pre-fetched area.
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).
bool isDataStale() {
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.
@@ -84,7 +84,7 @@ class PrefetchAreaService {
}
if (isStale) {
debugPrint('[PrefetchAreaService] Data is stale (>${kDataRefreshIntervalSeconds}s), refreshing');
debugPrint('[PrefetchAreaService] Data is stale (>${dev.kDataRefreshIntervalSeconds}s), refreshing');
} else {
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
final List<RecentAlert> _recentAlerts = [];
static const Duration _alertCooldown = kProximityAlertCooldown;
static final Duration _alertCooldown = dev.dev.kProximityAlertCooldown;
// Callback for showing in-app visual alerts
VoidCallback? _onVisualAlert;
+2 -2
View File
@@ -102,10 +102,10 @@ class SuspectedLocationService {
/// Fetch data from the CSV URL
Future<bool> _fetchData() async {
try {
debugPrint('[SuspectedLocationService] Fetching CSV data from $kSuspectedLocationsCsvUrl');
debugPrint('[SuspectedLocationService] Fetching CSV data from $dev.kSuspectedLocationsCsvUrl');
final response = await http.get(
Uri.parse(kSuspectedLocationsCsvUrl),
Uri.parse(dev.kSuspectedLocationsCsvUrl),
headers: {
'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 {
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);
+1 -1
View File
@@ -44,7 +44,7 @@ class Uploader {
final csXml = '''
<osm>
<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"/>
</changeset>
</osm>''';
+7 -7
View File
@@ -33,10 +33,10 @@ class SettingsState extends ChangeNotifier {
bool _offlineMode = false;
bool _pauseQueueProcessing = false;
int _maxCameras = 250;
UploadMode _uploadMode = kEnableDevelopmentModes ? UploadMode.simulate : UploadMode.production;
UploadMode _uploadMode = dev.kEnableDevelopmentModes ? UploadMode.simulate : UploadMode.production;
FollowMeMode _followMeMode = FollowMeMode.follow;
bool _proximityAlertsEnabled = false;
int _proximityAlertDistance = kProximityAlertDefaultDistance;
int _proximityAlertDistance = dev.kProximityAlertDefaultDistance;
bool _networkStatusIndicatorEnabled = true;
int _suspectedLocationMinDistance = 100; // meters
List<TileProvider> _tileProviders = [];
@@ -105,7 +105,7 @@ class SettingsState extends ChangeNotifier {
// Load proximity alerts settings
_proximityAlertsEnabled = prefs.getBool(_proximityAlertsEnabledPrefsKey) ?? false;
_proximityAlertDistance = prefs.getInt(_proximityAlertDistancePrefsKey) ?? kProximityAlertDefaultDistance;
_proximityAlertDistance = prefs.getInt(_proximityAlertDistancePrefsKey) ?? dev.kProximityAlertDefaultDistance;
// Load network status indicator setting
_networkStatusIndicatorEnabled = prefs.getBool(_networkStatusIndicatorEnabledPrefsKey) ?? true;
@@ -128,7 +128,7 @@ class SettingsState extends ChangeNotifier {
}
// 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');
_uploadMode = UploadMode.production;
await prefs.setInt(_uploadModePrefsKey, _uploadMode.index);
@@ -236,7 +236,7 @@ class SettingsState extends ChangeNotifier {
Future<void> setUploadMode(UploadMode mode) async {
// 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');
mode = UploadMode.production;
}
@@ -323,8 +323,8 @@ class SettingsState extends ChangeNotifier {
/// Set proximity alert distance in meters
Future<void> setProximityAlertDistance(int distance) async {
if (distance < kProximityAlertMinDistance) distance = kProximityAlertMinDistance;
if (distance > kProximityAlertMaxDistance) distance = kProximityAlertMaxDistance;
if (distance < dev.kProximityAlertMinDistance) distance = dev.kProximityAlertMinDistance;
if (distance > dev.kProximityAlertMaxDistance) distance = dev.kProximityAlertMaxDistance;
if (_proximityAlertDistance != distance) {
_proximityAlertDistance = distance;
final prefs = await SharedPreferences.getInstance();
+8 -49
View File
@@ -6,58 +6,13 @@ import '../dev_config.dart';
import '../models/node_profile.dart';
import '../models/operator_profile.dart';
import '../services/localization_service.dart';
import '../services/node_cache.dart';
import 'refine_tags_sheet.dart';
import 'proximity_warning_dialog.dart';
class AddNodeSheet extends StatelessWidget {
const AddNodeSheet({super.key, required this.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) {
final requiresDirection = session.profile != null && session.profile!.requiresDirection;
@@ -126,7 +81,7 @@ class AddNodeSheet extends StatelessWidget {
: null,
tooltip: requiresDirection ? 'Remove current direction' : 'Direction not required for this profile',
padding: EdgeInsets.zero,
constraints: const BoxConstraints(minWidth: kDirectionButtonMinWidth, minHeight: kDirectionButtonMinHeight),
constraints: BoxConstraints(minWidth: dev.kDirectionButtonMinWidth, minHeight: dev.kDirectionButtonMinHeight),
),
// Add button
IconButton(
@@ -140,7 +95,7 @@ class AddNodeSheet extends StatelessWidget {
? (session.directions.length >= 8 ? 'Maximum 8 directions allowed' : 'Add new direction')
: 'Direction not required for this profile',
padding: EdgeInsets.zero,
constraints: const BoxConstraints(minWidth: kDirectionButtonMinWidth, minHeight: kDirectionButtonMinHeight),
constraints: BoxConstraints(minWidth: dev.kDirectionButtonMinWidth, minHeight: dev.kDirectionButtonMinHeight),
),
// Cycle button
IconButton(
@@ -154,7 +109,7 @@ class AddNodeSheet extends StatelessWidget {
: null,
tooltip: requiresDirection ? 'Cycle through directions' : 'Direction not required for this profile',
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>();
void _commit() {
_checkProximityAndCommit(context, appState, locService);
appState.commitSession();
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(locService.t('node.queuedForUpload'))),
);
}
void _cancel() {
+9 -9
View File
@@ -19,28 +19,28 @@ class CameraIcon extends StatelessWidget {
Color get _ringColor {
switch (type) {
case CameraIconType.real:
return kNodeRingColorReal;
return dev.kNodeRingColorReal;
case CameraIconType.mock:
return kNodeRingColorMock;
return dev.kNodeRingColorMock;
case CameraIconType.pending:
return kNodeRingColorPending;
return dev.kNodeRingColorPending;
case CameraIconType.editing:
return kNodeRingColorEditing;
return dev.kNodeRingColorEditing;
case CameraIconType.pendingEdit:
return kNodeRingColorPendingEdit;
return dev.kNodeRingColorPendingEdit;
case CameraIconType.pendingDeletion:
return kNodeRingColorPendingDeletion;
return dev.kNodeRingColorPendingDeletion;
}
}
@override
Widget build(BuildContext context) {
return Container(
width: kNodeIconDiameter,
height: kNodeIconDiameter,
width: dev.kNodeIconDiameter,
height: dev.kNodeIconDiameter,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: _ringColor.withOpacity(kNodeDotOpacity),
color: _ringColor.withOpacity(dev.kNodeDotOpacity),
border: Border.all(
color: _ringColor,
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
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;
if (tileCount > kAbsoluteMaxTileCount) {
if (tileCount > dev.kAbsoluteMaxTileCount) {
// Return the previous zoom level that was still under the absolute limit
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
@@ -98,7 +98,7 @@ class _DownloadAreaDialogState extends State<DownloadAreaDialog> {
return previewSizeKb;
} else {
// Fall back to configured estimate
return kFallbackTileEstimateKb;
return dev.kFallbackTileEstimateKb;
}
}
@@ -176,7 +176,7 @@ class _DownloadAreaDialogState extends State<DownloadAreaDialog> {
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: _tileCount! > kMaxReasonableTileCount
color: _tileCount! > dev.kMaxReasonableTileCount
? Colors.orange.withOpacity(0.1)
: Colors.green.withOpacity(0.1),
borderRadius: BorderRadius.circular(4),
@@ -185,12 +185,12 @@ class _DownloadAreaDialogState extends State<DownloadAreaDialog> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_tileCount! > kMaxReasonableTileCount
_tileCount! > dev.kMaxReasonableTileCount
? 'Above recommended limit (Z${_maxPossibleZoom})'
: locService.t('download.maxRecommendedZoom', params: [_maxPossibleZoom.toString()]),
style: TextStyle(
fontSize: 12,
color: _tileCount! > kMaxReasonableTileCount
color: _tileCount! > dev.kMaxReasonableTileCount
? Colors.orange[700]
: Colors.green[700],
fontWeight: FontWeight.w500,
@@ -198,12 +198,12 @@ class _DownloadAreaDialogState extends State<DownloadAreaDialog> {
),
const SizedBox(height: 2),
Text(
_tileCount! > kMaxReasonableTileCount
? 'Current selection exceeds ${kMaxReasonableTileCount} recommended tile limit but is within ${kAbsoluteMaxTileCount} absolute limit'
_tileCount! > dev.kMaxReasonableTileCount
? 'Current selection exceeds ${dev.kMaxReasonableTileCount} recommended tile limit but is within ${dev.kAbsoluteMaxTileCount} absolute limit'
: locService.t('download.withinTileLimit', params: [kMaxReasonableTileCount.toString()]),
style: TextStyle(
fontSize: 11,
color: _tileCount! > kMaxReasonableTileCount
color: _tileCount! > dev.kMaxReasonableTileCount
? Colors.orange[600]
: Colors.green[600],
),
+12 -48
View File
@@ -6,55 +6,15 @@ import '../dev_config.dart';
import '../models/node_profile.dart';
import '../models/operator_profile.dart';
import '../services/localization_service.dart';
import '../services/node_cache.dart';
import '../state/settings_state.dart';
import 'refine_tags_sheet.dart';
import 'advanced_edit_options_sheet.dart';
import 'proximity_warning_dialog.dart';
class EditNodeSheet extends StatelessWidget {
const EditNodeSheet({super.key, required this.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) {
final requiresDirection = session.profile != null && session.profile!.requiresDirection;
@@ -123,7 +83,7 @@ class EditNodeSheet extends StatelessWidget {
: null,
tooltip: requiresDirection ? 'Remove current direction' : 'Direction not required for this profile',
padding: EdgeInsets.zero,
constraints: const BoxConstraints(minWidth: kDirectionButtonMinWidth, minHeight: kDirectionButtonMinHeight),
constraints: BoxConstraints(minWidth: dev.kDirectionButtonMinWidth, minHeight: dev.kDirectionButtonMinHeight),
),
// Add button
IconButton(
@@ -137,7 +97,7 @@ class EditNodeSheet extends StatelessWidget {
? (session.directions.length >= 8 ? 'Maximum 8 directions allowed' : 'Add new direction')
: 'Direction not required for this profile',
padding: EdgeInsets.zero,
constraints: const BoxConstraints(minWidth: kDirectionButtonMinWidth, minHeight: kDirectionButtonMinHeight),
constraints: BoxConstraints(minWidth: dev.kDirectionButtonMinWidth, minHeight: dev.kDirectionButtonMinHeight),
),
// Cycle button
IconButton(
@@ -151,7 +111,7 @@ class EditNodeSheet extends StatelessWidget {
: null,
tooltip: requiresDirection ? 'Cycle through directions' : 'Direction not required for this profile',
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>();
void _commit() {
_checkProximityAndCommit(context, appState, locService);
appState.commitEditSession();
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(locService.t('node.editQueuedForUpload'))),
);
}
void _cancel() {
@@ -196,7 +160,7 @@ class EditNodeSheet extends StatelessWidget {
final submittableProfiles = appState.enabledProfiles.where((p) => p.isSubmittable).toList();
final isSandboxMode = appState.uploadMode == UploadMode.sandbox;
final allowSubmit = kEnableNodeEdits &&
final allowSubmit = dev.kEnableNodeEdits &&
appState.isLoggedIn &&
submittableProfiles.isNotEmpty &&
session.profile != null &&
@@ -256,7 +220,7 @@ class EditNodeSheet extends StatelessWidget {
child: Column(
children: [
// Extract from way checkbox (only show if enabled in dev config)
if (kEnableNodeExtraction) ...[
if (dev.kEnableNodeExtraction) ...[
CheckboxListTile(
title: Text(locService.t('editNode.extractFromWay')),
subtitle: Text(locService.t('editNode.extractFromWaySubtitle')),
@@ -270,7 +234,7 @@ class EditNodeSheet extends StatelessWidget {
const SizedBox(height: 8),
],
// Constraint info message (only show if extract is not checked or not enabled)
if (!kEnableNodeExtraction || !session.extractFromWay) ...[
if (!dev.kEnableNodeExtraction || !session.extractFromWay) ...[
Row(
children: [
const Icon(Icons.info_outline, size: 20),
@@ -302,7 +266,7 @@ class EditNodeSheet extends StatelessWidget {
),
),
if (!kEnableNodeEdits)
if (!dev.kEnableNodeEdits)
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
child: Row(
+4 -4
View File
@@ -28,7 +28,7 @@ class CameraMapMarker extends StatefulWidget {
class _CameraMapMarkerState extends State<CameraMapMarker> {
Timer? _tapTimer;
// From dev_config.dart for build-time parameters
static const Duration tapTimeout = kMarkerTapTimeout;
static final Duration tapTimeout = dev.dev.kMarkerTapTimeout;
void _onTap() {
_tapTimer = Timer(tapTimeout, () {
@@ -49,7 +49,7 @@ class _CameraMapMarkerState extends State<CameraMapMarker> {
void _onDoubleTap() {
_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
@@ -108,8 +108,8 @@ class CameraMarkersBuilder {
return Marker(
point: n.coord,
width: kNodeIconDiameter,
height: kNodeIconDiameter,
width: dev.kNodeIconDiameter,
height: dev.kNodeIconDiameter,
child: Opacity(
opacity: shouldDimNode ? 0.5 : 1.0,
child: CameraMapMarker(
@@ -72,12 +72,12 @@ class CameraRefreshController {
}
final zoom = controller.mapController.camera.zoom;
if (zoom < kNodeMinZoomLevel) {
if (zoom < dev.kNodeMinZoomLevel) {
// Show a snackbar-style bubble warning
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
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),
),
);
+7 -7
View File
@@ -112,11 +112,11 @@ class DirectionConesBuilder {
bool isSession = false,
bool isActiveDirection = true,
}) {
final halfAngle = kDirectionConeHalfAngle;
final halfAngle = dev.kDirectionConeHalfAngle;
// Calculate pixel-based radii
final outerRadiusPx = kNodeIconDiameter + (kNodeIconDiameter * kDirectionConeBaseLength);
final innerRadiusPx = kNodeIconDiameter + (2 * getNodeRingThickness(context));
final outerRadiusPx = dev.kNodeIconDiameter + (dev.kNodeIconDiameter * dev.kDirectionConeBaseLength);
final innerRadiusPx = dev.kNodeIconDiameter + (2 * getNodeRingThickness(context));
// Convert pixels to coordinate distances with zoom scaling
final pixelToCoordinate = 0.00001 * math.pow(2, 15 - zoom);
@@ -150,15 +150,15 @@ class DirectionConesBuilder {
}
// Adjust opacity based on direction state
double opacity = kDirectionConeOpacity;
double opacity = dev.kDirectionConeOpacity;
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(
points: points,
color: kDirectionConeColor.withOpacity(opacity),
borderColor: kDirectionConeColor,
color: dev.kDirectionConeColor.withOpacity(opacity),
borderColor: dev.kDirectionConeColor,
borderStrokeWidth: getDirectionConeBorderWidth(context),
);
}
+5 -5
View File
@@ -60,7 +60,7 @@ class GpsController {
controller.animateTo(
dest: _currentLatLng!,
zoom: controller.mapController.camera.zoom,
duration: kFollowMeAnimationDuration,
duration: dev.kFollowMeAnimationDuration,
curve: Curves.easeOut,
);
onMapMovedProgrammatically?.call();
@@ -70,7 +70,7 @@ class GpsController {
dest: _currentLatLng!,
zoom: controller.mapController.camera.zoom,
rotation: 0.0,
duration: kFollowMeAnimationDuration,
duration: dev.kFollowMeAnimationDuration,
curve: Curves.easeOut,
);
onMapMovedProgrammatically?.call();
@@ -123,7 +123,7 @@ class GpsController {
dest: latLng,
zoom: controller.mapController.camera.zoom,
rotation: controller.mapController.camera.rotation,
duration: kFollowMeAnimationDuration,
duration: dev.kFollowMeAnimationDuration,
curve: Curves.easeOut,
);
@@ -135,14 +135,14 @@ class GpsController {
final speed = position.speed; // Speed in m/s
// 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;
controller.animateTo(
dest: latLng,
zoom: controller.mapController.camera.zoom,
rotation: rotation,
duration: kFollowMeAnimationDuration,
duration: dev.kFollowMeAnimationDuration,
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
Positioned(
left: leftPositionWithSafeArea(10, safeArea),
bottom: bottomPositionFromButtonBar(kZoomIndicatorSpacingAboveButtonBar, safeArea.bottom),
bottom: bottomPositionFromButtonBar(dev.kZoomIndicatorSpacingAboveButtonBar, safeArea.bottom),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2),
decoration: BoxDecoration(
@@ -125,7 +125,7 @@ class MapOverlays extends StatelessWidget {
// Attribution overlay, positioned relative to button bar with left safe area
if (attribution != null)
Positioned(
bottom: bottomPositionFromButtonBar(kAttributionSpacingAboveButtonBar, safeArea.bottom),
bottom: bottomPositionFromButtonBar(dev.kAttributionSpacingAboveButtonBar, safeArea.bottom),
left: leftPositionWithSafeArea(10, safeArea),
child: GestureDetector(
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
Positioned(
bottom: bottomPositionFromButtonBar(kZoomControlsSpacingAboveButtonBar, safeArea.bottom),
bottom: bottomPositionFromButtonBar(dev.kZoomControlsSpacingAboveButtonBar, safeArea.bottom),
right: rightPositionWithSafeArea(16, safeArea),
child: Consumer<AppState>(
builder: (context, appState, child) {
@@ -28,7 +28,7 @@ class SuspectedLocationMapMarker extends StatefulWidget {
class _SuspectedLocationMapMarkerState extends State<SuspectedLocationMapMarker> {
Timer? _tapTimer;
// From dev_config.dart for build-time parameters
static const Duration tapTimeout = kMarkerTapTimeout;
static final Duration tapTimeout = dev.dev.kMarkerTapTimeout;
void _onTap() {
_tapTimer = Timer(tapTimeout, () {
@@ -47,7 +47,7 @@ class _SuspectedLocationMapMarkerState extends State<SuspectedLocationMapMarker>
void _onDoubleTap() {
_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
+15 -15
View File
@@ -60,7 +60,7 @@ class MapView extends StatefulWidget {
class MapViewState extends State<MapView> {
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 _mapPositionDebounce = Debouncer(const Duration(milliseconds: 1000));
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
if (uploadMode == UploadMode.sandbox) {
return kOsmApiMinZoomLevel;
return dev.kOsmApiMinZoomLevel;
} 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
if (editSession?.originalNode.isConstrained == true && editSession?.extractFromWay != true) {
// Constrained node (not extracting): only allow pinch zoom and rotation, disable ALL panning
return const InteractionOptions(
return InteractionOptions(
enableMultiFingerGestureRace: true,
flags: InteractiveFlag.pinchZoom | InteractiveFlag.rotate,
scrollWheelVelocity: kScrollWheelVelocity,
pinchZoomThreshold: kPinchZoomThreshold,
pinchMoveThreshold: kPinchMoveThreshold,
scrollWheelVelocity: dev.kScrollWheelVelocity,
pinchZoomThreshold: dev.kPinchZoomThreshold,
pinchMoveThreshold: dev.kPinchMoveThreshold,
);
}
// Normal case: all interactions allowed with gesture race to prevent accidental rotation during zoom
return const InteractionOptions(
return InteractionOptions(
enableMultiFingerGestureRace: true,
flags: InteractiveFlag.doubleTapDragZoom |
InteractiveFlag.doubleTapZoom |
@@ -287,9 +287,9 @@ class MapViewState extends State<MapView> {
InteractiveFlag.pinchZoom |
InteractiveFlag.rotate |
InteractiveFlag.scrollWheelZoom,
scrollWheelVelocity: kScrollWheelVelocity,
pinchZoomThreshold: kPinchZoomThreshold,
pinchMoveThreshold: kPinchMoveThreshold,
scrollWheelVelocity: dev.kScrollWheelVelocity,
pinchZoomThreshold: dev.kPinchZoomThreshold,
pinchMoveThreshold: dev.kPinchMoveThreshold,
);
}
@@ -506,8 +506,8 @@ class MapViewState extends State<MapView> {
centerMarkers.add(
Marker(
point: center,
width: kNodeIconDiameter,
height: kNodeIconDiameter,
width: dev.kNodeIconDiameter,
height: dev.kNodeIconDiameter,
child: CameraIcon(
type: editSession != null ? CameraIconType.editing : CameraIconType.mock,
),
@@ -691,7 +691,7 @@ class MapViewState extends State<MapView> {
alignment: Alignment.bottomLeft,
padding: EdgeInsets.only(
left: leftPositionWithSafeArea(8, safeArea),
bottom: bottomPositionFromButtonBar(kScaleBarSpacingAboveButtonBar, safeArea.bottom)
bottom: bottomPositionFromButtonBar(dev.kScaleBarSpacingAboveButtonBar, safeArea.bottom)
),
textStyle: TextStyle(color: Colors.black, fontWeight: FontWeight.bold),
lineColor: Colors.black,
@@ -753,7 +753,7 @@ class MapViewState extends State<MapView> {
if (originalCoord != null) {
lines.add(Polyline(
points: [originalCoord, node.coord],
color: kNodeRingColorPending,
color: dev.kNodeRingColorPending,
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
description: Map public surveillance infrastructure with OpenStreetMap
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:
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"