Skip to content

Commit 5f8b6db

Browse files
committed
feat: Add theme toggle and improve UI consistency
- Implemented a theme toggle in the settings screen to switch between light and dark modes. - Enhanced the bug report dialog by adding an optional username field, auto-populating it from the remembered device name. - Updated connection panel UI to use color scheme for better visibility in dark mode. - Improved map widget behavior for initial GPS zoom and added logging for better debugging. - Refined color usage across various widgets to align with the current theme, ensuring consistent appearance in both light and dark modes. - Adjusted button colors and states to enhance user experience during interactions.
1 parent 585fb51 commit 5f8b6db

File tree

11 files changed

+521
-159
lines changed

11 files changed

+521
-159
lines changed

android/local.properties

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
sdk.dir=/opt/homebrew/share/android-commandlinetools
22
flutter.sdk=/opt/homebrew/share/flutter
3-
flutter.buildMode=debug
3+
flutter.buildMode=release
44
flutter.versionName=1.0.0
5-
flutter.versionCode=1
5+
flutter.versionCode=1770007371

ios/Flutter/Generated.xcconfig

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ COCOAPODS_PARALLEL_CODE_SIGN=true
55
FLUTTER_TARGET=lib/main.dart
66
FLUTTER_BUILD_DIR=build
77
FLUTTER_BUILD_NAME=1.0.0
8-
FLUTTER_BUILD_NUMBER=1769992189
8+
FLUTTER_BUILD_NUMBER=1770007371
99
EXCLUDED_ARCHS[sdk=iphonesimulator*]=i386
1010
EXCLUDED_ARCHS[sdk=iphoneos*]=armv7
11-
DART_DEFINES=QVBQX1ZFUlNJT049QVBQLTE3Njk5OTIxODk=,RkxVVFRFUl9WRVJTSU9OPTMuMzguOQ==,RkxVVFRFUl9DSEFOTkVMPXN0YWJsZQ==,RkxVVFRFUl9HSVRfVVJMPWh0dHBzOi8vZ2l0aHViLmNvbS9mbHV0dGVyL2ZsdXR0ZXIuZ2l0,RkxVVFRFUl9GUkFNRVdPUktfUkVWSVNJT049NjczMjNkZTI4NQ==,RkxVVFRFUl9FTkdJTkVfUkVWSVNJT049NTg3YzE4Zjg3Mw==,RkxVVFRFUl9EQVJUX1ZFUlNJT049My4xMC44
11+
DART_DEFINES=QVBQX1ZFUlNJT049QVBQLTE3NzAwMDczNzE=,RkxVVFRFUl9WRVJTSU9OPTMuMzguOQ==,RkxVVFRFUl9DSEFOTkVMPXN0YWJsZQ==,RkxVVFRFUl9HSVRfVVJMPWh0dHBzOi8vZ2l0aHViLmNvbS9mbHV0dGVyL2ZsdXR0ZXIuZ2l0,RkxVVFRFUl9GUkFNRVdPUktfUkVWSVNJT049NjczMjNkZTI4NQ==,RkxVVFRFUl9FTkdJTkVfUkVWSVNJT049NTg3YzE4Zjg3Mw==,RkxVVFRFUl9EQVJUX1ZFUlNJT049My4xMC44
1212
DART_OBFUSCATION=false
1313
TRACK_WIDGET_CREATION=false
1414
TREE_SHAKE_ICONS=true

ios/Flutter/flutter_export_environment.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ export "COCOAPODS_PARALLEL_CODE_SIGN=true"
66
export "FLUTTER_TARGET=lib/main.dart"
77
export "FLUTTER_BUILD_DIR=build"
88
export "FLUTTER_BUILD_NAME=1.0.0"
9-
export "FLUTTER_BUILD_NUMBER=1769992189"
10-
export "DART_DEFINES=QVBQX1ZFUlNJT049QVBQLTE3Njk5OTIxODk=,RkxVVFRFUl9WRVJTSU9OPTMuMzguOQ==,RkxVVFRFUl9DSEFOTkVMPXN0YWJsZQ==,RkxVVFRFUl9HSVRfVVJMPWh0dHBzOi8vZ2l0aHViLmNvbS9mbHV0dGVyL2ZsdXR0ZXIuZ2l0,RkxVVFRFUl9GUkFNRVdPUktfUkVWSVNJT049NjczMjNkZTI4NQ==,RkxVVFRFUl9FTkdJTkVfUkVWSVNJT049NTg3YzE4Zjg3Mw==,RkxVVFRFUl9EQVJUX1ZFUlNJT049My4xMC44"
9+
export "FLUTTER_BUILD_NUMBER=1770007371"
10+
export "DART_DEFINES=QVBQX1ZFUlNJT049QVBQLTE3NzAwMDczNzE=,RkxVVFRFUl9WRVJTSU9OPTMuMzguOQ==,RkxVVFRFUl9DSEFOTkVMPXN0YWJsZQ==,RkxVVFRFUl9HSVRfVVJMPWh0dHBzOi8vZ2l0aHViLmNvbS9mbHV0dGVyL2ZsdXR0ZXIuZ2l0,RkxVVFRFUl9GUkFNRVdPUktfUkVWSVNJT049NjczMjNkZTI4NQ==,RkxVVFRFUl9FTkdJTkVfUkVWSVNJT049NTg3YzE4Zjg3Mw==,RkxVVFRFUl9EQVJUX1ZFUlNJT049My4xMC44"
1111
export "DART_OBFUSCATION=false"
1212
export "TRACK_WIDGET_CREATION=false"
1313
export "TREE_SHAKE_ICONS=true"

lib/main.dart

Lines changed: 160 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ void main() async {
3535
await Hive.initFlutter();
3636
debugLog('[APP] Hive initialized');
3737

38+
// Load theme preference BEFORE runApp to avoid flash of wrong theme
39+
final initialThemeMode = await _loadInitialThemeMode();
40+
debugLog('[APP] Initial theme mode: $initialThemeMode');
41+
3842
// Register noise floor session adapters before opening any boxes
3943
if (!Hive.isAdapterRegistered(10)) {
4044
Hive.registerAdapter(NoiseFloorSampleAdapter());
@@ -58,7 +62,24 @@ void main() async {
5862
// Initialize background service for continuous wardriving (mobile only)
5963
await BackgroundServiceManager.initialize();
6064

61-
runApp(const MeshMapperApp());
65+
runApp(MeshMapperApp(initialThemeMode: initialThemeMode));
66+
}
67+
68+
/// Load theme mode from Hive before app starts to avoid flash of wrong theme
69+
Future<String> _loadInitialThemeMode() async {
70+
try {
71+
final box = await Hive.openBox('user_preferences');
72+
final json = box.get('preferences');
73+
if (json != null && json is Map) {
74+
final themeMode = json['themeMode'] as String?;
75+
if (themeMode != null) {
76+
return themeMode;
77+
}
78+
}
79+
} catch (e) {
80+
debugLog('[APP] Failed to load initial theme: $e');
81+
}
82+
return 'dark'; // Default to dark mode
6283
}
6384

6485
/// Request all required permissions on app startup
@@ -128,14 +149,50 @@ Future<void> _requestAndroidPermissions() async {
128149
}
129150
}
130151

152+
// Dark theme - Tailwind Slate palette
153+
const darkColorScheme = ColorScheme.dark(
154+
primary: Color(0xFF059669), // emerald-600 (main actions)
155+
onPrimary: Colors.white,
156+
secondary: Color(0xFF0284C7), // sky-600 (TX ping)
157+
onSecondary: Colors.white,
158+
tertiary: Color(0xFF4F46E5), // indigo-600 (auto modes)
159+
onTertiary: Colors.white,
160+
surface: Color(0xFF1E293B), // slate-800 (cards/panels)
161+
onSurface: Color(0xFFF1F5F9), // slate-100 (primary text)
162+
onSurfaceVariant: Color(0xFF94A3B8), // slate-400 (muted text)
163+
surfaceContainerHighest: Color(0xFF0F172A), // slate-900 (main bg)
164+
outline: Color(0xFF334155), // slate-700 (borders)
165+
error: Color(0xFFF87171), // red-400
166+
onError: Colors.white,
167+
);
168+
169+
// Light theme - Tailwind Slate palette (inverted)
170+
const lightColorScheme = ColorScheme.light(
171+
primary: Color(0xFF059669), // emerald-600
172+
onPrimary: Colors.white,
173+
secondary: Color(0xFF0284C7), // sky-600
174+
onSecondary: Colors.white,
175+
tertiary: Color(0xFF4F46E5), // indigo-600
176+
onTertiary: Colors.white,
177+
surface: Color(0xFFF8FAFC), // slate-50 (cards/panels)
178+
onSurface: Color(0xFF1E293B), // slate-800 (primary text)
179+
onSurfaceVariant: Color(0xFF64748B), // slate-500 (muted text)
180+
surfaceContainerHighest: Color(0xFFFFFFFF), // white (main bg)
181+
outline: Color(0xFFCBD5E1), // slate-300 (borders)
182+
error: Color(0xFFDC2626), // red-600
183+
onError: Colors.white,
184+
);
185+
131186
class MeshMapperApp extends StatelessWidget {
132-
const MeshMapperApp({super.key});
187+
final String initialThemeMode;
188+
189+
const MeshMapperApp({super.key, required this.initialThemeMode});
133190

134191
@override
135192
Widget build(BuildContext context) {
136193
// Create platform-appropriate Bluetooth service
137-
final BluetoothService bluetoothService = kIsWeb
138-
? WebBluetoothService()
194+
final BluetoothService bluetoothService = kIsWeb
195+
? WebBluetoothService()
139196
: MobileBluetoothService();
140197

141198
return MultiProvider(
@@ -144,55 +201,110 @@ class MeshMapperApp extends StatelessWidget {
144201
create: (_) => AppStateProvider(bluetoothService: bluetoothService),
145202
),
146203
],
147-
child: MaterialApp(
148-
title: 'MeshMapper',
149-
theme: ThemeData(
150-
colorScheme: const ColorScheme.dark(
151-
// Tailwind Slate palette
152-
primary: Color(0xFF059669), // emerald-600 (main actions)
153-
onPrimary: Colors.white,
154-
secondary: Color(0xFF0284C7), // sky-600 (TX ping)
155-
onSecondary: Colors.white,
156-
tertiary: Color(0xFF4F46E5), // indigo-600 (auto modes)
157-
onTertiary: Colors.white,
158-
surface: Color(0xFF1E293B), // slate-800 (cards/panels)
159-
onSurface: Color(0xFFF1F5F9), // slate-100 (primary text)
160-
onSurfaceVariant: Color(0xFF94A3B8), // slate-400 (muted text)
161-
surfaceContainerHighest: Color(0xFF0F172A), // slate-900 (main bg)
162-
outline: Color(0xFF334155), // slate-700 (borders)
163-
error: Color(0xFFF87171), // red-400
164-
onError: Colors.white,
165-
),
166-
scaffoldBackgroundColor: const Color(0xFF0F172A), // slate-900
167-
appBarTheme: const AppBarTheme(
168-
backgroundColor: Color(0xFF1E293B), // slate-800
169-
foregroundColor: Color(0xFFF1F5F9), // slate-100
170-
),
171-
cardTheme: CardThemeData(
172-
color: const Color(0xFF1E293B), // slate-800
173-
shape: RoundedRectangleBorder(
174-
borderRadius: BorderRadius.circular(12),
175-
side: const BorderSide(color: Color(0xFF334155)), // slate-700
204+
child: _ThemedApp(initialThemeMode: initialThemeMode),
205+
);
206+
}
207+
}
208+
209+
/// Separate widget to handle theme switching with Consumer
210+
/// Uses initialThemeMode on first build to avoid flash of wrong theme
211+
class _ThemedApp extends StatefulWidget {
212+
final String initialThemeMode;
213+
214+
const _ThemedApp({required this.initialThemeMode});
215+
216+
@override
217+
State<_ThemedApp> createState() => _ThemedAppState();
218+
}
219+
220+
class _ThemedAppState extends State<_ThemedApp> {
221+
bool _isFirstBuild = true;
222+
223+
@override
224+
Widget build(BuildContext context) {
225+
return Consumer<AppStateProvider>(
226+
builder: (context, appState, child) {
227+
String effectiveThemeMode;
228+
229+
if (_isFirstBuild) {
230+
// On first build, use the pre-loaded theme to avoid flash
231+
effectiveThemeMode = widget.initialThemeMode;
232+
// Schedule switching to provider-managed theme after first frame
233+
WidgetsBinding.instance.addPostFrameCallback((_) {
234+
if (mounted && _isFirstBuild) {
235+
setState(() => _isFirstBuild = false);
236+
}
237+
});
238+
} else {
239+
// After first build, always use provider's value for dynamic updates
240+
effectiveThemeMode = appState.preferences.themeMode;
241+
}
242+
243+
final isDarkMode = effectiveThemeMode == 'dark';
244+
245+
return MaterialApp(
246+
title: 'MeshMapper',
247+
theme: ThemeData(
248+
colorScheme: lightColorScheme,
249+
scaffoldBackgroundColor: const Color(0xFFF1F5F9), // slate-100
250+
appBarTheme: const AppBarTheme(
251+
backgroundColor: Color(0xFFF8FAFC), // slate-50
252+
foregroundColor: Color(0xFF1E293B), // slate-800
253+
),
254+
cardTheme: CardThemeData(
255+
color: const Color(0xFFF8FAFC), // slate-50
256+
shape: RoundedRectangleBorder(
257+
borderRadius: BorderRadius.circular(12),
258+
side: const BorderSide(color: Color(0xFFCBD5E1)), // slate-300
259+
),
176260
),
261+
dividerColor: const Color(0xFFCBD5E1), // slate-300
262+
snackBarTheme: SnackBarThemeData(
263+
backgroundColor: const Color(0xFF334155), // slate-700
264+
contentTextStyle: const TextStyle(
265+
color: Color(0xFFF1F5F9), // slate-100
266+
fontSize: 14,
267+
),
268+
shape: RoundedRectangleBorder(
269+
borderRadius: BorderRadius.circular(8),
270+
),
271+
behavior: SnackBarBehavior.floating,
272+
),
273+
useMaterial3: true,
177274
),
178-
dividerColor: const Color(0xFF334155), // slate-700
179-
snackBarTheme: SnackBarThemeData(
180-
backgroundColor: const Color(0xFF334155), // slate-700
181-
contentTextStyle: const TextStyle(
182-
color: Color(0xFFF1F5F9), // slate-100
183-
fontSize: 14,
275+
darkTheme: ThemeData(
276+
colorScheme: darkColorScheme,
277+
scaffoldBackgroundColor: const Color(0xFF0F172A), // slate-900
278+
appBarTheme: const AppBarTheme(
279+
backgroundColor: Color(0xFF1E293B), // slate-800
280+
foregroundColor: Color(0xFFF1F5F9), // slate-100
281+
),
282+
cardTheme: CardThemeData(
283+
color: const Color(0xFF1E293B), // slate-800
284+
shape: RoundedRectangleBorder(
285+
borderRadius: BorderRadius.circular(12),
286+
side: const BorderSide(color: Color(0xFF334155)), // slate-700
287+
),
184288
),
185-
shape: RoundedRectangleBorder(
186-
borderRadius: BorderRadius.circular(8),
289+
dividerColor: const Color(0xFF334155), // slate-700
290+
snackBarTheme: SnackBarThemeData(
291+
backgroundColor: const Color(0xFF334155), // slate-700
292+
contentTextStyle: const TextStyle(
293+
color: Color(0xFFF1F5F9), // slate-100
294+
fontSize: 14,
295+
),
296+
shape: RoundedRectangleBorder(
297+
borderRadius: BorderRadius.circular(8),
298+
),
299+
behavior: SnackBarBehavior.floating,
187300
),
188-
behavior: SnackBarBehavior.floating,
301+
useMaterial3: true,
189302
),
190-
useMaterial3: true,
191-
),
192-
themeMode: ThemeMode.light, // Force our dark theme
193-
home: const MainScaffold(),
194-
debugShowCheckedModeBanner: false,
195-
),
303+
themeMode: isDarkMode ? ThemeMode.dark : ThemeMode.light,
304+
home: const MainScaffold(),
305+
debugShowCheckedModeBanner: false,
306+
);
307+
},
196308
);
197309
}
198310
}

lib/models/user_preferences.dart

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ class UserPreferences {
4646
/// Close app after disconnect (Android only)
4747
final bool closeAppAfterDisconnect;
4848

49+
/// App theme mode (dark, light)
50+
final String themeMode;
51+
4952
const UserPreferences({
5053
this.powerLevel = 0.3,
5154
this.txPower = 22,
@@ -62,6 +65,7 @@ class UserPreferences {
6265
this.developerModeEnabled = false,
6366
this.mapStyle = 'dark',
6467
this.closeAppAfterDisconnect = false,
68+
this.themeMode = 'dark',
6569
});
6670

6771
/// Create from JSON (for persistence)
@@ -82,6 +86,7 @@ class UserPreferences {
8286
developerModeEnabled: (json['developerModeEnabled'] as bool?) ?? false,
8387
mapStyle: (json['mapStyle'] as String?) ?? 'dark',
8488
closeAppAfterDisconnect: (json['closeAppAfterDisconnect'] as bool?) ?? false,
89+
themeMode: (json['themeMode'] as String?) ?? 'dark',
8590
);
8691
}
8792

@@ -103,6 +108,7 @@ class UserPreferences {
103108
'developerModeEnabled': developerModeEnabled,
104109
'mapStyle': mapStyle,
105110
'closeAppAfterDisconnect': closeAppAfterDisconnect,
111+
'themeMode': themeMode,
106112
};
107113
}
108114

@@ -123,6 +129,7 @@ class UserPreferences {
123129
bool? developerModeEnabled,
124130
String? mapStyle,
125131
bool? closeAppAfterDisconnect,
132+
String? themeMode,
126133
}) {
127134
return UserPreferences(
128135
powerLevel: powerLevel ?? this.powerLevel,
@@ -140,6 +147,7 @@ class UserPreferences {
140147
developerModeEnabled: developerModeEnabled ?? this.developerModeEnabled,
141148
mapStyle: mapStyle ?? this.mapStyle,
142149
closeAppAfterDisconnect: closeAppAfterDisconnect ?? this.closeAppAfterDisconnect,
150+
themeMode: themeMode ?? this.themeMode,
143151
);
144152
}
145153

@@ -182,7 +190,8 @@ class UserPreferences {
182190
other.backgroundModeEnabled == backgroundModeEnabled &&
183191
other.developerModeEnabled == developerModeEnabled &&
184192
other.mapStyle == mapStyle &&
185-
other.closeAppAfterDisconnect == closeAppAfterDisconnect;
193+
other.closeAppAfterDisconnect == closeAppAfterDisconnect &&
194+
other.themeMode == themeMode;
186195
}
187196

188197
@override
@@ -202,6 +211,7 @@ class UserPreferences {
202211
developerModeEnabled,
203212
mapStyle,
204213
closeAppAfterDisconnect,
214+
themeMode,
205215
);
206216
}
207217
}

0 commit comments

Comments
 (0)