HomeRootView.swift 42 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042
  1. import CoreData
  2. import SpriteKit
  3. import SwiftDate
  4. import SwiftUI
  5. import Swinject
  6. extension Home {
  7. struct RootView: BaseView {
  8. let resolver: Resolver
  9. @ObservedObject var appState = AppState()
  10. @StateObject var state = StateModel()
  11. @State var isStatusPopupPresented = false
  12. @State var showCancelAlert = false
  13. @State var isMenuPresented = false
  14. @State var selectedTab: Int = 0
  15. @State var currentTab: Tab
  16. struct Buttons: Identifiable {
  17. let label: String
  18. let number: String
  19. var active: Bool
  20. let hours: Int16
  21. var id: String { label }
  22. }
  23. @State var timeButtons: [Buttons] = [
  24. Buttons(label: "2 hours", number: "2", active: false, hours: 2),
  25. Buttons(label: "4 hours", number: "4", active: false, hours: 4),
  26. Buttons(label: "6 hours", number: "6", active: false, hours: 6),
  27. Buttons(label: "12 hours", number: "12", active: false, hours: 12),
  28. Buttons(label: "24 hours", number: "24", active: false, hours: 24)
  29. ]
  30. let buttonFont = Font.custom("TimeButtonFont", size: 14)
  31. @Environment(\.managedObjectContext) var moc
  32. @Environment(\.colorScheme) var colorScheme
  33. @FetchRequest(
  34. entity: Override.entity(),
  35. sortDescriptors: [NSSortDescriptor(key: "date", ascending: false)]
  36. ) var fetchedPercent: FetchedResults<Override>
  37. @FetchRequest(
  38. entity: OverridePresets.entity(),
  39. sortDescriptors: [NSSortDescriptor(key: "name", ascending: true)], predicate: NSPredicate(
  40. format: "name != %@", "" as String
  41. )
  42. ) var fetchedProfiles: FetchedResults<OverridePresets>
  43. @FetchRequest(
  44. entity: TempTargets.entity(),
  45. sortDescriptors: [NSSortDescriptor(key: "date", ascending: false)]
  46. ) var sliderTTpresets: FetchedResults<TempTargets>
  47. @FetchRequest(
  48. entity: TempTargetsSlider.entity(),
  49. sortDescriptors: [NSSortDescriptor(key: "date", ascending: false)]
  50. ) var enactedSliderTT: FetchedResults<TempTargetsSlider>
  51. var bolusProgressFormatter: NumberFormatter {
  52. let formatter = NumberFormatter()
  53. formatter.numberStyle = .decimal
  54. formatter.minimum = 0
  55. formatter.maximumFractionDigits = state.settingsManager.preferences.bolusIncrement > 0.05 ? 1 : 2
  56. formatter.minimumFractionDigits = state.settingsManager.preferences.bolusIncrement > 0.05 ? 1 : 2
  57. formatter.allowsFloats = true
  58. formatter.roundingIncrement = Double(state.settingsManager.preferences.bolusIncrement) as NSNumber
  59. return formatter
  60. }
  61. private var numberFormatter: NumberFormatter {
  62. let formatter = NumberFormatter()
  63. formatter.numberStyle = .decimal
  64. formatter.maximumFractionDigits = 2
  65. return formatter
  66. }
  67. private var fetchedTargetFormatter: NumberFormatter {
  68. let formatter = NumberFormatter()
  69. formatter.numberStyle = .decimal
  70. if state.units == .mmolL {
  71. formatter.maximumFractionDigits = 1
  72. } else { formatter.maximumFractionDigits = 0 }
  73. return formatter
  74. }
  75. private var targetFormatter: NumberFormatter {
  76. let formatter = NumberFormatter()
  77. formatter.numberStyle = .decimal
  78. formatter.maximumFractionDigits = 1
  79. return formatter
  80. }
  81. private var tirFormatter: NumberFormatter {
  82. let formatter = NumberFormatter()
  83. formatter.numberStyle = .decimal
  84. formatter.maximumFractionDigits = 0
  85. return formatter
  86. }
  87. private var dateFormatter: DateFormatter {
  88. let dateFormatter = DateFormatter()
  89. dateFormatter.timeStyle = .short
  90. return dateFormatter
  91. }
  92. private var spriteScene: SKScene {
  93. let scene = SnowScene()
  94. scene.scaleMode = .resizeFill
  95. scene.backgroundColor = .clear
  96. return scene
  97. }
  98. private var color: LinearGradient {
  99. colorScheme == .dark ? LinearGradient(
  100. gradient: Gradient(colors: [
  101. Color.bgDarkBlue,
  102. Color.bgDarkBlue,
  103. Color.bgDarkerDarkBlue,
  104. Color.bgDarkBlue
  105. ]),
  106. startPoint: .top,
  107. endPoint: .bottom
  108. )
  109. :
  110. LinearGradient(
  111. gradient: Gradient(colors: [Color.gray.opacity(0.1)]),
  112. startPoint: .top,
  113. endPoint: .bottom
  114. )
  115. }
  116. private var historySFSymbol: String {
  117. if #available(iOS 17.0, *) {
  118. return "book.pages"
  119. } else {
  120. return "book"
  121. }
  122. }
  123. var glucoseView: some View {
  124. CurrentGlucoseView(
  125. recentGlucose: $state.recentGlucose,
  126. timerDate: $state.timerDate,
  127. delta: $state.glucoseDelta,
  128. units: $state.units,
  129. alarm: $state.alarm,
  130. lowGlucose: $state.lowGlucose,
  131. highGlucose: $state.highGlucose
  132. ).scaleEffect(0.9)
  133. /*
  134. .onTapGesture {
  135. if state.alarm == nil {
  136. state.openCGM()
  137. } else {
  138. state.showModal(for: .snooze)
  139. }
  140. }
  141. .onLongPressGesture {
  142. let impactHeavy = UIImpactFeedbackGenerator(style: .heavy)
  143. impactHeavy.impactOccurred()
  144. if state.alarm == nil {
  145. state.showModal(for: .snooze)
  146. } else {
  147. state.openCGM()
  148. }
  149. }
  150. */
  151. }
  152. var pumpView: some View {
  153. PumpView(
  154. reservoir: $state.reservoir,
  155. battery: $state.battery,
  156. name: $state.pumpName,
  157. expiresAtDate: $state.pumpExpiresAtDate,
  158. timerDate: $state.timerDate,
  159. timeZone: $state.timeZone,
  160. state: state
  161. )
  162. }
  163. var tempBasalString: String? {
  164. guard let tempRate = state.tempRate else {
  165. return nil
  166. }
  167. let rateString = numberFormatter.string(from: tempRate as NSNumber) ?? "0"
  168. var manualBasalString = ""
  169. if state.apsManager.isManualTempBasal {
  170. manualBasalString = NSLocalizedString(
  171. " - Manual Basal ⚠️",
  172. comment: "Manual Temp basal"
  173. )
  174. }
  175. return rateString + " " + NSLocalizedString(" U/hr", comment: "Unit per hour with space") + manualBasalString
  176. }
  177. var tempTargetString: String? {
  178. guard let tempTarget = state.tempTarget else {
  179. return nil
  180. }
  181. let target = tempTarget.targetBottom ?? 0
  182. let unitString = targetFormatter.string(from: (tempTarget.targetBottom?.asMmolL ?? 0) as NSNumber) ?? ""
  183. let rawString = (tirFormatter.string(from: (tempTarget.targetBottom ?? 0) as NSNumber) ?? "") + " " + state.units
  184. .rawValue
  185. var string = ""
  186. if sliderTTpresets.first?.active ?? false {
  187. let hbt = sliderTTpresets.first?.hbt ?? 0
  188. string = ", " + (tirFormatter.string(from: state.infoPanelTTPercentage(hbt, target) as NSNumber) ?? "") + " %"
  189. }
  190. let percentString = state
  191. .units == .mmolL ? (unitString + " mmol/L" + string) : (rawString + (string == "0" ? "" : string))
  192. return tempTarget.displayName + " " + percentString
  193. }
  194. var overrideString: String? {
  195. guard fetchedPercent.first?.enabled ?? false else {
  196. return nil
  197. }
  198. var percentString = "\((fetchedPercent.first?.percentage ?? 100).formatted(.number)) %"
  199. var target = (fetchedPercent.first?.target ?? 100) as Decimal
  200. let indefinite = (fetchedPercent.first?.indefinite ?? false)
  201. let unit = state.units.rawValue
  202. if state.units == .mmolL {
  203. target = target.asMmolL
  204. }
  205. var targetString = (fetchedTargetFormatter.string(from: target as NSNumber) ?? "") + " " + unit
  206. if tempTargetString != nil || target == 0 { targetString = "" }
  207. percentString = percentString == "100 %" ? "" : percentString
  208. let duration = (fetchedPercent.first?.duration ?? 0) as Decimal
  209. let addedMinutes = Int(duration)
  210. let date = fetchedPercent.first?.date ?? Date()
  211. var newDuration: Decimal = 0
  212. if date.addingTimeInterval(addedMinutes.minutes.timeInterval) > Date() {
  213. newDuration = Decimal(Date().distance(to: date.addingTimeInterval(addedMinutes.minutes.timeInterval)).minutes)
  214. }
  215. var durationString = indefinite ?
  216. "" : newDuration >= 1 ?
  217. (newDuration.formatted(.number.grouping(.never).rounded().precision(.fractionLength(0))) + " min") :
  218. (
  219. newDuration > 0 ? (
  220. (newDuration * 60).formatted(.number.grouping(.never).rounded().precision(.fractionLength(0))) + " s"
  221. ) :
  222. ""
  223. )
  224. let smbToggleString = (fetchedPercent.first?.smbIsOff ?? false) ? " \u{20e0}" : ""
  225. var comma1 = ", "
  226. var comma2 = comma1
  227. var comma3 = comma1
  228. if targetString == "" || percentString == "" { comma1 = "" }
  229. if durationString == "" { comma2 = "" }
  230. if smbToggleString == "" { comma3 = "" }
  231. if percentString == "", targetString == "" {
  232. comma1 = ""
  233. comma2 = ""
  234. }
  235. if percentString == "", targetString == "", smbToggleString == "" {
  236. durationString = ""
  237. comma1 = ""
  238. comma2 = ""
  239. comma3 = ""
  240. }
  241. if durationString == "" {
  242. comma2 = ""
  243. }
  244. if smbToggleString == "" {
  245. comma3 = ""
  246. }
  247. if durationString == "", !indefinite {
  248. return nil
  249. }
  250. return percentString + comma1 + targetString + comma2 + durationString + comma3 + smbToggleString
  251. }
  252. var infoPanel: some View {
  253. HStack(alignment: .center) {
  254. if state.pumpSuspended {
  255. Text("Pump suspended")
  256. .font(.system(size: 15, weight: .bold)).foregroundColor(.loopGray)
  257. .padding(.leading, 8)
  258. } else if let tempBasalString = tempBasalString {
  259. Text(tempBasalString)
  260. .font(.system(size: 15, weight: .bold))
  261. .foregroundColor(.insulin)
  262. .padding(.leading, 8)
  263. }
  264. if state.tins {
  265. Text(
  266. "TINS: \(state.calculateTINS())" +
  267. NSLocalizedString(" U", comment: "Unit in number of units delivered (keep the space character!)")
  268. )
  269. .font(.system(size: 15, weight: .bold))
  270. .foregroundColor(.insulin)
  271. }
  272. if let tempTargetString = tempTargetString {
  273. Text(tempTargetString)
  274. .font(.caption)
  275. .foregroundColor(.secondary)
  276. }
  277. Spacer()
  278. if state.closedLoop, state.settingsManager.preferences.maxIOB == 0 {
  279. Text("Max IOB: 0").font(.callout).foregroundColor(.orange).padding(.trailing, 20)
  280. }
  281. }
  282. .frame(maxWidth: .infinity, maxHeight: 30)
  283. }
  284. var timeInterval: some View {
  285. HStack(alignment: .center) {
  286. ForEach(timeButtons) { button in
  287. Text(button.active ? NSLocalizedString(button.label, comment: "") : button.number).onTapGesture {
  288. state.hours = button.hours
  289. }
  290. .foregroundStyle(button.active ? (colorScheme == .dark ? Color.white : Color.black).opacity(0.9) : .secondary)
  291. .frame(maxHeight: 30).padding(.horizontal, 8)
  292. .background(
  293. button.active ?
  294. // RGB(30, 60, 95)
  295. (
  296. colorScheme == .dark ? Color(red: 0.1176470588, green: 0.2352941176, blue: 0.3725490196) :
  297. Color.white
  298. ) :
  299. Color
  300. .clear
  301. )
  302. .cornerRadius(20)
  303. }
  304. }
  305. .shadow(
  306. color: Color.black.opacity(colorScheme == .dark ? 0.75 : 0.33),
  307. radius: colorScheme == .dark ? 5 : 3
  308. )
  309. .font(buttonFont)
  310. }
  311. var mainChart: some View {
  312. ZStack {
  313. if state.animatedBackground {
  314. SpriteView(scene: spriteScene, options: [.allowsTransparency])
  315. .ignoresSafeArea()
  316. .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
  317. }
  318. MainChartView(
  319. glucose: $state.glucose,
  320. units: $state.units,
  321. eventualBG: $state.eventualBG,
  322. suggestion: $state.suggestion,
  323. tempBasals: $state.tempBasals,
  324. boluses: $state.boluses,
  325. suspensions: $state.suspensions,
  326. announcement: $state.announcement,
  327. hours: .constant(state.filteredHours),
  328. maxBasal: $state.maxBasal,
  329. autotunedBasalProfile: $state.autotunedBasalProfile,
  330. basalProfile: $state.basalProfile,
  331. tempTargets: $state.tempTargets,
  332. carbs: $state.carbs,
  333. smooth: $state.smooth,
  334. highGlucose: $state.highGlucose,
  335. lowGlucose: $state.lowGlucose,
  336. screenHours: $state.hours,
  337. displayXgridLines: $state.displayXgridLines,
  338. displayYgridLines: $state.displayYgridLines,
  339. thresholdLines: $state.thresholdLines,
  340. isTempTargetActive: $state.isTempTargetActive
  341. )
  342. }
  343. .padding(.bottom)
  344. }
  345. func highlightButtons() {
  346. for i in 0 ..< timeButtons.count {
  347. timeButtons[i].active = timeButtons[i].hours == state.hours
  348. }
  349. }
  350. @ViewBuilder private func bottomPanel(_: GeometryProxy) -> some View {
  351. let colorIcon: Color = (colorScheme == .dark ? Color.white : Color.black).opacity(0.9)
  352. ZStack {
  353. Rectangle()
  354. .fill(Color("Chart"))
  355. .frame(height: UIScreen.main.bounds.height / 13)
  356. .cornerRadius(15)
  357. .shadow(
  358. color: colorScheme == .dark ? Color(red: 0.02745098039, green: 0.1098039216, blue: 0.1411764706) : Color
  359. .black.opacity(0.33),
  360. radius: 3
  361. )
  362. .padding([.leading, .trailing], 10)
  363. HStack {
  364. Button {
  365. state.showModal(for: .dataTable)
  366. }
  367. label: {
  368. if #available(iOS 17.0, *) {
  369. Image(systemName: "book.pages")
  370. .font(.system(size: 24))
  371. .foregroundColor(colorIcon)
  372. .padding(8)
  373. } else {
  374. Image(systemName: "book")
  375. .font(.system(size: 24))
  376. .foregroundColor(colorIcon)
  377. .padding(8)
  378. }
  379. }
  380. .foregroundColor(colorIcon)
  381. .buttonStyle(.borderless)
  382. Spacer()
  383. Button { state.showModal(for: .addCarbs(editMode: false, override: false)) }
  384. label: {
  385. ZStack(alignment: Alignment(horizontal: .trailing, vertical: .bottom)) {
  386. Image(systemName: "fork.knife")
  387. .font(.system(size: 24))
  388. .foregroundColor(colorIcon)
  389. .padding(8)
  390. if let carbsReq = state.carbsRequired {
  391. Text(numberFormatter.string(from: carbsReq as NSNumber)!)
  392. .font(.caption)
  393. .foregroundColor(.white)
  394. .padding(4)
  395. .background(Capsule().fill(Color.red))
  396. }
  397. }
  398. }.buttonStyle(.borderless)
  399. Spacer()
  400. Button {
  401. state.showModal(for: .bolus(
  402. waitForSuggestion: true,
  403. fetch: false
  404. ))
  405. }
  406. label: {
  407. Image(systemName: "syringe.fill")
  408. .font(.system(size: 24))
  409. .foregroundColor(colorIcon)
  410. .padding(8)
  411. }
  412. .foregroundColor(colorIcon)
  413. .buttonStyle(.borderless)
  414. Spacer()
  415. if state.allowManualTemp {
  416. Button { state.showModal(for: .manualTempBasal) }
  417. label: {
  418. Image("bolus1")
  419. .renderingMode(.template)
  420. .resizable()
  421. .frame(width: 24, height: 24)
  422. .padding(8)
  423. }
  424. .foregroundColor(colorIcon)
  425. .buttonStyle(.borderless)
  426. Spacer()
  427. }
  428. let isOverrideActive = fetchedPercent.first?.enabled ?? false
  429. Button {
  430. state.showModal(for: .overrideProfilesConfig)
  431. } label: {
  432. Image(systemName: (state.isTempTargetActive || isOverrideActive) ? "person.fill" : "person")
  433. .font(.system(size: 26))
  434. .padding(8)
  435. }
  436. .foregroundColor((state.isTempTargetActive || isOverrideActive) ? Color.purple : colorIcon)
  437. .buttonStyle(.borderless)
  438. Spacer()
  439. Button {
  440. state.showModal(for: .settings)
  441. } label: {
  442. Image(systemName: "gear")
  443. .font(.system(size: 26))
  444. .padding(8)
  445. }
  446. .foregroundColor(colorIcon)
  447. .buttonStyle(.borderless)
  448. }
  449. .padding(.horizontal, 24)
  450. .padding(.bottom, 16)
  451. }
  452. }
  453. @ViewBuilder func bolusProgressBar(_ progress: Decimal) -> some View {
  454. GeometryReader { geo in
  455. Rectangle()
  456. .frame(height: 6)
  457. .foregroundColor(.clear)
  458. .background(
  459. LinearGradient(colors: [
  460. Color(red: 0.7215686275, green: 0.3411764706, blue: 1),
  461. Color(red: 0.6235294118, green: 0.4235294118, blue: 0.9803921569),
  462. Color(red: 0.4862745098, green: 0.5450980392, blue: 0.9529411765),
  463. Color(red: 0.3411764706, green: 0.6666666667, blue: 0.9254901961),
  464. Color(red: 0.262745098, green: 0.7333333333, blue: 0.9137254902)
  465. ], startPoint: .leading, endPoint: .trailing)
  466. .mask(alignment: .leading) {
  467. Rectangle()
  468. .frame(width: geo.size.width * CGFloat(progress))
  469. }
  470. )
  471. }
  472. }
  473. @ViewBuilder func bolusProgressView(_: GeometryProxy, _ progress: Decimal) -> some View {
  474. let colorRectangle: Color = colorScheme == .dark ? Color(
  475. "Chart"
  476. ) : Color.white
  477. let colorIcon = (colorScheme == .dark ? Color.white : Color.black).opacity(0.9)
  478. let bolusTotal = state.boluses.last?.amount ?? 0
  479. let bolusFraction = progress * bolusTotal
  480. let bolusString =
  481. (bolusProgressFormatter.string(from: bolusFraction as NSNumber) ?? "0")
  482. + " of " +
  483. (numberFormatter.string(from: bolusTotal as NSNumber) ?? "0")
  484. + NSLocalizedString(" U", comment: "Insulin unit")
  485. ZStack(alignment: .bottom) {
  486. HStack {
  487. Button {
  488. state.cancelBolus()
  489. } label: {
  490. HStack(alignment: .center) {
  491. Text("Bolusing")
  492. .font(.subheadline)
  493. .fontWeight(.bold)
  494. Text(bolusString)
  495. .font(.subheadline)
  496. Spacer()
  497. Image(systemName: "xmark.app")
  498. .font(.system(size: 30))
  499. .padding(1)
  500. }
  501. }.foregroundColor(colorIcon)
  502. }.padding()
  503. bolusProgressBar(progress).offset(y: 59)
  504. }
  505. .background(colorRectangle)
  506. .clipShape(RoundedRectangle(cornerRadius: 8))
  507. .shadow(
  508. color: colorScheme == .dark ? Color(red: 0.02745098039, green: 0.1098039216, blue: 0.1411764706) :
  509. Color.black.opacity(0.33),
  510. radius: 3
  511. )
  512. .frame(height: 62, alignment: .center)
  513. .padding(.horizontal, 10)
  514. .offset(y: -90)
  515. }
  516. @ViewBuilder func rightHeaderPanel(_: GeometryProxy) -> some View {
  517. VStack(alignment: .leading, spacing: 20) {
  518. /// Loop view at bottomLeading
  519. LoopView(
  520. suggestion: $state.suggestion,
  521. enactedSuggestion: $state.enactedSuggestion,
  522. closedLoop: $state.closedLoop,
  523. timerDate: $state.timerDate,
  524. isLooping: $state.isLooping,
  525. lastLoopDate: $state.lastLoopDate,
  526. manualTempBasal: $state.manualTempBasal
  527. ).onTapGesture {
  528. state.isStatusPopupPresented = true
  529. }.onLongPressGesture {
  530. let impactHeavy = UIImpactFeedbackGenerator(style: .heavy)
  531. impactHeavy.impactOccurred()
  532. state.runLoop()
  533. }
  534. /// eventualBG string at bottomTrailing
  535. if let eventualBG = state.eventualBG {
  536. HStack {
  537. Image(systemName: "arrow.right.circle")
  538. .font(.system(size: 16, weight: .bold))
  539. Text(
  540. numberFormatter.string(
  541. from: (
  542. state.units == .mmolL ? eventualBG
  543. .asMmolL : Decimal(eventualBG)
  544. ) as NSNumber
  545. )!
  546. )
  547. .font(.system(size: 16))
  548. }
  549. }
  550. }
  551. }
  552. @ViewBuilder func mealPanel(_: GeometryProxy) -> some View {
  553. HStack {
  554. HStack {
  555. Image(systemName: "syringe.fill")
  556. .font(.system(size: 16))
  557. .foregroundColor(Color.insulin)
  558. Text(
  559. (numberFormatter.string(from: (state.suggestion?.iob ?? 0) as NSNumber) ?? "0") +
  560. NSLocalizedString(" U", comment: "Insulin unit")
  561. )
  562. .font(.system(size: 16, weight: .bold))
  563. }
  564. Spacer()
  565. HStack {
  566. Image(systemName: "fork.knife")
  567. .font(.system(size: 16))
  568. .foregroundColor(.loopYellow)
  569. Text(
  570. (numberFormatter.string(from: (state.suggestion?.cob ?? 0) as NSNumber) ?? "0") +
  571. NSLocalizedString(" g", comment: "gram of carbs")
  572. )
  573. .font(.system(size: 16, weight: .bold))
  574. }
  575. Spacer()
  576. HStack {
  577. if state.pumpSuspended {
  578. Text("Pump suspended")
  579. .font(.system(size: 12, weight: .bold)).foregroundColor(.loopGray)
  580. } else if let tempBasalString = tempBasalString {
  581. Image(systemName: "drop.circle")
  582. .font(.system(size: 16))
  583. .foregroundColor(.insulinTintColor)
  584. Text(tempBasalString)
  585. .font(.system(size: 16, weight: .bold))
  586. }
  587. }
  588. if !state.tins {
  589. Spacer()
  590. Text(
  591. "TDD: " + (numberFormatter.string(from: (state.suggestion?.tdd ?? 0) as NSNumber) ?? "0") +
  592. NSLocalizedString(" U", comment: "Insulin unit")
  593. )
  594. .font(.system(size: 16, weight: .bold))
  595. } else {
  596. Spacer()
  597. HStack {
  598. Text(
  599. "TINS: \(state.roundedTotalBolus)" +
  600. NSLocalizedString(" U", comment: "Unit in number of units delivered (keep the space character!)")
  601. )
  602. .font(.system(size: 16, weight: .bold))
  603. .onChange(of: state.hours) { _ in
  604. state.roundedTotalBolus = state.calculateTINS()
  605. }
  606. .onAppear {
  607. DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
  608. state.roundedTotalBolus = state.calculateTINS()
  609. }
  610. }
  611. }
  612. }
  613. }.padding(.horizontal, 10)
  614. }
  615. @ViewBuilder func profileView(_: GeometryProxy) -> some View {
  616. let colourChart: Color = colorScheme == .dark ? Color(
  617. "Chart"
  618. ) : .white
  619. if let overrideString = overrideString {
  620. ZStack {
  621. /// rectangle as background
  622. RoundedRectangle(cornerRadius: 15)
  623. .fill(colourChart)
  624. .clipShape(RoundedRectangle(cornerRadius: 15))
  625. .frame(height: UIScreen.main.bounds.height / 20)
  626. .shadow(
  627. color:
  628. Color(red: 0.7215686275, green: 0.3411764706, blue: 1),
  629. radius: 1
  630. )
  631. HStack {
  632. /// actual profile view
  633. Image(systemName: "person.fill")
  634. .font(.system(size: 20))
  635. .foregroundStyle(Color.purple)
  636. Spacer()
  637. Text(overrideString)
  638. .font(.system(size: 18))
  639. Spacer()
  640. Image(systemName: "xmark.app")
  641. .font(.system(size: 20))
  642. }.padding(.horizontal, 10)
  643. .alert(
  644. "Return to Normal?", isPresented: $showCancelAlert,
  645. actions: {
  646. Button("No", role: .cancel) {}
  647. Button("Yes", role: .destructive) {
  648. state.cancelProfile()
  649. }
  650. }, message: { Text("This will change settings back to your normal profile.") }
  651. )
  652. .padding(.trailing, 8)
  653. .onTapGesture {
  654. showCancelAlert = true
  655. }
  656. }.padding(.horizontal, 10)
  657. }
  658. /// just show temp target if no profile is already active
  659. if overrideString == nil, let tempTargetString = tempTargetString {
  660. ZStack {
  661. /// rectangle as background
  662. RoundedRectangle(cornerRadius: 15)
  663. .fill(colourChart)
  664. .clipShape(RoundedRectangle(cornerRadius: 15))
  665. .frame(height: UIScreen.main.bounds.height / 20)
  666. .shadow(
  667. color:
  668. Color(red: 0.7215686275, green: 0.3411764706, blue: 1),
  669. radius: 1
  670. )
  671. HStack {
  672. Image(systemName: "person.fill")
  673. .font(.system(size: 20))
  674. .foregroundStyle(Color.purple)
  675. Spacer()
  676. Text(tempTargetString)
  677. .font(.system(size: 15))
  678. Spacer()
  679. }.padding(.horizontal, 10)
  680. }.padding(.horizontal, 10)
  681. }
  682. }
  683. @ViewBuilder func menuSymbols(action: @escaping () -> Void, systemName: String) -> some View {
  684. Button(
  685. action: action,
  686. label: {
  687. HStack {
  688. Image(systemName: systemName)
  689. .font(.system(size: 21))
  690. .foregroundStyle(colorScheme == .dark ? .white : .black)
  691. }.padding(.top, 1)
  692. }
  693. )
  694. }
  695. @ViewBuilder func menuElements(action: @escaping () -> Void, title: String) -> some View {
  696. Button(
  697. action: action,
  698. label: {
  699. HStack {
  700. Text(title)
  701. .font(.system(size: 19))
  702. .foregroundStyle(colorScheme == .dark ? .white : .black)
  703. Spacer()
  704. Image(systemName: "arrow.right")
  705. .font(.system(size: 21))
  706. .foregroundStyle(colorScheme == .dark ? .white : .black)
  707. }.padding(.top, 1)
  708. }
  709. )
  710. }
  711. @ViewBuilder func sideMenuView() -> some View {
  712. ZStack {
  713. RoundedRectangle(cornerRadius: 8)
  714. .fill(color)
  715. .shadow(
  716. color: Color.black.opacity(0.33),
  717. radius: 3
  718. )
  719. .ignoresSafeArea(edges: .all)
  720. VStack(alignment: .leading) {
  721. Button {
  722. isMenuPresented.toggle()
  723. } label: {
  724. HStack {
  725. Image(systemName: "arrow.left")
  726. .font(.system(size: 30))
  727. .foregroundStyle(colorScheme == .dark ? .white : .black)
  728. Text("Menu")
  729. .font(.system(size: 30)).fontWeight(.bold)
  730. .foregroundStyle(colorScheme == .dark ? .white : .black)
  731. }
  732. }
  733. .padding(.top, 60)
  734. HStack(spacing: 15) {
  735. VStack(alignment: .leading, spacing: 25, content: {
  736. menuSymbols(action: { state.showModal(for: .statistics) }, systemName: "chart.bar.xaxis")
  737. .padding(.top, 20)
  738. menuSymbols(action: {
  739. if state.pumpDisplayState != nil {
  740. state.setupPump = true
  741. }
  742. }, systemName: "cross.vial.fill")
  743. menuSymbols(action: {
  744. if state.alarm == nil {
  745. state.openCGM()
  746. } else {
  747. state.showModal(for: .snooze)
  748. }
  749. }, systemName: "sensor.tag.radiowaves.forward.fill")
  750. menuSymbols(action: { state.showModal(for: .addTempTarget) }, systemName: "target")
  751. Spacer()
  752. })
  753. VStack(alignment: .leading, spacing: 25, content: {
  754. menuElements(action: { state.showModal(for: .statistics) }, title: "Statistics")
  755. .padding(.top, 20)
  756. menuElements(action: {
  757. if state.pumpDisplayState != nil {
  758. state.setupPump = true
  759. }
  760. }, title: "Pump Settings")
  761. menuElements(action: {
  762. if state.alarm == nil {
  763. state.openCGM()
  764. } else {
  765. state.showModal(for: .snooze)
  766. }
  767. }, title: "CGM")
  768. menuElements(action: { state.showModal(for: .addTempTarget) }, title: "Temp targets")
  769. Spacer()
  770. })
  771. }
  772. }.padding(.horizontal, 25)
  773. }
  774. .frame(width: UIScreen.main.bounds.width / 1.2, height: UIScreen.main.bounds.height - 20)
  775. }
  776. @ViewBuilder func mainView() -> some View {
  777. GeometryReader { geo in
  778. ZStack(alignment: .trailing) {
  779. VStack(spacing: 0) {
  780. Spacer()
  781. ZStack {
  782. /// glucose bobble
  783. glucoseView
  784. /// right panel with loop status and evBG
  785. HStack {
  786. Spacer()
  787. rightHeaderPanel(geo)
  788. }.padding(.trailing, 20)
  789. /// left panel with pump related info
  790. HStack {
  791. pumpView
  792. Spacer()
  793. }.padding(.leading, 20)
  794. HStack {
  795. Spacer()
  796. Button {
  797. isMenuPresented.toggle()
  798. }
  799. label: {
  800. Image(systemName: "text.justify")
  801. .font(.body).foregroundStyle(colorScheme == .dark ? Color.white : Color.black)
  802. }.padding(.trailing, 20).padding(.bottom, 110)
  803. }
  804. }.padding(.top, 40)
  805. Spacer()
  806. mealPanel(geo)
  807. Spacer()
  808. profileView(geo).padding(.vertical)
  809. RoundedRectangle(cornerRadius: 15)
  810. .fill(Color("Chart"))
  811. .overlay(mainChart)
  812. .clipShape(RoundedRectangle(cornerRadius: 15))
  813. .shadow(
  814. color: colorScheme == .dark ? Color(red: 0.02745098039, green: 0.1098039216, blue: 0.1411764706) :
  815. Color.black.opacity(0.33),
  816. radius: 3
  817. )
  818. .padding(.horizontal, 10)
  819. .frame(maxHeight: UIScreen.main.bounds.height / 2.1)
  820. Spacer()
  821. timeInterval
  822. Spacer()
  823. ZStack(alignment: .bottom) {
  824. bottomPanel(geo)
  825. if let progress = state.bolusProgress {
  826. bolusProgressView(geo, progress)
  827. }
  828. }
  829. }
  830. }
  831. .background(color)
  832. .blur(radius: isMenuPresented ? 5 : 0)
  833. .edgesIgnoringSafeArea(.all)
  834. }
  835. .onChange(of: state.hours) { _ in
  836. highlightButtons()
  837. }
  838. .onAppear {
  839. configureView {
  840. highlightButtons()
  841. }
  842. }
  843. .navigationTitle("Home")
  844. .navigationBarHidden(true)
  845. .ignoresSafeArea(.keyboard)
  846. .popup(isPresented: state.isStatusPopupPresented, alignment: .top, direction: .top) {
  847. popup
  848. .padding()
  849. .background(
  850. RoundedRectangle(cornerRadius: 8, style: .continuous)
  851. .fill(colorScheme == .dark ? Color(
  852. "Chart"
  853. ) : Color(UIColor.darkGray))
  854. )
  855. .onTapGesture {
  856. state.isStatusPopupPresented = false
  857. }
  858. .gesture(
  859. DragGesture(minimumDistance: 10, coordinateSpace: .local)
  860. .onEnded { value in
  861. if value.translation.height < 0 {
  862. state.isStatusPopupPresented = false
  863. }
  864. }
  865. )
  866. }
  867. }
  868. // @ViewBuilder func tabBar() -> some View {
  869. // TabView(selection: $appState.currentTab) {
  870. // mainView()
  871. // .tabItem { Label("Home", systemImage: "house") }
  872. // .tag(Tab.home)
  873. //
  874. // NavigationStack { DataTable.RootView(resolver: resolver) }
  875. // .tabItem { Label("History", systemImage: historySFSymbol) }
  876. // .tag(Tab.history)
  877. //
  878. // NavigationStack { AddCarbs.RootView(resolver: resolver, editMode: false, override: false) }
  879. // .tabItem { Label("Carbs", systemImage: "fork.knife") }
  880. // .tag(Tab.carbs)
  881. //
  882. // NavigationStack { Bolus.RootView(resolver: resolver, waitForSuggestion: false, fetch: false, appState: appState)
  883. // }
  884. // .tabItem { Label("Bolus", systemImage: "syringe.fill") }
  885. // .tag(Tab.bolus)
  886. //
  887. // NavigationStack { OverrideProfilesConfig.RootView(resolver: resolver) }
  888. // .tabItem {
  889. // Label(
  890. // "Profile",
  891. // systemImage: state.isTempTargetActive || overrideString != nil ? "person.fill" : "person"
  892. // ) }
  893. // .tag(Tab.profile)
  894. // }.tint(Color.tabBar)
  895. // }
  896. var body: some View {
  897. ZStack(alignment: .trailing) {
  898. mainView()
  899. // burger menu
  900. if isMenuPresented {
  901. HStack {
  902. sideMenuView().background(Color.chart).ignoresSafeArea(.all)
  903. }
  904. }
  905. }
  906. }
  907. private var popup: some View {
  908. VStack(alignment: .leading, spacing: 4) {
  909. Text(state.statusTitle).font(.headline).foregroundColor(.white)
  910. .padding(.bottom, 4)
  911. if let suggestion = state.suggestion {
  912. TagCloudView(tags: suggestion.reasonParts).animation(.none, value: false)
  913. Text(suggestion.reasonConclusion.capitalizingFirstLetter()).font(.caption).foregroundColor(.white)
  914. } else {
  915. Text("No sugestion found").font(.body).foregroundColor(.white)
  916. }
  917. if let errorMessage = state.errorMessage, let date = state.errorDate {
  918. Text(NSLocalizedString("Error at", comment: "") + " " + dateFormatter.string(from: date))
  919. .foregroundColor(.white)
  920. .font(.headline)
  921. .padding(.bottom, 4)
  922. .padding(.top, 8)
  923. Text(errorMessage).font(.caption).foregroundColor(.loopRed)
  924. } else if let suggestion = state.suggestion, (suggestion.bg ?? 100) == 400 {
  925. Text("Invalid CGM reading (HIGH).").font(.callout).bold().foregroundColor(.loopRed).padding(.top, 8)
  926. Text("SMBs and High Temps Disabled.").font(.caption).foregroundColor(.white).padding(.bottom, 4)
  927. }
  928. }
  929. }
  930. }
  931. }
  932. class AppState: ObservableObject {
  933. @Published var currentTab: Tab = .home
  934. }
  935. enum Tab {
  936. case home
  937. case history
  938. case carbs
  939. case bolus
  940. case profile
  941. }