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 _firstLaunchAtKey = 'onboarding.firstLaunchAt'; static const _homeRoleHintShownKey = 'onboarding.homeRoleHintShown'; static const _obdAddressKey = 'obd.adapterAddress'; static const _weatherTemperatureKey = 'weather.locationTemperature'; static const _keepScreenOnKey = 'display.keepScreenOn'; static const _speedEnabledKey = 'carData.speedEnabled'; static const _speedUseObdKey = 'carData.speedUseObd'; static const _temperatureEnabledKey = 'carData.temperatureEnabled'; static const _temperatureUseObdKey = 'carData.temperatureUseObd'; 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; /// When CarDash first ran after this installation. Older installs are /// migrated to the epoch so onboarding copy cannot reappear after an update. DateTime? get firstLaunchAt { final millis = _prefs.getInt(_firstLaunchAtKey); return millis == null ? null : DateTime.fromMillisecondsSinceEpoch(millis); } /// Records the first launch once. Existing preferences mean this is an /// upgraded install, so the onboarding window is treated as already over. Future ensureFirstLaunchAt(DateTime now) async { final stored = firstLaunchAt; if (stored != null) return stored; final first = _prefs.getKeys().isEmpty ? now : DateTime.fromMillisecondsSinceEpoch(0); await _prefs.setInt(_firstLaunchAtKey, first.millisecondsSinceEpoch); return first; } /// Freezes the one-time migration default for temperature. Existing installs /// that configured OBD-II or location weather keep the feature; a fresh /// install starts off so it cannot fetch weather without an explicit choice. Future ensureTelemetryDefaults() async { if (_prefs.containsKey(_temperatureEnabledKey)) return; final wasUsingCarData = (_prefs.getBool(_weatherTemperatureKey) ?? false) || obdAdapterAddress != null; await _prefs.setBool(_temperatureEnabledKey, wasUsingCarData); } bool get homeRoleHintShown { return _prefs.getBool(_homeRoleHintShownKey) ?? false; } /// Whether the speed card may be shown on Home. bool get speedEnabled => _prefs.getBool(_speedEnabledKey) ?? true; /// OBD-II is preferred for speed when it is connected; GPS is the fallback. bool get speedUseObd => _prefs.getBool(_speedUseObdKey) ?? true; /// Fresh installs start with temperature off. [ensureTelemetryDefaults] /// preserves it for an upgrade that already had OBD or weather configured. bool get temperatureEnabled => _prefs.getBool(_temperatureEnabledKey) ?? false; /// OBD-II ambient is preferred when available; weather is the fallback. bool get temperatureUseObd => _prefs.getBool(_temperatureUseObdKey) ?? true; 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 setSpeedEnabled(bool enabled) { return _prefs.setBool(_speedEnabledKey, enabled); } Future setSpeedUseObd(bool enabled) { return _prefs.setBool(_speedUseObdKey, enabled); } Future setTemperatureEnabled(bool enabled) { return _prefs.setBool(_temperatureEnabledKey, enabled); } Future setTemperatureUseObd(bool enabled) { return _prefs.setBool(_temperatureUseObdKey, enabled); } 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)), ); } }