mirror of
https://github.com/FoggedLens/deflock-app.git
synced 2026-04-02 02:00:28 +02:00
Compare commits
1 Commits
fix/node-r
...
secret_dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
999a918062 |
@@ -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 = true; // 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
|
||||||
@@ -151,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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
lib/screens/developer_settings_screen.dart
Normal file
127
lib/screens/developer_settings_screen.dart
Normal 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(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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: [
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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,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');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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)',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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>''';
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -81,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(
|
||||||
@@ -95,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(
|
||||||
@@ -109,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),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -83,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(
|
||||||
@@ -97,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(
|
||||||
@@ -111,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),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -160,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 &&
|
||||||
@@ -220,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')),
|
||||||
@@ -234,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),
|
||||||
@@ -266,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(
|
||||||
|
|||||||
@@ -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),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|||||||
150
scripts/update_dev_config.py
Normal file
150
scripts/update_dev_config.py
Normal 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
scripts/update_dev_config_references.sh
Executable file
12
scripts/update_dev_config_references.sh
Executable 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"
|
||||||
Reference in New Issue
Block a user