@@ -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+
131186class 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}
0 commit comments