import 'dart:convert'; import 'package:shared_preferences/shared_preferences.dart'; import '../../core/theme/accent_choice.dart'; import '../../core/theme/theme_preference.dart'; import '../models/slot_assignment.dart'; import '../models/slot_type.dart'; /// Persists launcher configuration. This is the single source of truth for /// appearance, home-button visibility, the automatic night window, and pinned /// apps as well as the three fixed slots. class SettingsRepository { SettingsRepository(this._prefs); final SharedPreferences _prefs; static const _themeKey = 'settings.theme'; static const _nightStartKey = 'settings.nightStartMinutes'; static const _nightEndKey = 'settings.nightEndMinutes'; static const _accentKey = 'settings.accent'; static const _showBatteryKey = 'settings.showBattery'; static const _slotPrefix = 'slot'; static const _pinnedKey = 'home.pinned'; static const _homeRoleHintShownKey = 'onboarding.homeRoleHintShown'; static const _obdAddressKey = 'obd.adapterAddress'; static const _weatherTemperatureKey = 'weather.locationTemperature'; static const _keepScreenOnKey = 'display.keepScreenOn'; ThemePreference get themePreference { return ThemePreference.fromStorage(_prefs.getString(_themeKey)); } int get nightStartMinutes => _prefs.getInt(_nightStartKey) ?? 19 * 60; int get nightEndMinutes => _prefs.getInt(_nightEndKey) ?? 7 * 60; AccentChoice get accent { return AccentChoice.fromStorage(_prefs.getString(_accentKey)); } bool get showBattery => _prefs.getBool(_showBatteryKey) ?? true; bool get homeRoleHintShown { return _prefs.getBool(_homeRoleHintShownKey) ?? false; } String? get obdAdapterAddress { final value = _prefs.getString(_obdAddressKey)?.trim(); return value == null || value.isEmpty ? null : value; } bool get weatherLocationTemperature { return _prefs.getBool(_weatherTemperatureKey) ?? false; } /// The dashboard keeps the display awake by default. bool get keepScreenOn { return _prefs.getBool(_keepScreenOnKey) ?? true; } Future setThemePreference(ThemePreference preference) { return _prefs.setString(_themeKey, preference.storageValue); } Future setAccent(AccentChoice accent) { return _prefs.setString(_accentKey, accent.storageValue); } Future setShowBattery(bool show) { return _prefs.setBool(_showBatteryKey, show); } Future setHomeRoleHintShown(bool shown) { return _prefs.setBool(_homeRoleHintShownKey, shown); } Future setObdAdapterAddress(String? address) async { if (address == null || address.isEmpty) { await _prefs.remove(_obdAddressKey); } else { await _prefs.setString(_obdAddressKey, address); } } Future setWeatherLocationTemperature(bool enabled) { return _prefs.setBool(_weatherTemperatureKey, enabled); } Future setKeepScreenOn(bool enabled) { return _prefs.setBool(_keepScreenOnKey, enabled); } Future setNightWindow({ required int startMinutes, required int endMinutes, }) async { await _prefs.setInt(_nightStartKey, startMinutes); await _prefs.setInt(_nightEndKey, endMinutes); } bool slotEnabled(SlotType slot) { return _prefs.getBool('$_slotPrefix.${slot.key}.enabled') ?? true; } Future setSlotEnabled(SlotType slot, bool enabled) { return _prefs.setBool('$_slotPrefix.${slot.key}.enabled', enabled); } SlotAssignment? slot(SlotType slot) { final packageName = _prefs.getString('$_slotPrefix.${slot.key}.package'); final activityName = _prefs.getString('$_slotPrefix.${slot.key}.activity'); final label = _prefs.getString('$_slotPrefix.${slot.key}.label'); if (packageName == null || activityName == null) return null; return SlotAssignment( packageName: packageName, activityName: activityName, label: label ?? packageName, ); } Future setSlot(SlotType slot, SlotAssignment assignment) async { await _prefs.setString( '$_slotPrefix.${slot.key}.package', assignment.packageName, ); await _prefs.setString( '$_slotPrefix.${slot.key}.activity', assignment.activityName, ); await _prefs.setString('$_slotPrefix.${slot.key}.label', assignment.label); } Future clearSlot(SlotType slot) async { await _prefs.remove('$_slotPrefix.${slot.key}.package'); await _prefs.remove('$_slotPrefix.${slot.key}.activity'); await _prefs.remove('$_slotPrefix.${slot.key}.label'); } /// Apps explicitly pinned to the home screen via All Apps. The three /// predefined slots are stored separately. List get pinnedApps { final raw = _prefs.getString(_pinnedKey); if (raw == null || raw.isEmpty) return const []; try { final decoded = jsonDecode(raw); if (decoded is! List) return const []; return decoded .whereType() .map((entry) => SlotAssignment.fromMap(entry)) .where( (entry) => entry.packageName.isNotEmpty && entry.activityName.isNotEmpty, ) .toList(growable: false); } catch (_) { return const []; } } Future setPinnedApps(List apps) { return _prefs.setString( _pinnedKey, jsonEncode(apps.map((app) => app.toMap()).toList(growable: false)), ); } }