Skip to content

Commit 58e6246

Browse files
committed
Add map style preference and update related functionality
1 parent 21e77eb commit 58e6246

File tree

7 files changed

+148
-51
lines changed

7 files changed

+148
-51
lines changed

android/local.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ sdk.dir=/opt/homebrew/share/android-commandlinetools
22
flutter.sdk=/opt/homebrew/share/flutter
33
flutter.buildMode=release
44
flutter.versionName=1.0.0
5-
flutter.versionCode=1769520926
5+
flutter.versionCode=1769521372

ios/Flutter/Generated.xcconfig

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22
FLUTTER_ROOT=/opt/homebrew/share/flutter
33
FLUTTER_APPLICATION_PATH=/Users/schnobbc/Documents/Github/MeshMapper_Flutter_App
44
COCOAPODS_PARALLEL_CODE_SIGN=true
5-
FLUTTER_TARGET=lib/main.dart
5+
FLUTTER_TARGET=/Users/schnobbc/Documents/Github/MeshMapper_Flutter_App/lib/main.dart
66
FLUTTER_BUILD_DIR=build
77
FLUTTER_BUILD_NAME=1.0.0
8-
FLUTTER_BUILD_NUMBER=1769401658
8+
FLUTTER_BUILD_NUMBER=1
99
EXCLUDED_ARCHS[sdk=iphonesimulator*]=i386
1010
EXCLUDED_ARCHS[sdk=iphoneos*]=armv7
11-
DART_DEFINES=QVBQX1ZFUlNJT049QVBQLTE3Njk0MDE2NTg=,RkxVVFRFUl9WRVJTSU9OPTMuMzguNw==,RkxVVFRFUl9DSEFOTkVMPXN0YWJsZQ==,RkxVVFRFUl9HSVRfVVJMPWh0dHBzOi8vZ2l0aHViLmNvbS9mbHV0dGVyL2ZsdXR0ZXIuZ2l0,RkxVVFRFUl9GUkFNRVdPUktfUkVWSVNJT049M2I2MmVmYzJhMw==,RkxVVFRFUl9FTkdJTkVfUkVWSVNJT049NzhmYzMwMTJlNA==,RkxVVFRFUl9EQVJUX1ZFUlNJT049My4xMC43
11+
DART_DEFINES=RkxVVFRFUl9WRVJTSU9OPTMuMzguNw==,RkxVVFRFUl9DSEFOTkVMPXN0YWJsZQ==,RkxVVFRFUl9HSVRfVVJMPWh0dHBzOi8vZ2l0aHViLmNvbS9mbHV0dGVyL2ZsdXR0ZXIuZ2l0,RkxVVFRFUl9GUkFNRVdPUktfUkVWSVNJT049M2I2MmVmYzJhMw==,RkxVVFRFUl9FTkdJTkVfUkVWSVNJT049NzhmYzMwMTJlNA==,RkxVVFRFUl9EQVJUX1ZFUlNJT049My4xMC43
1212
DART_OBFUSCATION=false
13-
TRACK_WIDGET_CREATION=false
14-
TREE_SHAKE_ICONS=true
13+
TRACK_WIDGET_CREATION=true
14+
TREE_SHAKE_ICONS=false
1515
PACKAGE_CONFIG=/Users/schnobbc/Documents/Github/MeshMapper_Flutter_App/.dart_tool/package_config.json

ios/Flutter/flutter_export_environment.sh

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@
33
export "FLUTTER_ROOT=/opt/homebrew/share/flutter"
44
export "FLUTTER_APPLICATION_PATH=/Users/schnobbc/Documents/Github/MeshMapper_Flutter_App"
55
export "COCOAPODS_PARALLEL_CODE_SIGN=true"
6-
export "FLUTTER_TARGET=lib/main.dart"
6+
export "FLUTTER_TARGET=/Users/schnobbc/Documents/Github/MeshMapper_Flutter_App/lib/main.dart"
77
export "FLUTTER_BUILD_DIR=build"
88
export "FLUTTER_BUILD_NAME=1.0.0"
9-
export "FLUTTER_BUILD_NUMBER=1769401658"
10-
export "DART_DEFINES=QVBQX1ZFUlNJT049QVBQLTE3Njk0MDE2NTg=,RkxVVFRFUl9WRVJTSU9OPTMuMzguNw==,RkxVVFRFUl9DSEFOTkVMPXN0YWJsZQ==,RkxVVFRFUl9HSVRfVVJMPWh0dHBzOi8vZ2l0aHViLmNvbS9mbHV0dGVyL2ZsdXR0ZXIuZ2l0,RkxVVFRFUl9GUkFNRVdPUktfUkVWSVNJT049M2I2MmVmYzJhMw==,RkxVVFRFUl9FTkdJTkVfUkVWSVNJT049NzhmYzMwMTJlNA==,RkxVVFRFUl9EQVJUX1ZFUlNJT049My4xMC43"
9+
export "FLUTTER_BUILD_NUMBER=1"
10+
export "DART_DEFINES=RkxVVFRFUl9WRVJTSU9OPTMuMzguNw==,RkxVVFRFUl9DSEFOTkVMPXN0YWJsZQ==,RkxVVFRFUl9HSVRfVVJMPWh0dHBzOi8vZ2l0aHViLmNvbS9mbHV0dGVyL2ZsdXR0ZXIuZ2l0,RkxVVFRFUl9GUkFNRVdPUktfUkVWSVNJT049M2I2MmVmYzJhMw==,RkxVVFRFUl9FTkdJTkVfUkVWSVNJT049NzhmYzMwMTJlNA==,RkxVVFRFUl9EQVJUX1ZFUlNJT049My4xMC43"
1111
export "DART_OBFUSCATION=false"
12-
export "TRACK_WIDGET_CREATION=false"
13-
export "TREE_SHAKE_ICONS=true"
12+
export "TRACK_WIDGET_CREATION=true"
13+
export "TREE_SHAKE_ICONS=false"
1414
export "PACKAGE_CONFIG=/Users/schnobbc/Documents/Github/MeshMapper_Flutter_App/.dart_tool/package_config.json"

lib/models/user_preferences.dart

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ class UserPreferences {
4040
/// Developer mode enabled (unlocked by tapping version 7 times)
4141
final bool developerModeEnabled;
4242

43+
/// Map tile style (dark, light, satellite)
44+
final String mapStyle;
45+
4346
const UserPreferences({
4447
this.powerLevel = 0.3,
4548
this.txPower = 22,
@@ -54,6 +57,7 @@ class UserPreferences {
5457
this.iataCode = 'YOW',
5558
this.backgroundModeEnabled = false,
5659
this.developerModeEnabled = false,
60+
this.mapStyle = 'dark',
5761
});
5862

5963
/// Create from JSON (for persistence)
@@ -72,6 +76,7 @@ class UserPreferences {
7276
iataCode: (json['iataCode'] as String?) ?? 'YOW',
7377
backgroundModeEnabled: (json['backgroundModeEnabled'] as bool?) ?? false,
7478
developerModeEnabled: (json['developerModeEnabled'] as bool?) ?? false,
79+
mapStyle: (json['mapStyle'] as String?) ?? 'dark',
7580
);
7681
}
7782

@@ -91,6 +96,7 @@ class UserPreferences {
9196
'iataCode': iataCode,
9297
'backgroundModeEnabled': backgroundModeEnabled,
9398
'developerModeEnabled': developerModeEnabled,
99+
'mapStyle': mapStyle,
94100
};
95101
}
96102

@@ -109,6 +115,7 @@ class UserPreferences {
109115
String? iataCode,
110116
bool? backgroundModeEnabled,
111117
bool? developerModeEnabled,
118+
String? mapStyle,
112119
}) {
113120
return UserPreferences(
114121
powerLevel: powerLevel ?? this.powerLevel,
@@ -124,6 +131,7 @@ class UserPreferences {
124131
iataCode: iataCode ?? this.iataCode,
125132
backgroundModeEnabled: backgroundModeEnabled ?? this.backgroundModeEnabled,
126133
developerModeEnabled: developerModeEnabled ?? this.developerModeEnabled,
134+
mapStyle: mapStyle ?? this.mapStyle,
127135
);
128136
}
129137

@@ -164,7 +172,8 @@ class UserPreferences {
164172
other.offlineMode == offlineMode &&
165173
other.iataCode == iataCode &&
166174
other.backgroundModeEnabled == backgroundModeEnabled &&
167-
other.developerModeEnabled == developerModeEnabled;
175+
other.developerModeEnabled == developerModeEnabled &&
176+
other.mapStyle == mapStyle;
168177
}
169178

170179
@override
@@ -182,6 +191,7 @@ class UserPreferences {
182191
iataCode,
183192
backgroundModeEnabled,
184193
developerModeEnabled,
194+
mapStyle,
185195
);
186196
}
187197
}

lib/providers/app_state_provider.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1649,6 +1649,14 @@ class AppStateProvider extends ChangeNotifier {
16491649
_savePreferences();
16501650
}
16511651

1652+
/// Set map style (dark, light, satellite) and persist
1653+
void setMapStyle(String style) {
1654+
_preferences = _preferences.copyWith(mapStyle: style);
1655+
debugLog('[MAP] Map style set to $style');
1656+
notifyListeners();
1657+
_savePreferences();
1658+
}
1659+
16521660
/// Toggle sound notifications on/off
16531661
Future<void> toggleSoundEnabled() async {
16541662
await _audioService.toggle();

lib/screens/home_screen.dart

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,15 @@ class _HomeScreenState extends State<HomeScreen> {
2020
bool _showControlPanel = true;
2121
bool _isControlsMinimized = false;
2222

23+
/// Calculate the current control panel height for map centering offset
24+
double _getControlPanelHeight() {
25+
if (!_showControlPanel) return 0;
26+
// Approximate heights including Card margins (8px * 2 = 16px)
27+
// Minimized: Row padding (12) + content (~32) + margin (16) = ~60px
28+
// Expanded: ListTile (56) + Divider (1) + ConnectionPanel (~100) + PingControls (~140) + margin (16) = ~320px
29+
return _isControlsMinimized ? 60 : 320;
30+
}
31+
2332
@override
2433
Widget build(BuildContext context) {
2534
final appState = context.watch<AppStateProvider>();
@@ -93,10 +102,10 @@ class _HomeScreenState extends State<HomeScreen> {
93102
return Stack(
94103
children: [
95104
// Map fills entire screen
96-
const Column(
105+
Column(
97106
children: [
98-
StatusBar(),
99-
Expanded(child: MapWidget()),
107+
const StatusBar(),
108+
Expanded(child: MapWidget(bottomPaddingPixels: _getControlPanelHeight())),
100109
],
101110
),
102111

lib/widgets/map_widget.dart

Lines changed: 106 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,19 @@ enum MapStyle {
2121
}
2222

2323
extension MapStyleExtension on MapStyle {
24+
/// Convert from stored string preference to MapStyle enum
25+
static MapStyle fromString(String value) {
26+
switch (value) {
27+
case 'light':
28+
return MapStyle.light;
29+
case 'satellite':
30+
return MapStyle.satellite;
31+
case 'dark':
32+
default:
33+
return MapStyle.dark;
34+
}
35+
}
36+
2437
String get label {
2538
switch (this) {
2639
case MapStyle.dark:
@@ -81,15 +94,18 @@ final class SilentCancellableNetworkTileProvider extends CancellableNetworkTileP
8194
/// Map widget with TX/RX markers
8295
/// Uses flutter_map with OpenStreetMap tiles
8396
class MapWidget extends StatefulWidget {
84-
const MapWidget({super.key});
97+
/// Bottom padding in pixels to account for overlays (e.g., control panel)
98+
/// The map will offset its center point upward by half this value
99+
final double bottomPaddingPixels;
100+
101+
const MapWidget({super.key, this.bottomPaddingPixels = 0});
85102

86103
@override
87104
State<MapWidget> createState() => _MapWidgetState();
88105
}
89106

90107
class _MapWidgetState extends State<MapWidget> with TickerProviderStateMixin {
91108
final MapController _mapController = MapController();
92-
MapStyle _mapStyle = MapStyle.dark;
93109

94110
// Auto-follow GPS like a navigation app
95111
bool _autoFollow = true;
@@ -132,6 +148,23 @@ class _MapWidgetState extends State<MapWidget> with TickerProviderStateMixin {
132148
super.dispose();
133149
}
134150

151+
@override
152+
void didUpdateWidget(MapWidget oldWidget) {
153+
super.didUpdateWidget(oldWidget);
154+
// When bottom padding changes (panel opened/closed/minimized), re-center if auto-following
155+
if (widget.bottomPaddingPixels != oldWidget.bottomPaddingPixels &&
156+
_autoFollow &&
157+
_isMapReady &&
158+
_lastGpsPosition != null) {
159+
WidgetsBinding.instance.addPostFrameCallback((_) {
160+
if (mounted && _autoFollow && _lastGpsPosition != null) {
161+
final adjustedPosition = _offsetPositionForPadding(_lastGpsPosition!, widget.bottomPaddingPixels);
162+
_animateToPosition(adjustedPosition);
163+
}
164+
});
165+
}
166+
}
167+
135168
/// Smoothly animate the map to a new position
136169
void _animateToPosition(LatLng target) {
137170
if (!_isMapReady || !mounted) return;
@@ -294,6 +327,30 @@ class _MapWidgetState extends State<MapWidget> with TickerProviderStateMixin {
294327
_rotationAnimationController!.forward();
295328
}
296329

330+
/// Offset a lat/lon position by screen pixels (to account for UI overlays)
331+
/// Shifts the map center down so the GPS marker appears centered in the
332+
/// visible map area above the control panel
333+
LatLng _offsetPositionForPadding(LatLng position, double bottomPadding) {
334+
if (bottomPadding <= 0 || !_isMapReady) return position;
335+
336+
// Shift map center down by half the bottom padding
337+
// This makes the GPS marker appear higher (centered in visible area)
338+
final offsetPixels = bottomPadding / 2;
339+
340+
// Get meters per pixel at current zoom
341+
// Approx: 40075km / (256 * 2^zoom) at equator, adjusted by cos(lat)
342+
final zoom = _mapController.camera.zoom;
343+
final metersPerPixel = 40075000 / (256 * math.pow(2, zoom)) *
344+
math.cos(position.latitude * math.pi / 180);
345+
346+
// Convert pixel offset to meters, then to latitude offset
347+
// Subtract latitude to move map center south, making marker appear higher on screen
348+
final meterOffset = offsetPixels * metersPerPixel;
349+
final latOffset = meterOffset / 111000; // ~111km per degree latitude
350+
351+
return LatLng(position.latitude - latOffset, position.longitude);
352+
}
353+
297354
@override
298355
Widget build(BuildContext context) {
299356
final appState = context.watch<AppStateProvider>();
@@ -317,7 +374,9 @@ class _MapWidgetState extends State<MapWidget> with TickerProviderStateMixin {
317374
// Use post frame callback to avoid build-during-build issues
318375
WidgetsBinding.instance.addPostFrameCallback((_) {
319376
if (mounted && _autoFollow) {
320-
_animateToPosition(newPosition); // Smooth animation instead of jump
377+
// Apply offset for bottom padding when control panel is open
378+
final adjustedPosition = _offsetPositionForPadding(newPosition, widget.bottomPaddingPixels);
379+
_animateToPosition(adjustedPosition); // Smooth animation instead of jump
321380
}
322381
});
323382
}
@@ -348,9 +407,12 @@ class _MapWidgetState extends State<MapWidget> with TickerProviderStateMixin {
348407
// Disable auto-follow when navigating from log
349408
_autoFollow = false;
350409
// Navigate to the coordinates with close zoom (18 = street level view)
410+
// Apply offset for bottom padding when control panel is open
351411
WidgetsBinding.instance.addPostFrameCallback((_) {
352412
if (mounted) {
353-
_animateToPositionWithZoom(LatLng(target.lat, target.lon), 18.0);
413+
final targetPosition = LatLng(target.lat, target.lon);
414+
final adjustedPosition = _offsetPositionForPadding(targetPosition, widget.bottomPaddingPixels);
415+
_animateToPositionWithZoom(adjustedPosition, 18.0);
354416
}
355417
});
356418
}
@@ -401,14 +463,19 @@ class _MapWidgetState extends State<MapWidget> with TickerProviderStateMixin {
401463
},
402464
),
403465
children: [
404-
// Tile layer (dynamic based on selected style)
405-
TileLayer(
406-
urlTemplate: _mapStyle.urlTemplate,
407-
subdomains: _mapStyle.subdomains ?? const [],
408-
userAgentPackageName: 'com.meshmapper.app',
409-
maxZoom: 19,
410-
retinaMode: RetinaMode.isHighDensity(context), // Enable high-res tiles on retina displays
411-
tileProvider: SilentCancellableNetworkTileProvider(), // Silently handles tile errors
466+
// Tile layer (dynamic based on selected style from preferences)
467+
Builder(
468+
builder: (context) {
469+
final mapStyle = MapStyleExtension.fromString(appState.preferences.mapStyle);
470+
return TileLayer(
471+
urlTemplate: mapStyle.urlTemplate,
472+
subdomains: mapStyle.subdomains ?? const [],
473+
userAgentPackageName: 'com.meshmapper.app',
474+
maxZoom: 19,
475+
retinaMode: RetinaMode.isHighDensity(context), // Enable high-res tiles on retina displays
476+
tileProvider: SilentCancellableNetworkTileProvider(), // Silently handles tile errors
477+
);
478+
},
412479
),
413480

414481
// MeshMapper coverage overlay (only when zone code available and overlay enabled)
@@ -521,6 +588,7 @@ class _MapWidgetState extends State<MapWidget> with TickerProviderStateMixin {
521588

522589
/// Map controls (top-right corner) - Apple Maps style
523590
Widget _buildMapControls(AppStateProvider appState) {
591+
final mapStyle = MapStyleExtension.fromString(appState.preferences.mapStyle);
524592
return Container(
525593
decoration: BoxDecoration(
526594
color: Colors.black.withValues(alpha: 0.7),
@@ -531,10 +599,24 @@ class _MapWidgetState extends State<MapWidget> with TickerProviderStateMixin {
531599
children: [
532600
// Map style toggle
533601
_buildControlButton(
534-
icon: _mapStyle.icon,
535-
tooltip: 'Map Style: ${_mapStyle.label}',
536-
onPressed: _cycleMapStyle,
602+
icon: mapStyle.icon,
603+
tooltip: 'Map Style: ${mapStyle.label}',
604+
onPressed: () => _cycleMapStyle(appState),
537605
),
606+
// MeshMapper overlay toggle (only show when zone code available)
607+
if (appState.zoneCode != null) ...[
608+
Container(
609+
height: 1,
610+
width: 32,
611+
color: Colors.white24,
612+
),
613+
_buildControlButton(
614+
icon: Icons.layers,
615+
tooltip: _showMeshMapperOverlay ? 'Hide Coverage Overlay' : 'Show Coverage Overlay',
616+
onPressed: _toggleMeshMapperOverlay,
617+
isActive: _showMeshMapperOverlay,
618+
),
619+
],
538620
Container(
539621
height: 1,
540622
width: 32,
@@ -571,20 +653,6 @@ class _MapWidgetState extends State<MapWidget> with TickerProviderStateMixin {
571653
onPressed: _toggleRotationLock,
572654
isActive: _rotationLocked,
573655
),
574-
// MeshMapper overlay toggle (only show when zone code available)
575-
if (appState.zoneCode != null) ...[
576-
Container(
577-
height: 1,
578-
width: 32,
579-
color: Colors.white24,
580-
),
581-
_buildControlButton(
582-
icon: Icons.layers,
583-
tooltip: _showMeshMapperOverlay ? 'Hide Coverage Overlay' : 'Show Coverage Overlay',
584-
onPressed: _toggleMeshMapperOverlay,
585-
isActive: _showMeshMapperOverlay,
586-
),
587-
],
588656
// Legend button (always visible)
589657
Container(
590658
height: 1,
@@ -628,12 +696,12 @@ class _MapWidgetState extends State<MapWidget> with TickerProviderStateMixin {
628696
);
629697
}
630698

631-
void _cycleMapStyle() {
632-
setState(() {
633-
const styles = MapStyle.values;
634-
final currentIndex = styles.indexOf(_mapStyle);
635-
_mapStyle = styles[(currentIndex + 1) % styles.length];
636-
});
699+
void _cycleMapStyle(AppStateProvider appState) {
700+
const styles = MapStyle.values;
701+
final currentStyle = MapStyleExtension.fromString(appState.preferences.mapStyle);
702+
final currentIndex = styles.indexOf(currentStyle);
703+
final newStyle = styles[(currentIndex + 1) % styles.length];
704+
appState.setMapStyle(newStyle.name);
637705
}
638706

639707
void _centerOnPosition() {
@@ -656,7 +724,9 @@ class _MapWidgetState extends State<MapWidget> with TickerProviderStateMixin {
656724
_autoFollow = true;
657725
_lastGpsPosition = targetPosition;
658726
});
659-
_animateToPositionWithZoom(targetPosition, 16.0); // Smooth animation with zoom
727+
// Apply offset for bottom padding when control panel is open
728+
final adjustedPosition = _offsetPositionForPadding(targetPosition, widget.bottomPaddingPixels);
729+
_animateToPositionWithZoom(adjustedPosition, 16.0); // Smooth animation with zoom
660730
}
661731
}
662732

0 commit comments

Comments
 (0)