OverrideRootView.swift 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726
  1. import CoreData
  2. import SwiftUI
  3. import Swinject
  4. extension OverrideConfig {
  5. struct RootView: BaseView {
  6. let resolver: Resolver
  7. @State var state = StateModel()
  8. @State private var isEditing = false
  9. @State private var showOverrideCreationSheet = false
  10. @State private var showTempTargetCreationSheet = false
  11. @State private var showingDetail = false
  12. @State private var showCheckmark: Bool = false
  13. @State private var selectedPresetID: String?
  14. @State private var selectedTempTargetPresetID: String?
  15. @State private var selectedOverride: OverrideStored?
  16. @State private var selectedTempTarget: TempTargetStored?
  17. // temp targets
  18. @State private var isConfirmDeletePresented = false
  19. @State private var isPromptPresented = false
  20. @State private var isRemoveAlertPresented = false
  21. @State private var removeAlert: Alert?
  22. @State private var isEditingTT = false
  23. @Environment(\.managedObjectContext) var moc
  24. @Environment(\.colorScheme) var colorScheme
  25. var color: LinearGradient {
  26. colorScheme == .dark ? LinearGradient(
  27. gradient: Gradient(colors: [
  28. Color.bgDarkBlue,
  29. Color.bgDarkerDarkBlue
  30. ]),
  31. startPoint: .top,
  32. endPoint: .bottom
  33. )
  34. :
  35. LinearGradient(
  36. gradient: Gradient(colors: [Color.gray.opacity(0.1)]),
  37. startPoint: .top,
  38. endPoint: .bottom
  39. )
  40. }
  41. private var formatter: NumberFormatter {
  42. let formatter = NumberFormatter()
  43. formatter.numberStyle = .decimal
  44. formatter.maximumFractionDigits = 0
  45. return formatter
  46. }
  47. private var glucoseFormatter: NumberFormatter {
  48. let formatter = NumberFormatter()
  49. formatter.numberStyle = .decimal
  50. formatter.maximumFractionDigits = 0
  51. if state.units == .mmolL {
  52. formatter.maximumFractionDigits = 1
  53. }
  54. formatter.roundingMode = .halfUp
  55. return formatter
  56. }
  57. private func formattedGlucose(glucose: Decimal) -> String {
  58. let formattedValue: String
  59. if state.units == .mgdL {
  60. formattedValue = glucoseFormatter.string(from: glucose as NSDecimalNumber) ?? "\(glucose)"
  61. } else {
  62. formattedValue = glucose.formattedAsMmolL
  63. }
  64. return "\(formattedValue) \(state.units.rawValue)"
  65. }
  66. var body: some View {
  67. VStack {
  68. HStack(spacing: 6) {
  69. HStack {
  70. Spacer()
  71. Image(systemName: "clock.arrow.2.circlepath")
  72. .font(.system(size: 20))
  73. .foregroundStyle(Color.primary, Color(red: 0.6235294118, green: 0.4235294118, blue: 0.9803921569))
  74. Text(OverrideConfig.Tab.overrides.name)
  75. .font(.subheadline)
  76. .lineLimit(1)
  77. .minimumScaleFactor(0.8)
  78. Spacer()
  79. }
  80. .padding(.vertical, 6)
  81. .background(state.selectedTab == .overrides ? Color.loopGray.opacity(0.4) : Color.clear)
  82. .cornerRadius(8)
  83. .onTapGesture {
  84. withAnimation {
  85. state.selectedTab = .overrides
  86. }
  87. }
  88. HStack {
  89. Spacer()
  90. Image(systemName: "target")
  91. .font(.system(size: 20))
  92. .foregroundStyle(Color.loopGreen)
  93. Text(OverrideConfig.Tab.tempTargets.name)
  94. .font(.subheadline)
  95. .lineLimit(1)
  96. .minimumScaleFactor(0.8)
  97. Spacer()
  98. }
  99. .padding(.vertical, 6)
  100. .background(state.selectedTab == .tempTargets ? Color.loopGray.opacity(0.4) : Color.clear)
  101. .cornerRadius(8)
  102. .onTapGesture {
  103. withAnimation {
  104. state.selectedTab = .tempTargets
  105. }
  106. }
  107. }
  108. .background(Color.gray.opacity(0.2))
  109. .cornerRadius(8)
  110. .padding(.horizontal)
  111. List {
  112. switch state.selectedTab {
  113. case .overrides: overrides()
  114. case .tempTargets: tempTargets() }
  115. }
  116. .listSectionSpacing(10)
  117. .safeAreaInset(edge: .bottom, spacing: 30) { stickyStopButton }
  118. .scrollContentBackground(.hidden).background(color)
  119. .onAppear(perform: configureView)
  120. .navigationBarTitle("Adjustments")
  121. .navigationBarTitleDisplayMode(.large)
  122. .toolbar {
  123. ToolbarItem(placement: .topBarTrailing) {
  124. switch state.selectedTab {
  125. case .overrides:
  126. Button(action: {
  127. showOverrideCreationSheet = true
  128. }, label: {
  129. HStack {
  130. Text("Add Override")
  131. Image(systemName: "plus")
  132. }
  133. })
  134. case .tempTargets:
  135. Button(action: {
  136. showTempTargetCreationSheet = true
  137. }, label: {
  138. HStack {
  139. Text("Add Temp Target")
  140. Image(systemName: "plus")
  141. }
  142. })
  143. }
  144. }
  145. }
  146. .sheet(isPresented: $state.showOverrideEditSheet, onDismiss: {
  147. Task {
  148. await state.resetStateVariables()
  149. state.showOverrideEditSheet = false
  150. }
  151. }) {
  152. if let override = selectedOverride {
  153. EditOverrideForm(overrideToEdit: override, state: state)
  154. }
  155. }
  156. .sheet(isPresented: $showOverrideCreationSheet, onDismiss: {
  157. Task {
  158. await state.resetStateVariables()
  159. showOverrideCreationSheet = false
  160. }
  161. }) {
  162. AddOverrideForm(state: state)
  163. }
  164. .sheet(isPresented: $showTempTargetCreationSheet, onDismiss: {
  165. Task {
  166. await state.resetTempTargetState()
  167. showTempTargetCreationSheet = false
  168. }
  169. }) {
  170. AddTempTargetForm(state: state)
  171. }
  172. .sheet(isPresented: $state.showTempTargetEditSheet, onDismiss: {
  173. Task {
  174. await state.resetTempTargetState()
  175. state.showTempTargetEditSheet = false
  176. }
  177. }) {
  178. if let tempTarget = selectedTempTarget {
  179. EditTempTargetForm(tempTargetToEdit: tempTarget, state: state)
  180. }
  181. }
  182. }.background(color)
  183. }
  184. @ViewBuilder func overrides() -> some View {
  185. if state.isEnabled, state.activeOverrideName.isNotEmpty {
  186. currentActiveAdjustment
  187. }
  188. if state.overridePresets.isNotEmpty {
  189. overridePresets
  190. } else {
  191. defaultText
  192. }
  193. // if state.overridePresets.isNotEmpty || state.currentActiveOverride != nil {
  194. // cancelAdjustmentButton
  195. // }
  196. }
  197. @ViewBuilder func tempTargets() -> some View {
  198. if state.isTempTargetEnabled, state.activeTempTargetName.isNotEmpty {
  199. currentActiveAdjustment
  200. }
  201. if state.tempTargetPresets.isNotEmpty {
  202. tempTargetPresets
  203. } else {
  204. defaultText
  205. }
  206. // if state.tempTargetPresets.isNotEmpty || state.currentActiveTempTarget != nil {
  207. // cancelAdjustmentButton
  208. // }
  209. }
  210. private var defaultText: some View {
  211. switch state.selectedTab {
  212. case .overrides:
  213. Section {} header: {
  214. Text("Add Preset or Override by tapping 'Add Override +' in the top right-hand corner of the screen.")
  215. .textCase(nil)
  216. .foregroundStyle(.secondary)
  217. }
  218. case .tempTargets:
  219. Section {} header: {
  220. Text(
  221. "Add Preset or Temp Target by tapping 'Add Temp Target +' in the top right-hand corner of the screen."
  222. )
  223. .textCase(nil)
  224. .foregroundStyle(.secondary)
  225. }
  226. }
  227. }
  228. private var overridePresets: some View {
  229. Section {
  230. ForEach(state.overridePresets) { preset in
  231. overridesView(for: preset)
  232. .swipeActions(edge: .trailing, allowsFullSwipe: true) {
  233. Button(role: .none) {
  234. selectedOverride = preset
  235. isConfirmDeletePresented = true
  236. } label: {
  237. Label("Delete", systemImage: "trash")
  238. .tint(.red)
  239. }
  240. Button(action: {
  241. // Set the selected Override to the chosen Preset and pass it to the Edit Sheet
  242. selectedOverride = preset
  243. state.showOverrideEditSheet = true
  244. }, label: {
  245. Label("Edit", systemImage: "pencil")
  246. .tint(.blue)
  247. })
  248. }
  249. }
  250. .onMove(perform: state.reorderOverride)
  251. .confirmationDialog(
  252. "Delete the Override Preset \"\(selectedOverride?.name ?? "")\"?",
  253. isPresented: $isConfirmDeletePresented,
  254. titleVisibility: .visible
  255. ) {
  256. if let itemToDelete = selectedOverride {
  257. Button(
  258. state.currentActiveOverride == selectedOverride ? "Stop and Delete" : "Delete",
  259. role: .destructive
  260. ) {
  261. if state.currentActiveOverride == selectedOverride {
  262. Task {
  263. // Save cancelled Override in OverrideRunStored Entity
  264. // Cancel ALL active Override
  265. await state.disableAllActiveOverrides(createOverrideRunEntry: true)
  266. }
  267. }
  268. // Perform the delete action
  269. Task {
  270. await state.invokeOverridePresetDeletion(itemToDelete.objectID)
  271. }
  272. // Reset the selected item after deletion
  273. selectedOverride = nil
  274. }
  275. }
  276. Button("Cancel", role: .cancel) {
  277. // Dismiss the dialog without action
  278. selectedOverride = nil
  279. }
  280. } message: {
  281. if state.currentActiveOverride == selectedOverride {
  282. Text(
  283. state
  284. .currentActiveOverride == selectedOverride ?
  285. "This override preset is currently running. Deleting will stop it." : ""
  286. )
  287. }
  288. }
  289. .listRowBackground(Color.chart)
  290. } header: {
  291. Text("Presets")
  292. } footer: {
  293. HStack {
  294. Image(systemName: "hand.draw.fill")
  295. Text("Swipe left to edit or delete an override preset. Hold, drag and drop to reorder a preset.")
  296. }
  297. }
  298. }
  299. private var tempTargetPresets: some View {
  300. Section {
  301. ForEach(state.tempTargetPresets) { preset in
  302. tempTargetView(for: preset)
  303. .swipeActions(edge: .trailing, allowsFullSwipe: true) {
  304. Button(role: .none) {
  305. Task {
  306. selectedTempTarget = preset
  307. isConfirmDeletePresented = true
  308. }
  309. } label: {
  310. Label("Delete", systemImage: "trash")
  311. .tint(.red)
  312. }
  313. Button(action: {
  314. // Set the selected Temp Target to the chosen Preset and pass it to the Edit Sheet
  315. selectedTempTarget = preset
  316. state.showTempTargetEditSheet = true
  317. }, label: {
  318. Label("Edit", systemImage: "pencil")
  319. .tint(.blue)
  320. })
  321. }
  322. }
  323. .onMove(perform: state.reorderTempTargets)
  324. .confirmationDialog(
  325. "Delete the Temp Target Preset \"\(selectedTempTarget?.name ?? "")\"?",
  326. isPresented: $isConfirmDeletePresented,
  327. titleVisibility: .visible
  328. ) {
  329. if let itemToDelete = selectedTempTarget {
  330. Button(
  331. state.currentActiveTempTarget == selectedTempTarget ? "Stop and Delete" : "Delete",
  332. role: .destructive
  333. ) {
  334. if state.currentActiveTempTarget == selectedTempTarget {
  335. Task {
  336. // Save cancelled Temp Target in Temp Target run Entity
  337. await state.disableAllActiveTempTargets(createTempTargetRunEntry: true)
  338. }
  339. }
  340. // Perform the stop action
  341. Task {
  342. await state.invokeTempTargetPresetDeletion(itemToDelete.objectID)
  343. }
  344. // Reset the selected item after deletion
  345. selectedTempTarget = nil
  346. }
  347. }
  348. Button("Cancel", role: .cancel) {
  349. // Dismiss the dialog without action
  350. selectedTempTarget = nil
  351. }
  352. } message: {
  353. if state.currentActiveTempTarget == selectedTempTarget {
  354. Text(
  355. state
  356. .currentActiveTempTarget == selectedTempTarget ?
  357. "This Temp Target preset is currently running. Deleting will stop it." : ""
  358. )
  359. }
  360. }
  361. .listRowBackground(Color.chart)
  362. } header: {
  363. Text("Presets")
  364. } footer: {
  365. HStack {
  366. Image(systemName: "hand.draw.fill")
  367. Text("Swipe left to edit or delete a Temp Target preset. Hold, drag and drop to reorder a preset.")
  368. }
  369. }
  370. }
  371. private var currentActiveAdjustment: some View {
  372. switch state.selectedTab {
  373. case .overrides:
  374. Section {
  375. HStack {
  376. Text("\(state.activeOverrideName) is running")
  377. Spacer()
  378. Image(systemName: "square.and.pencil")
  379. .foregroundStyle(Color.blue)
  380. }
  381. .contentShape(Rectangle())
  382. .onTapGesture {
  383. Task {
  384. /// To avoid editing the Preset when a Preset-Override is running we first duplicate the Preset-Override as a non-Preset Override
  385. /// The currentActiveOverride variable in the State will update automatically via MOC notification
  386. await state.duplicateOverridePresetAndCancelPreviousOverride()
  387. /// selectedOverride is used for passing the chosen Override to the EditSheet so we have to set the updated currentActiveOverride to be the selectedOverride
  388. selectedOverride = state.currentActiveOverride
  389. /// Now we can show the Edit sheet
  390. state.showOverrideEditSheet = true
  391. }
  392. }
  393. }
  394. .listRowBackground(Color.blue.opacity(0.2))
  395. case .tempTargets:
  396. Section {
  397. HStack {
  398. Text("\(state.activeTempTargetName) is running")
  399. Spacer()
  400. Image(systemName: "square.and.pencil")
  401. .foregroundStyle(Color.blue)
  402. }
  403. .contentShape(Rectangle())
  404. .onTapGesture {
  405. Task {
  406. /// To avoid editing the Preset when a Preset-Override is running we first duplicate the Preset-Override as a non-Preset Override
  407. /// The currentActiveOverride variable in the State will update automatically via MOC notification
  408. await state.duplicateTempTargetPresetAndCancelPreviousTempTarget()
  409. /// selectedOverride is used for passing the chosen Override to the EditSheet so we have to set the updated currentActiveOverride to be the selectedOverride
  410. selectedTempTarget = state.currentActiveTempTarget
  411. /// Now we can show the Edit sheet
  412. state.showTempTargetEditSheet = true
  413. }
  414. }
  415. }
  416. .listRowBackground(Color.blue.opacity(0.2))
  417. }
  418. }
  419. var stickyStopButton: some View {
  420. ZStack {
  421. Rectangle()
  422. .frame(width: UIScreen.main.bounds.width, height: 65)
  423. .shadow(
  424. color: colorScheme == .dark ? Color(red: 0.02745098039, green: 0.1098039216, blue: 0.1411764706) :
  425. Color.black.opacity(0.33),
  426. radius: 3
  427. )
  428. .foregroundStyle(Color.chart)
  429. Group {
  430. switch state.selectedTab {
  431. case .overrides:
  432. Button(action: {
  433. Task {
  434. // Save cancelled Override in OverrideRunStored Entity
  435. // Cancel ALL active Override
  436. await state.disableAllActiveOverrides(createOverrideRunEntry: true)
  437. }
  438. }, label: {
  439. Text("Stop Override")
  440. .padding(10)
  441. })
  442. .frame(width: UIScreen.main.bounds.width * 0.9, alignment: .center)
  443. .disabled(!state.isEnabled)
  444. .background(!state.isEnabled ? Color(.systemGray4) : Color(.systemRed))
  445. .tint(.white)
  446. .tint(.white)
  447. .clipShape(RoundedRectangle(cornerRadius: 8))
  448. case .tempTargets:
  449. Button(action: {
  450. Task {
  451. // Save cancelled Temp Targets in TempTargetRunStored Entity
  452. // Cancel ALL active Temp Targets
  453. await state.disableAllActiveTempTargets(createTempTargetRunEntry: true)
  454. // Update View
  455. state.updateLatestTempTargetConfiguration()
  456. }
  457. }, label: {
  458. Text("Stop Temp Target")
  459. .padding(10)
  460. })
  461. .frame(width: UIScreen.main.bounds.width * 0.9, alignment: .center)
  462. .disabled(!state.isTempTargetEnabled)
  463. .background(!state.isTempTargetEnabled ? Color(.systemGray4) : Color(.systemRed))
  464. .tint(.white)
  465. .clipShape(RoundedRectangle(cornerRadius: 8))
  466. }
  467. }.padding(5)
  468. }
  469. }
  470. private var cancelAdjustmentButton: some View {
  471. switch state.selectedTab {
  472. case .overrides:
  473. Button(action: {
  474. Task {
  475. // Save cancelled Override in OverrideRunStored Entity
  476. // Cancel ALL active Override
  477. await state.disableAllActiveOverrides(createOverrideRunEntry: true)
  478. }
  479. }, label: {
  480. Text("Stop Override")
  481. })
  482. .frame(maxWidth: .infinity, alignment: .center)
  483. .disabled(!state.isEnabled)
  484. .listRowBackground(!state.isEnabled ? Color(.systemGray4) : Color(.systemRed))
  485. .tint(.white)
  486. case .tempTargets:
  487. Button(action: {
  488. Task {
  489. // Save cancelled Temp Targets in TempTargetRunStored Entity
  490. // Cancel ALL active Temp Targets
  491. await state.disableAllActiveTempTargets(createTempTargetRunEntry: true)
  492. // Update View
  493. state.updateLatestTempTargetConfiguration()
  494. }
  495. }, label: {
  496. Text("Stop Temp Target")
  497. })
  498. .frame(maxWidth: .infinity, alignment: .center)
  499. .disabled(!state.isTempTargetEnabled)
  500. .listRowBackground(!state.isTempTargetEnabled ? Color(.systemGray4) : Color(.systemRed))
  501. .tint(.white)
  502. }
  503. }
  504. private func tempTargetView(for preset: TempTargetStored) -> some View {
  505. let target = preset.target ?? 100
  506. let presetTarget = Decimal(target as! Double.RawValue)
  507. let isSelected = preset.id?.uuidString == selectedTempTargetPresetID
  508. let presetHalfBasalTarget = Decimal(
  509. preset.halfBasalTarget as? Double
  510. .RawValue ?? Double(state.settingHalfBasalTarget)
  511. )
  512. let percentage = Int(
  513. state.computeAdjustedPercentage(usingHBT: presetHalfBasalTarget, usingTarget: presetTarget)
  514. )
  515. return ZStack(alignment: .trailing, content: {
  516. HStack {
  517. VStack {
  518. HStack {
  519. Text(preset.name ?? "")
  520. Spacer()
  521. }
  522. HStack(spacing: 2) {
  523. Text(formattedGlucose(glucose: target as! Decimal))
  524. .foregroundColor(.secondary)
  525. .font(.caption)
  526. Text("for")
  527. .foregroundColor(.secondary)
  528. .font(.caption)
  529. Text("\(formatter.string(from: (preset.duration ?? 0) as NSNumber)!)")
  530. .foregroundColor(.secondary)
  531. .font(.caption)
  532. Text("min")
  533. .foregroundColor(.secondary)
  534. .font(.caption)
  535. if state.isAdjustSensEnabled(usingTarget: presetTarget) { Text(", \(percentage)%")
  536. .foregroundColor(.secondary)
  537. .font(.caption)
  538. }
  539. Spacer()
  540. }.padding(.top, 2)
  541. }
  542. .contentShape(Rectangle())
  543. .onTapGesture {
  544. Task {
  545. let objectID = preset.objectID
  546. await state.enactTempTargetPreset(withID: objectID)
  547. selectedTempTargetPresetID = preset.id?.uuidString
  548. showCheckmark.toggle()
  549. DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
  550. showCheckmark = false
  551. }
  552. }
  553. }
  554. }
  555. if showCheckmark && isSelected {
  556. // show checkmark to indicate if the preset was actually pressed
  557. Image(systemName: "checkmark.circle.fill")
  558. .imageScale(.large)
  559. .fontWeight(.bold)
  560. .foregroundStyle(Color.green)
  561. } else {
  562. Image(systemName: "line.3.horizontal")
  563. .imageScale(.medium)
  564. .foregroundStyle(.secondary)
  565. }
  566. })
  567. }
  568. private var overrideLabelDivider: some View {
  569. Divider()
  570. .frame(width: 1, height: 20)
  571. }
  572. @ViewBuilder private func overridesView(for preset: OverrideStored) -> some View {
  573. let isSelected = preset.id == selectedPresetID
  574. let name = preset.name ?? ""
  575. let indefinite = preset.indefinite
  576. let duration = preset.duration?.decimalValue ?? Decimal(0)
  577. let percentage = preset.percentage
  578. let smbMinutes = preset.smbMinutes?.decimalValue ?? Decimal(0)
  579. let uamMinutes = preset.uamMinutes?.decimalValue ?? Decimal(0)
  580. let target: String = {
  581. guard let targetValue = preset.target, targetValue != 0 else { return "" }
  582. return state.units == .mgdL ? targetValue.description : targetValue.decimalValue.formattedAsMmolL
  583. }()
  584. let targetString = target.isEmpty ? "" : "\(target) \(state.units.rawValue)"
  585. let durationString = indefinite ? "" : "\(formatHrMin(Int(duration)))"
  586. let scheduledSMBString: String = {
  587. guard preset.smbIsScheduledOff, preset.start != preset.end else { return "" }
  588. return " \(formatTimeRange(start: preset.start?.stringValue, end: preset.end?.stringValue))"
  589. }()
  590. let smbString: String = {
  591. guard preset.smbIsOff || preset.smbIsScheduledOff else { return "" }
  592. return "SMBs Off\(scheduledSMBString)"
  593. }()
  594. let maxSmbMinsString: String = {
  595. guard smbMinutes != 0, preset.advancedSettings, !preset.smbIsOff,
  596. smbMinutes != state.defaultSmbMinutes else { return "" }
  597. return "\(smbMinutes.formatted()) min SMB"
  598. }()
  599. let maxUamMinsString: String = {
  600. guard uamMinutes != 0, preset.advancedSettings, !preset.smbIsOff,
  601. uamMinutes != state.defaultUamMinutes else { return "" }
  602. return "\(uamMinutes.formatted()) min UAM"
  603. }()
  604. let isfAndCrString: String = {
  605. switch (preset.isfAndCr, preset.isf, preset.cr) {
  606. case (_, true, true),
  607. (true, _, _):
  608. return " ISF/CR"
  609. case (false, true, false):
  610. return " ISF"
  611. case (false, false, true):
  612. return " CR"
  613. default:
  614. return ""
  615. }
  616. }()
  617. let percentageString = percentage != 100 ? "\(Int(percentage))%\(isfAndCrString)" : ""
  618. // Combine all labels into a single array, filtering out empty strings
  619. let labels: [String] = [
  620. durationString,
  621. percentageString,
  622. targetString,
  623. smbString,
  624. maxSmbMinsString,
  625. maxUamMinsString
  626. ].filter { !$0.isEmpty }
  627. if !name.isEmpty {
  628. ZStack(alignment: .trailing) {
  629. HStack {
  630. VStack {
  631. HStack {
  632. Text(name)
  633. Spacer()
  634. }
  635. HStack(spacing: 5) {
  636. ForEach(labels, id: \.self) { label in
  637. Text(label)
  638. if label != labels.last { // Add divider between labels
  639. overrideLabelDivider
  640. }
  641. }
  642. Spacer()
  643. }
  644. .padding(.top, 2)
  645. .foregroundColor(.secondary)
  646. .font(.caption)
  647. }
  648. .contentShape(Rectangle())
  649. .onTapGesture {
  650. Task {
  651. let objectID = preset.objectID
  652. await state.enactOverridePreset(withID: objectID)
  653. state.hideModal()
  654. showCheckmark.toggle()
  655. selectedPresetID = preset.id
  656. // Deactivate checkmark after 3 seconds
  657. DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
  658. showCheckmark = false
  659. }
  660. }
  661. }
  662. }
  663. // show checkmark to indicate if the preset was actually pressed
  664. if showCheckmark && isSelected {
  665. Image(systemName: "checkmark.circle.fill")
  666. .imageScale(.large)
  667. .fontWeight(.bold)
  668. .foregroundStyle(Color.green)
  669. } else {
  670. Image(systemName: "line.3.horizontal")
  671. .imageScale(.medium)
  672. .foregroundStyle(.secondary)
  673. }
  674. }
  675. }
  676. }
  677. }
  678. }