settings_repository.dart 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. import 'dart:convert';
  2. import 'package:shared_preferences/shared_preferences.dart';
  3. import '../../core/theme/accent_choice.dart';
  4. import '../../core/theme/theme_preference.dart';
  5. import '../models/slot_assignment.dart';
  6. import '../models/slot_type.dart';
  7. /// Persists launcher configuration. This is the single source of truth for
  8. /// appearance, home-button visibility, the automatic night window, and pinned
  9. /// apps as well as the three fixed slots.
  10. class SettingsRepository {
  11. SettingsRepository(this._prefs);
  12. final SharedPreferences _prefs;
  13. static const _themeKey = 'settings.theme';
  14. static const _nightStartKey = 'settings.nightStartMinutes';
  15. static const _nightEndKey = 'settings.nightEndMinutes';
  16. static const _accentKey = 'settings.accent';
  17. static const _showBatteryKey = 'settings.showBattery';
  18. static const _slotPrefix = 'slot';
  19. static const _pinnedKey = 'home.pinned';
  20. static const _firstLaunchAtKey = 'onboarding.firstLaunchAt';
  21. static const _homeRoleHintShownKey = 'onboarding.homeRoleHintShown';
  22. static const _obdAddressKey = 'obd.adapterAddress';
  23. static const _weatherTemperatureKey = 'weather.locationTemperature';
  24. static const _keepScreenOnKey = 'display.keepScreenOn';
  25. static const _speedEnabledKey = 'carData.speedEnabled';
  26. static const _speedUseObdKey = 'carData.speedUseObd';
  27. static const _temperatureEnabledKey = 'carData.temperatureEnabled';
  28. static const _temperatureUseObdKey = 'carData.temperatureUseObd';
  29. ThemePreference get themePreference {
  30. return ThemePreference.fromStorage(_prefs.getString(_themeKey));
  31. }
  32. int get nightStartMinutes => _prefs.getInt(_nightStartKey) ?? 19 * 60;
  33. int get nightEndMinutes => _prefs.getInt(_nightEndKey) ?? 7 * 60;
  34. AccentChoice get accent {
  35. return AccentChoice.fromStorage(_prefs.getString(_accentKey));
  36. }
  37. bool get showBattery => _prefs.getBool(_showBatteryKey) ?? true;
  38. /// When CarDash first ran after this installation. Older installs are
  39. /// migrated to the epoch so onboarding copy cannot reappear after an update.
  40. DateTime? get firstLaunchAt {
  41. final millis = _prefs.getInt(_firstLaunchAtKey);
  42. return millis == null ? null : DateTime.fromMillisecondsSinceEpoch(millis);
  43. }
  44. /// Records the first launch once. Existing preferences mean this is an
  45. /// upgraded install, so the onboarding window is treated as already over.
  46. Future<DateTime> ensureFirstLaunchAt(DateTime now) async {
  47. final stored = firstLaunchAt;
  48. if (stored != null) return stored;
  49. final first = _prefs.getKeys().isEmpty
  50. ? now
  51. : DateTime.fromMillisecondsSinceEpoch(0);
  52. await _prefs.setInt(_firstLaunchAtKey, first.millisecondsSinceEpoch);
  53. return first;
  54. }
  55. /// Freezes the one-time migration default for temperature. Existing installs
  56. /// that configured OBD-II or location weather keep the feature; a fresh
  57. /// install starts off so it cannot fetch weather without an explicit choice.
  58. Future<void> ensureTelemetryDefaults() async {
  59. if (_prefs.containsKey(_temperatureEnabledKey)) return;
  60. final wasUsingCarData =
  61. (_prefs.getBool(_weatherTemperatureKey) ?? false) ||
  62. obdAdapterAddress != null;
  63. await _prefs.setBool(_temperatureEnabledKey, wasUsingCarData);
  64. }
  65. bool get homeRoleHintShown {
  66. return _prefs.getBool(_homeRoleHintShownKey) ?? false;
  67. }
  68. /// Whether the speed card may be shown on Home.
  69. bool get speedEnabled => _prefs.getBool(_speedEnabledKey) ?? true;
  70. /// OBD-II is preferred for speed when it is connected; GPS is the fallback.
  71. bool get speedUseObd => _prefs.getBool(_speedUseObdKey) ?? true;
  72. /// Fresh installs start with temperature off. [ensureTelemetryDefaults]
  73. /// preserves it for an upgrade that already had OBD or weather configured.
  74. bool get temperatureEnabled =>
  75. _prefs.getBool(_temperatureEnabledKey) ?? false;
  76. /// OBD-II ambient is preferred when available; weather is the fallback.
  77. bool get temperatureUseObd => _prefs.getBool(_temperatureUseObdKey) ?? true;
  78. String? get obdAdapterAddress {
  79. final value = _prefs.getString(_obdAddressKey)?.trim();
  80. return value == null || value.isEmpty ? null : value;
  81. }
  82. bool get weatherLocationTemperature {
  83. return _prefs.getBool(_weatherTemperatureKey) ?? false;
  84. }
  85. /// The dashboard keeps the display awake by default.
  86. bool get keepScreenOn {
  87. return _prefs.getBool(_keepScreenOnKey) ?? true;
  88. }
  89. Future<void> setThemePreference(ThemePreference preference) {
  90. return _prefs.setString(_themeKey, preference.storageValue);
  91. }
  92. Future<void> setAccent(AccentChoice accent) {
  93. return _prefs.setString(_accentKey, accent.storageValue);
  94. }
  95. Future<void> setShowBattery(bool show) {
  96. return _prefs.setBool(_showBatteryKey, show);
  97. }
  98. Future<void> setSpeedEnabled(bool enabled) {
  99. return _prefs.setBool(_speedEnabledKey, enabled);
  100. }
  101. Future<void> setSpeedUseObd(bool enabled) {
  102. return _prefs.setBool(_speedUseObdKey, enabled);
  103. }
  104. Future<void> setTemperatureEnabled(bool enabled) {
  105. return _prefs.setBool(_temperatureEnabledKey, enabled);
  106. }
  107. Future<void> setTemperatureUseObd(bool enabled) {
  108. return _prefs.setBool(_temperatureUseObdKey, enabled);
  109. }
  110. Future<void> setHomeRoleHintShown(bool shown) {
  111. return _prefs.setBool(_homeRoleHintShownKey, shown);
  112. }
  113. Future<void> setObdAdapterAddress(String? address) async {
  114. if (address == null || address.isEmpty) {
  115. await _prefs.remove(_obdAddressKey);
  116. } else {
  117. await _prefs.setString(_obdAddressKey, address);
  118. }
  119. }
  120. Future<void> setWeatherLocationTemperature(bool enabled) {
  121. return _prefs.setBool(_weatherTemperatureKey, enabled);
  122. }
  123. Future<void> setKeepScreenOn(bool enabled) {
  124. return _prefs.setBool(_keepScreenOnKey, enabled);
  125. }
  126. Future<void> setNightWindow({
  127. required int startMinutes,
  128. required int endMinutes,
  129. }) async {
  130. await _prefs.setInt(_nightStartKey, startMinutes);
  131. await _prefs.setInt(_nightEndKey, endMinutes);
  132. }
  133. bool slotEnabled(SlotType slot) {
  134. return _prefs.getBool('$_slotPrefix.${slot.key}.enabled') ?? true;
  135. }
  136. Future<void> setSlotEnabled(SlotType slot, bool enabled) {
  137. return _prefs.setBool('$_slotPrefix.${slot.key}.enabled', enabled);
  138. }
  139. SlotAssignment? slot(SlotType slot) {
  140. final packageName = _prefs.getString('$_slotPrefix.${slot.key}.package');
  141. final activityName = _prefs.getString('$_slotPrefix.${slot.key}.activity');
  142. final label = _prefs.getString('$_slotPrefix.${slot.key}.label');
  143. if (packageName == null || activityName == null) return null;
  144. return SlotAssignment(
  145. packageName: packageName,
  146. activityName: activityName,
  147. label: label ?? packageName,
  148. );
  149. }
  150. Future<void> setSlot(SlotType slot, SlotAssignment assignment) async {
  151. await _prefs.setString(
  152. '$_slotPrefix.${slot.key}.package',
  153. assignment.packageName,
  154. );
  155. await _prefs.setString(
  156. '$_slotPrefix.${slot.key}.activity',
  157. assignment.activityName,
  158. );
  159. await _prefs.setString('$_slotPrefix.${slot.key}.label', assignment.label);
  160. }
  161. Future<void> clearSlot(SlotType slot) async {
  162. await _prefs.remove('$_slotPrefix.${slot.key}.package');
  163. await _prefs.remove('$_slotPrefix.${slot.key}.activity');
  164. await _prefs.remove('$_slotPrefix.${slot.key}.label');
  165. }
  166. /// Apps explicitly pinned to the home screen via All Apps. The three
  167. /// predefined slots are stored separately.
  168. List<SlotAssignment> get pinnedApps {
  169. final raw = _prefs.getString(_pinnedKey);
  170. if (raw == null || raw.isEmpty) return const <SlotAssignment>[];
  171. try {
  172. final decoded = jsonDecode(raw);
  173. if (decoded is! List) return const <SlotAssignment>[];
  174. return decoded
  175. .whereType<Map>()
  176. .map((entry) => SlotAssignment.fromMap(entry))
  177. .where(
  178. (entry) =>
  179. entry.packageName.isNotEmpty && entry.activityName.isNotEmpty,
  180. )
  181. .toList(growable: false);
  182. } catch (_) {
  183. return const <SlotAssignment>[];
  184. }
  185. }
  186. Future<void> setPinnedApps(List<SlotAssignment> apps) {
  187. return _prefs.setString(
  188. _pinnedKey,
  189. jsonEncode(apps.map((app) => app.toMap()).toList(growable: false)),
  190. );
  191. }
  192. }