AdjustmentsRootView.swift 31 KB

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