DeviceAlarmEditorView.swift 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. import LoopKit
  2. import SwiftUI
  3. struct DeviceAlarmEditorView: View {
  4. @ObservedObject var store: DeviceAlertsStore
  5. let configID: UUID
  6. let isNew: Bool
  7. var onDone: () -> Void
  8. var onCancel: () -> Void
  9. @Environment(\.dismiss) private var dismiss
  10. @Environment(\.colorScheme) private var colorScheme
  11. @Environment(AppState.self) private var appState
  12. @State private var working: DeviceAlertSeverityConfig
  13. init(
  14. store: DeviceAlertsStore,
  15. initial: DeviceAlertSeverityConfig,
  16. isNew: Bool,
  17. onDone: @escaping () -> Void,
  18. onCancel: @escaping () -> Void = {}
  19. ) {
  20. self.store = store
  21. configID = initial.id
  22. self.isNew = isNew
  23. self.onDone = onDone
  24. self.onCancel = onCancel
  25. _working = State(initialValue: initial)
  26. }
  27. var body: some View {
  28. NavigationStack {
  29. Form {
  30. Section(
  31. header: Text("Behavior"),
  32. footer: Text(working.severity.blurb)
  33. ) {
  34. HStack {
  35. Text(working.severity.displayName).font(.headline)
  36. Spacer()
  37. Text(activeLabel)
  38. .font(.footnote)
  39. .foregroundColor(.secondary)
  40. }
  41. // Critical-tier configs are always armed — the user can
  42. // mute the sound via the Audio section but not turn the
  43. // alarm itself off. Other tiers expose the toggle.
  44. if working.severity != .critical {
  45. Toggle(String(localized: "Enabled"), isOn: $working.isEnabled)
  46. }
  47. Toggle(
  48. String(localized: "Override Silence & Focus Mode"),
  49. isOn: $working.overridesSilenceAndDND
  50. )
  51. }.listRowBackground(Color.chart)
  52. AlarmActiveSection(activeOption: $working.activeOption)
  53. AlarmAudioSection(
  54. playsSound: $working.playsSound,
  55. soundFilename: $working.soundFilename
  56. )
  57. Section(header: Text("Applies To (Cannot be changed)")) {
  58. ForEach(conceptsForTier(working.severity), id: \.self) { concept in
  59. Text(concept.displayTitle)
  60. .font(.footnote)
  61. .foregroundColor(.secondary)
  62. }
  63. }.listRowBackground(Color.chart)
  64. if !isNew, store.canDelete(working) {
  65. Section {
  66. Button(role: .destructive) {
  67. store.remove(working)
  68. dismiss()
  69. } label: {
  70. Text("Delete Variant")
  71. }
  72. }.listRowBackground(Color.chart)
  73. }
  74. }
  75. .scrollContentBackground(.hidden).background(appState.trioBackgroundColor(for: colorScheme))
  76. .navigationTitle(working.severity.displayName)
  77. .navigationBarTitleDisplayMode(.inline)
  78. .toolbar {
  79. ToolbarItem(placement: .confirmationAction) {
  80. Button(isNew ? String(localized: "Add") : String(localized: "Done")) {
  81. if isNew {
  82. store.add(working)
  83. } else {
  84. store.update(working)
  85. }
  86. onDone()
  87. dismiss()
  88. }
  89. }
  90. ToolbarItem(placement: .cancellationAction) {
  91. Button(String(localized: "Cancel")) {
  92. onCancel()
  93. dismiss()
  94. }
  95. }
  96. }
  97. }
  98. }
  99. private var activeLabel: String {
  100. switch working.activeOption {
  101. case .always: return String(localized: "Day & Night")
  102. case .day: return String(localized: "Day only")
  103. case .night: return String(localized: "Night only")
  104. }
  105. }
  106. /// Distinct alarm concepts whose catalog entries fall into this tier.
  107. /// Sorted by display title so the list is stable across plugin changes.
  108. private func conceptsForTier(_ tier: DeviceAlertSeverity) -> [LoopKit.Alert.CatalogConcept] {
  109. var seen: Set<LoopKit.Alert.CatalogConcept> = []
  110. var ordered: [LoopKit.Alert.CatalogConcept] = []
  111. for entry in AlertCatalogRegistry.entries
  112. where DeviceAlertSeverity(level: entry.interruptionLevel) == tier
  113. {
  114. if seen.insert(entry.concept).inserted {
  115. ordered.append(entry.concept)
  116. }
  117. }
  118. return ordered.sorted { $0.displayTitle < $1.displayTitle }
  119. }
  120. }