Storage+Migrate.swift 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693
  1. // LoopFollow
  2. // Storage+Migrate.swift
  3. // Created by Jonas Björkert on 2025-05-26.
  4. import Foundation
  5. extension Storage {
  6. func migrate() {
  7. // Helper: 1-to-1 type -----------------------------------------------------------------
  8. func move<T: AnyConvertible & Equatable>(
  9. _ legacy: @autoclosure () -> UserDefaultsValue<T>,
  10. into newValue: StorageValue<T>
  11. ) {
  12. let item = legacy()
  13. guard item.exists else { return }
  14. newValue.value = item.value
  15. item.setNil(key: item.key)
  16. }
  17. // Helper: Float → Double ------------------------------------------------------------
  18. func moveFloatToDouble(
  19. _ legacy: @autoclosure () -> UserDefaultsValue<Float>,
  20. into newValue: StorageValue<Double>
  21. ) {
  22. let item = legacy()
  23. guard item.exists else { return }
  24. newValue.value = Double(item.value)
  25. item.setNil(key: item.key)
  26. }
  27. if !UserDefaultsRepository.backgroundRefresh.value {
  28. Storage.shared.backgroundRefreshType.value = .none
  29. UserDefaultsRepository.backgroundRefresh.value = true
  30. }
  31. // Remove this in a year later than the release of the new Alarms [BEGIN]
  32. let legacyColorBGText = UserDefaultsValue<Bool>(key: "colorBGText", default: true)
  33. if legacyColorBGText.exists {
  34. Storage.shared.colorBGText.value = legacyColorBGText.value
  35. legacyColorBGText.setNil(key: "colorBGText")
  36. }
  37. let legacyAppBadge = UserDefaultsValue<Bool>(key: "appBadge", default: true)
  38. if legacyAppBadge.exists {
  39. Storage.shared.appBadge.value = legacyAppBadge.value
  40. legacyAppBadge.setNil(key: "appBadge")
  41. }
  42. let legacyForceDarkMode = UserDefaultsValue<Bool>(key: "forceDarkMode", default: true)
  43. if legacyForceDarkMode.exists {
  44. Storage.shared.forceDarkMode.value = legacyForceDarkMode.value
  45. legacyForceDarkMode.setNil(key: "forceDarkMode")
  46. }
  47. let legacyShowStats = UserDefaultsValue<Bool>(key: "showStats", default: true)
  48. if legacyShowStats.exists {
  49. Storage.shared.showStats.value = legacyShowStats.value
  50. legacyShowStats.setNil(key: "showStats")
  51. }
  52. let legacyUseIFCC = UserDefaultsValue<Bool>(key: "useIFCC", default: false)
  53. if legacyUseIFCC.exists {
  54. Storage.shared.useIFCC.value = legacyUseIFCC.value
  55. legacyUseIFCC.setNil(key: "useIFCC")
  56. }
  57. let legacyShowSmallGraph = UserDefaultsValue<Bool>(key: "showSmallGraph", default: true)
  58. if legacyShowSmallGraph.exists {
  59. Storage.shared.showSmallGraph.value = legacyShowSmallGraph.value
  60. legacyShowSmallGraph.setNil(key: "showSmallGraph")
  61. }
  62. let legacyScreenlockSwitchState = UserDefaultsValue<Bool>(key: "screenlockSwitchState", default: true)
  63. if legacyScreenlockSwitchState.exists {
  64. Storage.shared.screenlockSwitchState.value = legacyScreenlockSwitchState.value
  65. legacyScreenlockSwitchState.setNil(key: "screenlockSwitchState")
  66. }
  67. let legacyShowDisplayName = UserDefaultsValue<Bool>(key: "showDisplayName", default: false)
  68. if legacyShowDisplayName.exists {
  69. Storage.shared.showDisplayName.value = legacyShowDisplayName.value
  70. legacyShowDisplayName.setNil(key: "showDisplayName")
  71. }
  72. let legacySpeakBG = UserDefaultsValue<Bool>(key: "speakBG", default: false)
  73. if legacySpeakBG.exists {
  74. Storage.shared.speakBG.value = legacySpeakBG.value
  75. legacySpeakBG.setNil(key: "speakBG")
  76. }
  77. let legacySpeakBGAlways = UserDefaultsValue<Bool>(key: "speakBGAlways", default: true)
  78. if legacySpeakBGAlways.exists {
  79. Storage.shared.speakBGAlways.value = legacySpeakBGAlways.value
  80. legacySpeakBGAlways.setNil(key: "speakBGAlways")
  81. }
  82. let legacySpeakLowBG = UserDefaultsValue<Bool>(key: "speakLowBG", default: false)
  83. if legacySpeakLowBG.exists {
  84. Storage.shared.speakLowBG.value = legacySpeakLowBG.value
  85. legacySpeakLowBG.setNil(key: "speakLowBG")
  86. }
  87. let legacySpeakProactiveLowBG = UserDefaultsValue<Bool>(key: "speakProactiveLowBG", default: false)
  88. if legacySpeakProactiveLowBG.exists {
  89. Storage.shared.speakProactiveLowBG.value = legacySpeakProactiveLowBG.value
  90. legacySpeakProactiveLowBG.setNil(key: "speakProactiveLowBG")
  91. }
  92. let legacySpeakFastDropDelta = UserDefaultsValue<Float>(key: "speakFastDropDelta", default: 10.0)
  93. if legacySpeakFastDropDelta.exists {
  94. Storage.shared.speakFastDropDelta.value = Double(legacySpeakFastDropDelta.value)
  95. legacySpeakFastDropDelta.setNil(key: "speakFastDropDelta")
  96. }
  97. let legacySpeakLowBGLimit = UserDefaultsValue<Float>(key: "speakLowBGLimit", default: 72.0)
  98. if legacySpeakLowBGLimit.exists {
  99. Storage.shared.speakLowBGLimit.value = Double(legacySpeakLowBGLimit.value)
  100. legacySpeakLowBGLimit.setNil(key: "speakLowBGLimit")
  101. }
  102. let legacySpeakHighBGLimit = UserDefaultsValue<Float>(key: "speakHighBGLimit", default: 180.0)
  103. if legacySpeakHighBGLimit.exists {
  104. Storage.shared.speakHighBGLimit.value = Double(legacySpeakHighBGLimit.value)
  105. legacySpeakHighBGLimit.setNil(key: "speakHighBGLimit")
  106. }
  107. let legacySpeakHighBG = UserDefaultsValue<Bool>(key: "speakHighBG", default: false)
  108. if legacySpeakHighBG.exists {
  109. Storage.shared.speakHighBG.value = legacySpeakHighBG.value
  110. legacySpeakHighBG.setNil(key: "speakHighBG")
  111. }
  112. let legacySpeakLanguage = UserDefaultsValue<String>(key: "speakLanguage", default: "en")
  113. if legacySpeakLanguage.exists {
  114. Storage.shared.speakLanguage.value = legacySpeakLanguage.value
  115. legacySpeakLanguage.setNil(key: "speakLanguage")
  116. }
  117. // ── General (done earlier, but safe to repeat) ──
  118. move(UserDefaultsValue<Bool>(key: "colorBGText", default: true), into: Storage.shared.colorBGText)
  119. move(UserDefaultsValue<Bool>(key: "appBadge", default: true), into: appBadge)
  120. move(UserDefaultsValue<Bool>(key: "forceDarkMode", default: false), into: forceDarkMode)
  121. move(UserDefaultsValue<Bool>(key: "showStats", default: true), into: showStats)
  122. move(UserDefaultsValue<Bool>(key: "useIFCC", default: false), into: useIFCC)
  123. move(UserDefaultsValue<Bool>(key: "showSmallGraph", default: true), into: showSmallGraph)
  124. move(UserDefaultsValue<Bool>(key: "screenlockSwitchState", default: false), into: screenlockSwitchState)
  125. move(UserDefaultsValue<Bool>(key: "showDisplayName", default: false), into: showDisplayName)
  126. // ── Speak-BG ──
  127. move(UserDefaultsValue<Bool>(key: "speakBG", default: false), into: speakBG)
  128. move(UserDefaultsValue<Bool>(key: "speakBGAlways", default: true), into: speakBGAlways)
  129. move(UserDefaultsValue<Bool>(key: "speakLowBG", default: false), into: speakLowBG)
  130. move(UserDefaultsValue<Bool>(key: "speakProactiveLowBG", default: false), into: speakProactiveLowBG)
  131. move(UserDefaultsValue<Bool>(key: "speakHighBG", default: false), into: speakHighBG)
  132. moveFloatToDouble(UserDefaultsValue<Float>(key: "speakLowBGLimit", default: 72.0), into: speakLowBGLimit)
  133. moveFloatToDouble(UserDefaultsValue<Float>(key: "speakHighBGLimit", default: 180.0), into: speakHighBGLimit)
  134. moveFloatToDouble(UserDefaultsValue<Float>(key: "speakFastDropDelta", default: 10.0), into: speakFastDropDelta)
  135. move(UserDefaultsValue<String>(key: "speakLanguage", default: "en"), into: speakLanguage)
  136. // ── Graph ──
  137. move(UserDefaultsValue<Bool>(key: "showDots", default: true), into: showDots)
  138. move(UserDefaultsValue<Bool>(key: "showLines", default: true), into: showLines)
  139. move(UserDefaultsValue<Bool>(key: "showValues", default: true), into: showValues)
  140. move(UserDefaultsValue<Bool>(key: "showAbsorption", default: true), into: showAbsorption)
  141. move(UserDefaultsValue<Bool>(key: "showDIAMarkers", default: true), into: showDIALines)
  142. move(UserDefaultsValue<Bool>(key: "show30MinLine", default: false), into: show30MinLine)
  143. move(UserDefaultsValue<Bool>(key: "show90MinLine", default: false), into: show90MinLine)
  144. move(UserDefaultsValue<Bool>(key: "showMidnightMarkers", default: false), into: showMidnightLines)
  145. move(UserDefaultsValue<Bool>(key: "smallGraphTreatments", default: true), into: smallGraphTreatments)
  146. move(UserDefaultsValue<Int>(key: "smallGraphHeight", default: 40), into: smallGraphHeight)
  147. move(UserDefaultsValue<Double>(key: "predictionToLoad", default: 1.0), into: predictionToLoad)
  148. move(UserDefaultsValue<Double>(key: "minBasalScale", default: 5.0), into: minBasalScale)
  149. moveFloatToDouble(UserDefaultsValue<Float>(key: "minBGScale", default: 250.0), into: minBGScale)
  150. moveFloatToDouble(UserDefaultsValue<Float>(key: "lowLine", default: 70.0), into: lowLine)
  151. moveFloatToDouble(UserDefaultsValue<Float>(key: "highLine", default: 180.0), into: highLine)
  152. move(UserDefaultsValue<Int>(key: "downloadDays", default: 1), into: downloadDays)
  153. // Remove this in a year later than the release of the new Alarms [END]
  154. // ── Watch / Calendar ────────────────────────────────────────────────
  155. move(UserDefaultsValue<Bool>(key: "writeCalendarEvent", default: false), into: writeCalendarEvent)
  156. move(UserDefaultsValue<String>(key: "calendarIdentifier", default: ""), into: calendarIdentifier)
  157. move(UserDefaultsValue<String>(key: "watchLine1", default: "%BG% %DIRECTION% %DELTA% %MINAGO%"), into: watchLine1)
  158. move(UserDefaultsValue<String>(key: "watchLine2", default: "C:%COB% I:%IOB% B:%BASAL%"), into: watchLine2)
  159. // Migration of generic alarm settings
  160. // ── AlarmConfiguration migration ─────────────────────────────────────────
  161. do {
  162. // Work on a mutable copy, then write the whole thing back once.
  163. var cfg = Storage.shared.alarmConfiguration.value
  164. let cal = Calendar.current
  165. /// Copy *one* legacy value → struct field → delete old key
  166. func move<T: AnyConvertible & Equatable>(
  167. _ legacy: @autoclosure () -> UserDefaultsValue<T>,
  168. write: (inout AlarmConfiguration, T) -> Void
  169. ) {
  170. let item = legacy()
  171. guard item.exists else { return }
  172. write(&cfg, item.value)
  173. item.setNil(key: item.key)
  174. }
  175. // 1. Override-volume toggle
  176. move(UserDefaultsValue<Bool>(key: "overrideSystemOutputVolume",
  177. default: cfg.overrideSystemOutputVolume))
  178. {
  179. $0.overrideSystemOutputVolume = $1
  180. }
  181. // 2. Forced output volume itself.
  182. // Prefer newer key (“forcedOutputVolume”); otherwise fall back.
  183. if UserDefaultsValue<Float>(key: "forcedOutputVolume",
  184. default: cfg.forcedOutputVolume).exists
  185. {
  186. move(UserDefaultsValue<Float>(key: "forcedOutputVolume",
  187. default: cfg.forcedOutputVolume))
  188. {
  189. $0.forcedOutputVolume = $1
  190. }
  191. } else {
  192. move(UserDefaultsValue<Float>(key: "systemOutputVolume",
  193. default: cfg.forcedOutputVolume))
  194. {
  195. $0.forcedOutputVolume = $1
  196. }
  197. }
  198. // 3. Play audio during phone calls
  199. move(UserDefaultsValue<Bool>(key: "alertAudioDuringPhone",
  200. default: cfg.audioDuringCalls))
  201. {
  202. $0.audioDuringCalls = $1
  203. }
  204. // 4. Auto-snooze CGM-start alarm
  205. move(UserDefaultsValue<Bool>(key: "alertAutoSnoozeCGMStart",
  206. default: cfg.autoSnoozeCGMStart))
  207. {
  208. $0.autoSnoozeCGMStart = $1
  209. }
  210. // 5. Global “Snooze all” → snoozeUntil
  211. move(UserDefaultsValue<Date?>(key: "alertSnoozeAllTime",
  212. default: cfg.snoozeUntil))
  213. {
  214. $0.snoozeUntil = $1
  215. }
  216. // 6. Global “Mute all” → muteUntil
  217. move(UserDefaultsValue<Date?>(key: "alertMuteAllTime",
  218. default: cfg.muteUntil))
  219. {
  220. $0.muteUntil = $1
  221. }
  222. // 7 & 8. Legacy quiet-hours → day/night start
  223. // (only if both dates exist and are on the same “reference” day)
  224. let qStart = UserDefaultsValue<Date?>(key: "quietHourStart", default: nil)
  225. let qEnd = UserDefaultsValue<Date?>(key: "quietHourEnd", default: nil)
  226. if let s = qStart.value, let e = qEnd.value {
  227. let compsStart = cal.dateComponents([.hour, .minute], from: s)
  228. let compsEnd = cal.dateComponents([.hour, .minute], from: e)
  229. if let sh = compsStart.hour, let sm = compsStart.minute,
  230. let eh = compsEnd.hour, let em = compsEnd.minute
  231. {
  232. cfg.nightStart = TimeOfDay(hour: sh, minute: sm)
  233. cfg.dayStart = TimeOfDay(hour: eh, minute: em)
  234. qStart.setNil(key: qStart.key)
  235. qEnd.setNil(key: qEnd.key)
  236. }
  237. }
  238. // 9. Legacy “ignore zero BG” flag → ignoreZeroBG
  239. move(UserDefaultsValue<Bool>(key: "alertIgnoreZero",
  240. default: cfg.ignoreZeroBG))
  241. {
  242. $0.ignoreZeroBG = $1
  243. }
  244. // finally persist the whole struct
  245. Storage.shared.alarmConfiguration.value = cfg
  246. }
  247. migrateUrgentLowAlarm()
  248. migrateLowAlarm()
  249. migrateHighAlarm()
  250. migrateUrgentHighAlarm()
  251. migrateFastDropAlarm()
  252. migrateFastRiseAlarm()
  253. migrateMissedReadingAlarm()
  254. }
  255. // MARK: - One-off alarm migrations
  256. /// Reads *all* `alertUrgentLow*` keys, converts them into a single `Alarm`,
  257. /// saves it to the modern `[Alarm]` store, then deletes the legacy keys.
  258. private func migrateUrgentLowAlarm() {
  259. // Did the user ever change that alert? (No key ⇒ nothing to do.)
  260. guard UserDefaultsValue<Bool>(key: "alertUrgentLowActive", default: false).exists else { return }
  261. /// Helper: fetch-then-delete a legacy value in one line.
  262. func take<V: AnyConvertible & Equatable>(_ key: String, default def: V) -> V {
  263. let box = UserDefaultsValue<V>(key: key, default: def)
  264. defer { box.setNil(key: key) }
  265. return box.value
  266. }
  267. // Build the new Alarm ------------------------------------------------
  268. var alarm = Alarm(type: .low)
  269. alarm.name = "Urgent Low"
  270. alarm.isEnabled = take("alertUrgentLowActive", default: false)
  271. alarm.belowBG = Double(take("alertUrgentLowBG", default: 55.0))
  272. alarm.predictiveMinutes = take("alertUrgentLowPredictiveMinutes", default: 0)
  273. alarm.snoozeDuration = take("alertUrgentLowSnooze", default: 5)
  274. alarm.snoozedUntil = take("alertUrgentLowSnoozedTime", default: nil as Date?)
  275. alarm.soundFile = SoundFile(
  276. rawValue: take("alertUrgentLowSound",
  277. default: "Emergency_Alarm_Siren"))
  278. ?? .emergencyAlarmSiren
  279. alarm.playSoundOption = PlaySoundOption(
  280. rawValue: take("alertUrgentLowAudible",
  281. default: "Always").lowercased()) ?? .always
  282. alarm.repeatSoundOption = RepeatSoundOption(
  283. rawValue: take("alertUrgentLowRepeat",
  284. default: "Always").lowercased()) ?? .always
  285. // Day / Night active-window (“Pre-Snooze”)
  286. let autoStr = take("alertUrgentLowAutosnooze", default: "Never").lowercased()
  287. let dayFlag = take("alertUrgentLowAutosnoozeDay", default: false)
  288. let nightFlag = take("alertUrgentLowAutosnoozeNight", default: false)
  289. alarm.activeOption = {
  290. if dayFlag, !nightFlag { return .day }
  291. if !dayFlag, nightFlag { return .night }
  292. switch autoStr {
  293. case "day", "at day": return .day
  294. case "night", "at night": return .night
  295. default: return .always // “Never” → always active
  296. }
  297. }()
  298. // Persist -----------------------------------------------------------------
  299. Storage.shared.alarms.value.append(alarm)
  300. }
  301. // MARK: - Low-BG alarm -------------------------------------------------------
  302. private func migrateLowAlarm() {
  303. // Bail if the old keys were never written
  304. guard UserDefaultsValue<Bool>(key: "alertLowActive", default: false).exists else { return }
  305. // tiny helper: fetch value → erase key
  306. func take<V: AnyConvertible & Equatable>(_ key: String, default def: V) -> V {
  307. let box = UserDefaultsValue<V>(key: key, default: def)
  308. defer { box.setNil(key: key) }
  309. return box.value
  310. }
  311. // Build the new Alarm ----------------------------------------------------
  312. var alarm = Alarm(type: .low)
  313. alarm.name = "Low"
  314. alarm.isEnabled = take("alertLowActive", default: false)
  315. alarm.belowBG = Double(take("alertLowBG", default: 70.0))
  316. // “Persistent ≥ X min” → `persistentMinutes`
  317. alarm.persistentMinutes = take("alertLowPersistent", default: 0)
  318. // “Persistence max BG drop” -- ignoring this for now
  319. // let persistentLowTriggerImmediatelyBG = UserDefaultsRepository.alertLowBG.value - UserDefaultsRepository.alertLowPersistenceMax.value
  320. // (Float(persistentLowBG) <= UserDefaultsRepository.alertLowBG.value || Float(currentBG) <= persistentLowTriggerImmediatelyBG)
  321. _ = Double(take("alertLowPersistenceMax", default: 5.0))
  322. alarm.snoozeDuration = take("alertLowSnooze", default: 5)
  323. alarm.snoozedUntil = take("alertLowSnoozedTime", default: nil as Date?)
  324. alarm.soundFile = SoundFile(
  325. rawValue: take("alertLowSound",
  326. default: "Indeed")) ?? .indeed
  327. alarm.playSoundOption = PlaySoundOption(
  328. rawValue: take("alertLowAudible",
  329. default: "Always").lowercased()) ?? .always
  330. alarm.repeatSoundOption = RepeatSoundOption(
  331. rawValue: take("alertLowRepeat",
  332. default: "Always").lowercased()) ?? .always
  333. // activeOption ← legacy “Pre-Snooze” flags / picker
  334. let autoStr = take("alertLowAutosnooze", default: "Never").lowercased()
  335. let dayFlag = take("alertLowAutosnoozeDay", default: false)
  336. let nightFlag = take("alertLowAutosnoozeNight", default: false)
  337. alarm.activeOption = {
  338. if dayFlag, !nightFlag { return .day }
  339. if !dayFlag, nightFlag { return .night }
  340. switch autoStr {
  341. case "day", "at day": return .day
  342. case "night", "at night": return .night
  343. default: return .always
  344. }
  345. }()
  346. // Done → append to the modern store
  347. Storage.shared.alarms.value.append(alarm)
  348. }
  349. // MARK: - High-BG alarm -----------------------------------------------------
  350. private func migrateHighAlarm() {
  351. // Only run if the legacy key ever existed
  352. guard UserDefaultsValue<Bool>(key: "alertHighActive", default: false).exists else { return }
  353. /// Fetch → erase helper
  354. func take<V: AnyConvertible & Equatable>(_ key: String, default def: V) -> V {
  355. let box = UserDefaultsValue<V>(key: key, default: def)
  356. defer { box.setNil(key: key) } // remove legacy value
  357. return box.value
  358. }
  359. // ---------- Build Alarm -----------------------------------------------
  360. var alarm = Alarm(type: .high)
  361. alarm.name = "High"
  362. alarm.isEnabled = take("alertHighActive", default: false)
  363. alarm.aboveBG = Double(take("alertHighBG", default: 180.0))
  364. alarm.persistentMinutes = take("alertHighPersistent", default: 60)
  365. alarm.snoozeDuration = take("alertHighSnooze", default: 60)
  366. alarm.snoozedUntil = take("alertHighSnoozedTime", default: nil as Date?)
  367. alarm.soundFile = SoundFile(
  368. rawValue: take("alertHighSound",
  369. default: "Time_Has_Come")) ?? .timeHasCome
  370. alarm.playSoundOption = PlaySoundOption(
  371. rawValue: take("alertHighAudible",
  372. default: "Always").lowercased()) ?? .always
  373. alarm.repeatSoundOption = RepeatSoundOption(
  374. rawValue: take("alertHighRepeat",
  375. default: "Always").lowercased()) ?? .always
  376. // ── activeOption derived from “Pre-Snooze” picker & flags
  377. let autoStr = take("alertHighAutosnooze", default: "Never").lowercased()
  378. let dayFlag = take("alertHighAutosnoozeDay", default: false)
  379. let nightFlag = take("alertHighAutosnoozeNight", default: false)
  380. alarm.activeOption = {
  381. if dayFlag, !nightFlag { return .day }
  382. if !dayFlag, nightFlag { return .night }
  383. switch autoStr {
  384. case "day", "at day": return .day
  385. case "night", "at night": return .night
  386. default: return .always
  387. }
  388. }()
  389. // ---------- Persist & we’re done --------------------------------------
  390. Storage.shared.alarms.value.append(alarm)
  391. }
  392. // MARK: - Urgent-High alarm --------------------------------------------------
  393. private func migrateUrgentHighAlarm() {
  394. // run only once, only if the user ever changed that toggle
  395. guard UserDefaultsValue<Bool>(key: "alertUrgentHighActive", default: false).exists else { return }
  396. // helper: read-then-erase a legacy value
  397. func take<V: AnyConvertible & Equatable>(_ key: String, default def: V) -> V {
  398. let box = UserDefaultsValue<V>(key: key, default: def)
  399. defer { box.setNil(key: key) } // wipe legacy key
  400. return box.value
  401. }
  402. // ───────── Build the Alarm ────────────────────────────────────────────
  403. var alarm = Alarm(type: .high) // we map to the existing `.high` type
  404. alarm.name = "Urgent High"
  405. alarm.isEnabled = take("alertUrgentHighActive", default: false)
  406. alarm.aboveBG = Double(take("alertUrgentHighBG", default: 250.0))
  407. alarm.snoozeDuration = take("alertUrgentHighSnooze", default: 30)
  408. alarm.snoozedUntil = take("alertUrgentHighSnoozedTime", default: nil as Date?)
  409. alarm.soundFile = SoundFile(
  410. rawValue: take("alertUrgentHighSound",
  411. default: "Pager_Beeps")) ?? .pagerBeeps
  412. alarm.playSoundOption = PlaySoundOption(
  413. rawValue: take("alertUrgentHighAudible",
  414. default: "Always").lowercased()) ?? .always
  415. alarm.repeatSoundOption = RepeatSoundOption(
  416. rawValue: take("alertUrgentHighRepeat",
  417. default: "Always").lowercased()) ?? .always
  418. // activeOption comes from the old “Pre-Snooze” picker + its day/night flags
  419. let autoStr = take("alertUrgentHighAutosnooze", default: "Never").lowercased()
  420. let dayFlag = take("alertUrgentHighAutosnoozeDay", default: false)
  421. let nightFlag = take("alertUrgentHighAutosnoozeNight", default: false)
  422. alarm.activeOption = {
  423. if dayFlag, !nightFlag { return .day }
  424. if !dayFlag, nightFlag { return .night }
  425. switch autoStr { // fall back to picker value
  426. case "day", "at day": return .day
  427. case "night", "at night": return .night
  428. default: return .always
  429. }
  430. }()
  431. // ───────── Persist in new storage ─────────────────────────────────────
  432. Storage.shared.alarms.value.append(alarm)
  433. }
  434. // MARK: - Fast-Drop alarm ----------------------------------------------------
  435. private func migrateFastDropAlarm() {
  436. guard UserDefaultsValue<Bool>(key: "alertFastDropDeltaActive",
  437. default: false).exists else { return }
  438. // helper: read-then-erase
  439. func take<V: AnyConvertible & Equatable>(_ k: String, default d: V) -> V {
  440. let box = UserDefaultsValue<V>(key: k, default: d)
  441. defer { box.setNil(key: k) }
  442. return box.value
  443. }
  444. var alarm = Alarm(type: .fastDrop)
  445. alarm.name = "Fast Drop"
  446. alarm.isEnabled = take("alertFastDropDeltaActive", default: false)
  447. // core trigger parameters
  448. alarm.delta = Double(take("alertFastDropDelta", default: 10.0))
  449. alarm.monitoringWindow = take("alertFastDropReadings", default: 3) // store #readings
  450. if take("alertFastDropUseLimit", default: false) {
  451. alarm.belowBG = Double(take("alertFastDropBelowBG", default: 120.0))
  452. }
  453. // snoozing
  454. alarm.snoozeDuration = take("alertFastDropDeltaSnooze", default: 10)
  455. alarm.snoozedUntil = take("alertFastDropSnoozedTime", default: nil as Date?)
  456. // sound + options
  457. alarm.soundFile = SoundFile(
  458. rawValue: take("alertFastDropSound", default: "Big_Clock_Ticking")
  459. ) ?? .bigClockTicking
  460. alarm.playSoundOption = PlaySoundOption(
  461. rawValue: take("alertFastDropAudible", default: "Always").lowercased()
  462. ) ?? .always
  463. alarm.repeatSoundOption = RepeatSoundOption(
  464. rawValue: take("alertFastDropRepeat", default: "Never").lowercased()
  465. ) ?? .never
  466. // activeOption from old “Pre-Snooze” picker + day/night flags
  467. let autoStr = take("alertFastDropAutosnooze", default: "Never").lowercased()
  468. let dayFlag = take("alertFastDropAutosnoozeDay", default: false)
  469. let nightFlag = take("alertFastDropAutosnoozeNight", default: false)
  470. alarm.activeOption = {
  471. if dayFlag, !nightFlag { return .day }
  472. if !dayFlag, nightFlag { return .night }
  473. switch autoStr {
  474. case "day", "at day": return .day
  475. case "night", "at night": return .night
  476. default: return .always
  477. }
  478. }()
  479. Storage.shared.alarms.value.append(alarm)
  480. }
  481. // MARK: - Fast-Rise alarm ----------------------------------------------------
  482. private func migrateFastRiseAlarm() {
  483. guard UserDefaultsValue<Bool>(key: "alertFastRiseDeltaActive",
  484. default: false).exists else { return }
  485. func take<V: AnyConvertible & Equatable>(_ k: String, default d: V) -> V {
  486. let box = UserDefaultsValue<V>(key: k, default: d)
  487. defer { box.setNil(key: k) }
  488. return box.value
  489. }
  490. var alarm = Alarm(type: .fastRise)
  491. alarm.name = "Fast Rise"
  492. alarm.isEnabled = take("alertFastRiseDeltaActive", default: false)
  493. alarm.delta = Double(take("alertFastRiseDelta", default: 10.0))
  494. alarm.monitoringWindow = take("alertFastRiseReadings", default: 3)
  495. if take("alertFastRiseUseLimit", default: false) {
  496. alarm.aboveBG = Double(take("alertFastRiseAboveBG", default: 200.0))
  497. }
  498. alarm.snoozeDuration = take("alertFastRiseDeltaSnooze", default: 10)
  499. alarm.snoozedUntil = take("alertFastRiseSnoozedTime", default: nil as Date?)
  500. alarm.soundFile = SoundFile(
  501. rawValue: take("alertFastRiseSound",
  502. default: "Cartoon_Fail_Strings_Trumpet")
  503. ) ?? .cartoonFailStringsTrumpet
  504. alarm.playSoundOption = PlaySoundOption(
  505. rawValue: take("alertFastRiseAudible", default: "Always").lowercased()
  506. ) ?? .always
  507. alarm.repeatSoundOption = RepeatSoundOption(
  508. rawValue: take("alertFastRiseRepeat", default: "Never").lowercased()
  509. ) ?? .never
  510. let autoStr = take("alertFastRiseAutosnooze", default: "Never").lowercased()
  511. let dayFlag = take("alertFastRiseAutosnoozeDay", default: false)
  512. let nightFlag = take("alertFastRiseAutosnoozeNight", default: false)
  513. alarm.activeOption = {
  514. if dayFlag, !nightFlag { return .day }
  515. if !dayFlag, nightFlag { return .night }
  516. switch autoStr {
  517. case "day", "at day": return .day
  518. case "night", "at night": return .night
  519. default: return .always
  520. }
  521. }()
  522. Storage.shared.alarms.value.append(alarm)
  523. }
  524. // MARK: - Missed-Reading alarm ---------------------------------------------
  525. private func migrateMissedReadingAlarm() {
  526. // Run only when the user has ever modified those settings
  527. guard UserDefaultsValue<Bool>(key: "alertMissedReadingActive",
  528. default: false).exists else { return }
  529. // read-then-erase helper
  530. func take<V: AnyConvertible & Equatable>(_ k: String, default d: V) -> V {
  531. let b = UserDefaultsValue<V>(key: k, default: d)
  532. defer { b.setNil(key: k) }
  533. return b.value
  534. }
  535. var alarm = Alarm(type: .missedReading)
  536. alarm.name = "Missed Reading"
  537. alarm.isEnabled = take("alertMissedReadingActive", default: false)
  538. // “No CGM data for X minutes”
  539. alarm.persistentMinutes = take("alertMissedReading", default: 31)
  540. // snoozing
  541. alarm.snoozeDuration = take("alertMissedReadingSnooze", default: 30)
  542. alarm.snoozedUntil = take("alertMissedReadingSnoozedTime",
  543. default: nil as Date?)
  544. // (legacy “is-snoozed” flag is implicit in snoozedUntil)
  545. // sound
  546. alarm.soundFile = SoundFile(
  547. rawValue: take("alertMissedReadingSound",
  548. default: "Cartoon_Tip_Toe_Sneaky_Walk")
  549. ) ?? .cartoonTipToeSneakyWalk
  550. // play / repeat options
  551. alarm.playSoundOption = PlaySoundOption(
  552. rawValue: take("alertMissedReadingAudible",
  553. default: "Always").lowercased()
  554. ) ?? .always
  555. alarm.repeatSoundOption = RepeatSoundOption(
  556. rawValue: take("alertMissedReadingRepeat",
  557. default: "Never").lowercased()
  558. ) ?? .never
  559. // activeOption ← “Pre-Snooze” picker + day/night flags
  560. let autoStr = take("alertMissedReadingAutosnooze", default: "Never")
  561. .lowercased()
  562. let dayFlag = take("alertMissedReadingAutosnoozeDay", default: false)
  563. let nightFlag = take("alertMissedReadingAutosnoozeNight", default: false)
  564. alarm.activeOption = {
  565. if dayFlag, !nightFlag { return .day }
  566. if !dayFlag, nightFlag { return .night }
  567. switch autoStr {
  568. case "day", "at day": return .day
  569. case "night", "at night": return .night
  570. default: return .always // “Never” → always on
  571. }
  572. }()
  573. // store in the new world
  574. Storage.shared.alarms.value.append(alarm)
  575. }
  576. }