Skip to content

Commit bc36e42

Browse files
committed
feat(app): auto-persist dark/light theme preference via Vault
MagicApplication now automatically saves and restores the user's theme toggle preference using the Vault facade. No manual wiring needed — apps get persistence for free. - Load saved brightness on startup (_initialize) - Save on user toggle via onThemeChanged - Graceful fallback when VaultServiceProvider is not registered - Updated magic_cli submodule (JsonEditor deep merge)
1 parent 0328e98 commit bc36e42

2 files changed

Lines changed: 110 additions & 9 deletions

File tree

lib/src/foundation/magic_app_widget.dart

Lines changed: 109 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import 'package:fluttersdk_wind/fluttersdk_wind.dart';
55
import '../facades/config.dart';
66
import '../facades/lang.dart';
77
import '../facades/log.dart';
8+
import '../facades/vault.dart';
89
import '../routing/magic_router.dart';
910

1011
import 'magic.dart';
@@ -56,6 +57,11 @@ class MagicAppWidgetState extends State<MagicAppWidget> {
5657
/// Handles all internal initialization (environment, config, routing)
5758
/// and wraps the app in WindTheme for Wind widget support.
5859
///
60+
/// Theme preference is automatically persisted to Vault.
61+
/// When a user manually toggles dark/light mode, the preference is saved
62+
/// and restored on next app launch. If no preference is saved, the app
63+
/// follows the system brightness setting.
64+
///
5965
/// ## Usage
6066
///
6167
/// ```dart
@@ -92,10 +98,11 @@ class MagicApplication extends StatefulWidget {
9298

9399
/// Callback fired when the user manually toggles the theme.
94100
///
95-
/// Use this to persist the user's preference:
101+
/// This is called IN ADDITION to the built-in theme persistence.
102+
/// Use this for custom side-effects beyond storage:
96103
/// ```dart
97104
/// MagicApplication(
98-
/// onThemeChanged: (brightness) => saveThemePreference(brightness),
105+
/// onThemeChanged: (brightness) => analytics.track('theme_changed'),
99106
/// )
100107
/// ```
101108
final ValueChanged<Brightness>? onThemeChanged;
@@ -128,25 +135,40 @@ class MagicApplication extends StatefulWidget {
128135
}
129136

130137
class _MagicApplicationState extends State<MagicApplication> {
138+
/// Vault storage key for theme preference.
139+
static const _themeKey = 'theme_mode';
140+
131141
bool _initialized = false;
132142
bool _hasError = false;
133143

144+
/// Saved brightness preference loaded from Vault.
145+
///
146+
/// - `null` means no preference saved (follow system).
147+
/// - Non-null means user has a manual preference.
148+
Brightness? _savedBrightness;
149+
134150
@override
135151
void initState() {
136152
super.initState();
137153
_initialize();
138154
}
139155

140-
void _initialize() {
156+
/// Initialize the application and load saved theme preference.
157+
///
158+
/// Loads theme preference from Vault before marking as initialized.
159+
/// If Vault is not available (no VaultServiceProvider registered),
160+
/// gracefully falls back to system default.
161+
Future<void> _initialize() async {
141162
try {
142-
// Magic.init() has already been called in main.dart before runApp()
143-
// So we just need to call onInit callback if provided
163+
// 1. Load saved theme preference from Vault.
164+
_savedBrightness = await _loadThemePreference();
144165

145-
// Configure initial route if different from default
166+
// 2. Configure initial route if different from default.
146167
if (widget.initialRoute != '/') {
147168
MagicRouter.instance.setInitialLocation(widget.initialRoute);
148169
}
149170

171+
// 3. Call the app's onInit callback.
150172
widget.onInit?.call();
151173
setState(() => _initialized = true);
152174
} catch (e) {
@@ -174,11 +196,14 @@ class _MagicApplicationState extends State<MagicApplication> {
174196
);
175197
}
176198

177-
final windThemeData = widget.windTheme ?? WindThemeData();
199+
// 1. Apply saved brightness preference to WindThemeData.
200+
final windThemeData = _applyThemePreference(
201+
widget.windTheme ?? WindThemeData(),
202+
);
178203

179204
return WindTheme(
180205
data: windThemeData,
181-
onThemeChanged: widget.onThemeChanged,
206+
onThemeChanged: _onThemeChanged,
182207
builder: (context, controller) => MagicAppWidget(
183208
key: MagicAppWidget._appKey,
184209
themeMode: widget.themeMode,
@@ -236,4 +261,80 @@ class _MagicApplicationState extends State<MagicApplication> {
236261
return Locale(code.toString());
237262
}).toList();
238263
}
264+
265+
// ---------------------------------------------------------------------------
266+
// Theme Persistence
267+
// ---------------------------------------------------------------------------
268+
269+
/// Apply saved brightness preference to the theme data.
270+
///
271+
/// When a saved preference exists, overrides the brightness and disables
272+
/// system sync. Otherwise, returns the original theme data unchanged.
273+
WindThemeData _applyThemePreference(WindThemeData data) {
274+
if (_savedBrightness == null) {
275+
return data;
276+
}
277+
278+
return data.copyWith(
279+
brightness: _savedBrightness,
280+
syncWithSystem: false,
281+
);
282+
}
283+
284+
/// Handle theme change — persist to Vault and forward to user callback.
285+
///
286+
/// Called only on user-initiated theme changes (not system changes).
287+
void _onThemeChanged(Brightness brightness) {
288+
// 1. Persist the preference to Vault.
289+
_saveThemePreference(brightness);
290+
291+
// 2. Forward to user's callback if provided.
292+
widget.onThemeChanged?.call(brightness);
293+
}
294+
295+
/// Load theme preference from Vault.
296+
///
297+
/// Returns the saved [Brightness], or `null` if no preference is stored.
298+
/// Gracefully handles missing VaultServiceProvider.
299+
Future<Brightness?> _loadThemePreference() async {
300+
try {
301+
if (!Magic.bound('vault')) {
302+
return null;
303+
}
304+
305+
final saved = await Vault.get(_themeKey);
306+
307+
if (saved == 'dark') {
308+
Log.info('[MagicApplication] Theme preference loaded: dark');
309+
return Brightness.dark;
310+
}
311+
312+
if (saved == 'light') {
313+
Log.info('[MagicApplication] Theme preference loaded: light');
314+
return Brightness.light;
315+
}
316+
317+
return null;
318+
} catch (e) {
319+
Log.error('[MagicApplication] Failed to load theme preference: $e');
320+
return null;
321+
}
322+
}
323+
324+
/// Save theme preference to Vault.
325+
///
326+
/// Stores 'dark' or 'light' string. Gracefully handles missing Vault.
327+
Future<void> _saveThemePreference(Brightness brightness) async {
328+
try {
329+
if (!Magic.bound('vault')) {
330+
return;
331+
}
332+
333+
final value = brightness == Brightness.dark ? 'dark' : 'light';
334+
await Vault.put(_themeKey, value);
335+
Log.info('[MagicApplication] Theme preference saved: $value');
336+
} catch (e) {
337+
Log.error('[MagicApplication] Failed to save theme preference: $e');
338+
}
339+
}
239340
}

0 commit comments

Comments
 (0)