PumpView.swift 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. import CoreData
  2. import SwiftUI
  3. struct PumpView: View {
  4. let reservoir: Decimal?
  5. let name: String
  6. let expiresAtDate: Date?
  7. let timerDate: Date
  8. let pumpStatusHighlightMessage: String?
  9. let battery: [OpenAPS_Battery]
  10. @Environment(\.colorScheme) var colorScheme
  11. private var batteryFormatter: NumberFormatter {
  12. let formatter = NumberFormatter()
  13. formatter.numberStyle = .percent
  14. return formatter
  15. }
  16. private var hourglassIcon: String {
  17. guard let expiration = expiresAtDate else { return "hourglass" }
  18. let hoursRemaining = expiration.timeIntervalSince(timerDate) / 3600
  19. switch hoursRemaining {
  20. case 60 ... 72:
  21. return "hourglass.bottomhalf.filled"
  22. case 12 ..< 60:
  23. return "hourglass"
  24. case -8 ..< 12:
  25. return "hourglass.tophalf.filled"
  26. default:
  27. return "hourglass"
  28. }
  29. }
  30. var body: some View {
  31. if let pumpStatusHighlightMessage = pumpStatusHighlightMessage { // display message instead pump info
  32. VStack(alignment: .center) {
  33. Text(pumpStatusHighlightMessage).font(.footnote).fontWeight(.bold)
  34. .multilineTextAlignment(.center).frame(maxWidth: /*@START_MENU_TOKEN@*/ .infinity/*@END_MENU_TOKEN@*/)
  35. }.frame(width: 100)
  36. } else {
  37. VStack(alignment: .leading, spacing: 20) {
  38. if reservoir == nil && battery.isEmpty {
  39. VStack(alignment: .center, spacing: 12) {
  40. HStack {
  41. Image(systemName: "keyboard.onehanded.left")
  42. .font(.body)
  43. .imageScale(.large)
  44. }
  45. HStack {
  46. Text("Add pump")
  47. .font(.caption)
  48. .bold()
  49. }
  50. }
  51. .frame(alignment: .top)
  52. }
  53. if let reservoir = reservoir {
  54. HStack {
  55. Image(systemName: "cross.vial.fill")
  56. .font(.callout)
  57. if reservoir == 0xDEAD_BEEF {
  58. Text("50+ " + String(localized: "U", comment: "Insulin unit"))
  59. .font(.callout)
  60. .fontWeight(.bold)
  61. .fontDesign(.rounded)
  62. } else {
  63. Text(
  64. Formatter.integerFormatter
  65. .string(from: reservoir as NSNumber)! + String(localized: " U", comment: "Insulin unit")
  66. )
  67. .font(.callout)
  68. .fontWeight(.bold)
  69. .fontDesign(.rounded)
  70. }
  71. }
  72. .padding(.vertical, 5)
  73. .padding(.horizontal, 10)
  74. .foregroundStyle(reservoirColor)
  75. .overlay(
  76. Capsule()
  77. .stroke(reservoirColor.opacity(0.4), lineWidth: 2)
  78. )
  79. }
  80. if (battery.first?.display) != nil, let shouldBatteryDisplay = battery.first?.display, shouldBatteryDisplay {
  81. HStack {
  82. Image(systemName: "battery.100")
  83. .font(.callout)
  84. .foregroundStyle(batteryColor)
  85. Text("\(Formatter.integerFormatter.string(for: battery.first?.percent ?? 100) ?? "100") %")
  86. .font(.callout).fontWeight(.bold).fontDesign(.rounded)
  87. }
  88. }
  89. if let date = expiresAtDate {
  90. HStack {
  91. Image(systemName: hourglassIcon)
  92. .font(.callout)
  93. .foregroundStyle(timerColor)
  94. let remainingTimeString = remainingTimeString(time: date.timeIntervalSince(timerDate))
  95. Text(remainingTimeString)
  96. .font(date.timeIntervalSince(timerDate) > 0 ? .callout : .subheadline)
  97. .fontWeight(.bold)
  98. .fontDesign(.rounded)
  99. .lineLimit(2)
  100. .multilineTextAlignment(.leading)
  101. .frame(
  102. // If the string is > 6 chars, i.e., exceeds "xd yh", limit width to 80 pts
  103. // This forces the "Replace pod" string to wrap to 2 lines.
  104. maxWidth: remainingTimeString.count > 6 ? 80 : .infinity,
  105. alignment: .leading
  106. )
  107. }
  108. // aligns the stopwatch icon exactly with the first pixel of the reservoir icon
  109. .padding(.leading, date.timeIntervalSince(timerDate) > 0 ? 12 : 0)
  110. }
  111. }
  112. }
  113. }
  114. private func remainingTimeString(time: TimeInterval) -> String {
  115. guard time > 0 else {
  116. return String(localized: "Replace pod", comment: "View/Header when pod expired")
  117. }
  118. var time = time
  119. let days = Int(time / 1.days.timeInterval)
  120. time -= days.days.timeInterval
  121. let hours = Int(time / 1.hours.timeInterval)
  122. time -= hours.hours.timeInterval
  123. let minutes = Int(time / 1.minutes.timeInterval)
  124. if days >= 1 {
  125. return "\(days)" + String(localized: "d", comment: "abbreviation for days") + " \(hours)" +
  126. String(localized: "h", comment: "abbreviation for hours")
  127. }
  128. if hours >= 1 {
  129. return "\(hours)" + String(localized: "h", comment: "abbreviation for hours")
  130. }
  131. return "\(minutes)" + String(localized: "m", comment: "abbreviation for minutes")
  132. }
  133. private var batteryColor: Color {
  134. guard let battery = battery.first else {
  135. return .gray
  136. }
  137. switch battery.percent {
  138. case ...10:
  139. return Color.loopRed
  140. case ...20:
  141. return Color.orange
  142. default:
  143. return Color.loopGreen
  144. }
  145. }
  146. private var reservoirColor: Color {
  147. guard let reservoir = reservoir else {
  148. return .gray
  149. }
  150. switch reservoir {
  151. case ...10:
  152. return Color.loopRed
  153. case ...30:
  154. return Color.orange
  155. default:
  156. return Color.insulin
  157. }
  158. }
  159. private var timerColor: Color {
  160. guard let expisesAt = expiresAtDate else {
  161. return .gray
  162. }
  163. let time = expisesAt.timeIntervalSince(timerDate)
  164. switch time {
  165. case ...8.hours.timeInterval:
  166. return Color.loopRed
  167. case ...1.days.timeInterval:
  168. return Color.orange
  169. default:
  170. return Color.loopGreen
  171. }
  172. }
  173. }
  174. // #Preview("message") {
  175. // PumpView(
  176. // reservoir: .constant(Decimal(10.0)),
  177. // battery: .constant(nil),
  178. // name: .constant("Pump test"),
  179. // expiresAtDate: .constant(Date().addingTimeInterval(24.hours)),
  180. // timerDate: .constant(Date()),
  181. // pumpStatusHighlightMessage: .constant("⚠️\n Insulin suspended")
  182. // )
  183. // }
  184. //
  185. // #Preview("pump reservoir") {
  186. // PumpView(
  187. // reservoir: .constant(Decimal(40.0)),
  188. // battery: .constant(Battery(percent: 50, voltage: 2.0, string: BatteryState.normal, display: true)),
  189. // name: .constant("Pump test"),
  190. // expiresAtDate: .constant(nil),
  191. // timerDate: .constant(Date().addingTimeInterval(-24.hours)),
  192. // pumpStatusHighlightMessage: .constant(nil)
  193. // )
  194. // }
  195. //
  196. // #Preview("pump expiration") {
  197. // PumpView(
  198. // reservoir: .constant(Decimal(10.0)),
  199. // battery: .constant(Battery(percent: 50, voltage: 2.0, string: BatteryState.normal, display: false)),
  200. // name: .constant("Pump test"),
  201. // expiresAtDate: .constant(Date().addingTimeInterval(2.hours)),
  202. // timerDate: .constant(Date().addingTimeInterval(2.hours)),
  203. // pumpStatusHighlightMessage: .constant(nil)
  204. // )
  205. // }
  206. //
  207. // #Preview("no pump") {
  208. // PumpView(
  209. // reservoir: .constant(nil),
  210. // name: .constant(nil),
  211. // expiresAtDate: .constant(""),
  212. // timerDate: .constant(nil),
  213. // timeZone: .constant(Date()),
  214. // pumpStatusHighlightMessage: .constant(nil)
  215. // )
  216. // }