// // MinimedPumpSettingsView.swift // MinimedKitUI // // Created by Pete Schwamb on 11/29/22. // Copyright © 2022 LoopKit Authors. All rights reserved. // import Foundation import SwiftUI import LoopKitUI import LoopKit import RileyLinkKit import RileyLinkBLEKit struct MinimedPumpSettingsView: View { @Environment(\.guidanceColors) private var guidanceColors @Environment(\.insulinTintColor) var insulinTintColor @ObservedObject var viewModel: MinimedPumpSettingsViewModel @ObservedObject var rileyLinkListDataSource: RileyLinkListDataSource var supportedInsulinTypes: [InsulinType] @State private var showingDeletionSheet = false @State private var showSyncTimeOptions = false; var handleRileyLinkSelection: (RileyLinkDevice) -> Void init(viewModel: MinimedPumpSettingsViewModel, supportedInsulinTypes: [InsulinType], handleRileyLinkSelection: @escaping (RileyLinkDevice) -> Void, rileyLinkListDataSource: RileyLinkListDataSource) { self.viewModel = viewModel self.supportedInsulinTypes = supportedInsulinTypes self.handleRileyLinkSelection = handleRileyLinkSelection self.rileyLinkListDataSource = rileyLinkListDataSource } var body: some View { List { Section { headerImage .padding(.vertical) HStack(alignment: .top) { deliveryStatus Spacer() reservoirStatus } .padding(.bottom, 5) } if let basalDeliveryState = viewModel.basalDeliveryState { Section { HStack { Button(basalDeliveryState.buttonLabelText) { viewModel.suspendResumeButtonPressed(action: basalDeliveryState.shownAction) }.disabled(viewModel.suspendResumeButtonEnabled) if viewModel.suspendResumeButtonEnabled { Spacer() ProgressView() } } } } Section(header: SectionHeader(label: LocalizedString("Configuration", comment: "The title of the configuration section in MinimedPumpManager settings"))) { NavigationLink(destination: InsulinTypeSetting(initialValue: viewModel.pumpManager.state.insulinType, supportedInsulinTypes: supportedInsulinTypes, allowUnsetInsulinType: false, didChange: viewModel.didChangeInsulinType)) { HStack { Text(LocalizedString("Insulin Type", comment: "Text for confidence reminders navigation link")).foregroundColor(Color.primary) if let currentTitle = viewModel.pumpManager.state.insulinType?.brandName { Spacer() Text(currentTitle) .foregroundColor(.secondary) } } } NavigationLink(destination: BatteryTypeSelectionView(batteryType: $viewModel.batteryChemistryType)) { HStack { Text(LocalizedString("Pump Battery Type", comment: "Text for medtronic pump battery type")).foregroundColor(Color.primary) Spacer() Text(viewModel.batteryChemistryType.description) .foregroundColor(.secondary) } } NavigationLink(destination: DataSourceSelectionView(batteryType: $viewModel.preferredDataSource)) { HStack { Text(LocalizedString("Preferred Data Source", comment: "Text for medtronic pump preferred data source")).foregroundColor(Color.primary) Spacer() Text(viewModel.preferredDataSource.description) .foregroundColor(.secondary) } } if viewModel.pumpManager.state.pumpModel.hasMySentry { NavigationLink(destination: UseMySentrySelectionView(mySentryConfig: $viewModel.mySentryConfig)) { HStack { Text(LocalizedString("Use MySentry", comment: "Text for medtronic pump to use MySentry")).foregroundColor(Color.primary) Spacer() Text((viewModel.mySentryConfig == .useMySentry ? LocalizedString("Yes", comment: "Value string for MySentry config when MySentry is being used") : LocalizedString("No", comment: "Value string for MySentry config when MySentry is not being used")) ) .foregroundColor(.secondary) } } } } Section(header: HStack { Text(LocalizedString("Devices", comment: "Header for devices section of RileyLinkSetupView")) Spacer() ProgressView() }) { ForEach(rileyLinkListDataSource.devices, id: \.peripheralIdentifier) { device in Toggle(isOn: rileyLinkListDataSource.autoconnectBinding(for: device)) { HStack { Text(device.name ?? "Unknown") Spacer() if rileyLinkListDataSource.autoconnectBinding(for: device).wrappedValue { if device.isConnected { Text(formatRSSI(rssi:device.rssi)).foregroundColor(.secondary) } else { Image(systemName: "wifi.exclamationmark") .imageScale(.large) .foregroundColor(guidanceColors.warning) } } } .contentShape(Rectangle()) .onTapGesture { handleRileyLinkSelection(device) } } } } .onAppear { rileyLinkListDataSource.isScanningEnabled = true } .onDisappear { rileyLinkListDataSource.isScanningEnabled = false } Section() { HStack { Text(LocalizedString("Pump Battery Remaining", comment: "Text for medtronic pump battery percent remaining")).foregroundColor(Color.primary) Spacer() if let chargeRemaining = viewModel.pumpManager.status.pumpBatteryChargeRemaining { Text(String("\(Int(round(chargeRemaining * 100)))%")) } else { Text(String(LocalizedString("unknown", comment: "Text to indicate battery percentage is unknown"))) } } HStack { Text(LocalizedString("Pump Time", comment: "The title of the command to change pump time zone")) Spacer() if viewModel.isClockOffset { Image(systemName: "clock.fill") .foregroundColor(guidanceColors.warning) } TimeView(timeZone: viewModel.pumpManager.status.timeZone) .foregroundColor( viewModel.isClockOffset ? guidanceColors.warning : nil) } if viewModel.synchronizingTime { HStack { Text(LocalizedString("Adjusting Pump Time...", comment: "Text indicating ongoing pump time synchronization")) .foregroundColor(.secondary) Spacer() ActivityIndicator(isAnimating: .constant(true), style: .medium) } } else if self.viewModel.pumpManager.status.timeZone != TimeZone.currentFixed { Button(action: { showSyncTimeOptions = true }) { Text(LocalizedString("Sync to Current Time", comment: "The title of the command to change pump time zone")) } .actionSheet(isPresented: $showSyncTimeOptions) { syncPumpTimeActionSheet } } } Section { LabeledValueView(label: LocalizedString("Pump ID", comment: "The title text for the pump ID config value"), value: viewModel.pumpManager.state.pumpID) LabeledValueView(label: LocalizedString("Firmware Version", comment: "The title of the cell showing the pump firmware version"), value: String(describing: viewModel.pumpManager.state.pumpFirmwareVersion)) LabeledValueView(label: LocalizedString("Region", comment: "The title of the cell showing the pump region"), value: String(describing: viewModel.pumpManager.state.pumpRegion)) } Section() { deletePumpButton } } .alert(item: $viewModel.activeAlert, content: { alert in switch alert { case .suspendError(let error): return Alert(title: Text(LocalizedString("Error Suspending", comment: "The alert title for a suspend error")), message: Text(errorText(error))) case .resumeError(let error): return Alert(title: Text(LocalizedString("Error Resuming", comment: "The alert title for a resume error")), message: Text(errorText(error))) case .syncTimeError(let error): return Alert(title: Text(LocalizedString("Error Syncing Time", comment: "The alert title for an error while synching time")), message: Text(errorText(error))) } }) .insetGroupedListStyle() .navigationBarItems(trailing: doneButton) .navigationBarTitle(String(format: LocalizedString("Medtronic %1$@", comment: "Format string fof navigation bar title for MinimedPumpSettingsView (1: model number)"), viewModel.pumpManager.state.pumpModel.description)) } var deliverySectionTitle: String { if self.viewModel.isScheduledBasal { return LocalizedString("Scheduled Basal", comment: "Title of insulin delivery section") } else { return LocalizedString("Insulin Delivery", comment: "Title of insulin delivery section") } } var deliveryStatus: some View { VStack(alignment: .leading, spacing: 5) { Text(deliverySectionTitle) .foregroundColor(Color(UIColor.secondaryLabel)) if viewModel.isSuspendedOrResuming { HStack(alignment: .center) { Image(systemName: "pause.circle.fill") .font(.system(size: 34)) .fixedSize() .foregroundColor(viewModel.suspendResumeButtonColor(guidanceColors: guidanceColors)) Text(LocalizedString("Insulin\nSuspended", comment: "Text shown in insulin delivery space when insulin suspended")) .fontWeight(.bold) .fixedSize() } } else if let basalRate = self.viewModel.basalDeliveryRate { HStack(alignment: .center) { HStack(alignment: .lastTextBaseline, spacing: 3) { Text(viewModel.basalRateFormatter.string(from: basalRate) ?? "") .font(.system(size: 28)) .fontWeight(.heavy) .fixedSize() Text(LocalizedString("U/hr", comment: "Units for showing temp basal rate")) .foregroundColor(.secondary) } } } else if viewModel.basalDeliveryState?.isTransitioning == true { HStack(alignment: .center) { Image(systemName: "arrow.clockwise.circle.fill") .font(.system(size: 34)) .fixedSize() .foregroundColor(.secondary) Text(LocalizedString("Changing", comment: "Text shown in basal rate space when basal is changing")) .fontWeight(.bold) .fixedSize() .foregroundColor(.secondary) } } else { HStack(alignment: .center) { Image(systemName: "x.circle.fill") .font(.system(size: 34)) .fixedSize() .foregroundColor(guidanceColors.warning) Text(LocalizedString("Unknown", comment: "Text shown in basal rate space when delivery status is unknown")) .fontWeight(.bold) .fixedSize() } } } } func reservoirColor(for reservoirLevelHighlightState: ReservoirLevelHighlightState) -> Color { switch reservoirLevelHighlightState { case .normal: return insulinTintColor case .warning: return guidanceColors.warning case .critical: return guidanceColors.critical } } var reservoirStatus: some View { VStack(alignment: .leading, spacing: 5) { Text(LocalizedString("Insulin Remaining", comment: "Header for insulin remaining on pod settings screen")) .foregroundColor(Color(UIColor.secondaryLabel)) if let reservoirReading = viewModel.reservoirReading, let reservoirLevelHighlightState = viewModel.reservoirLevelHighlightState, let reservoirPercent = viewModel.reservoirPercentage { HStack { MinimedReservoirView(filledPercent: reservoirPercent, fillColor: reservoirColor(for: reservoirLevelHighlightState)) .frame(width: 23, height: 32) Text(viewModel.reservoirText(for: reservoirReading.units)) .font(.system(size: 28)) .fontWeight(.heavy) .fixedSize() } } } } var syncPumpTimeActionSheet: ActionSheet { ActionSheet( title: Text(LocalizedString("Time Change Detected", comment: "Title for pod sync time action sheet.")), message: Text(LocalizedString("The time on your pump is different from the current time. Do you want to update the time on your pump to the current time?", comment: "Message for pod sync time action sheet")), buttons: [ .default(Text(LocalizedString("Yes, Sync to Current Time", comment: "Button text to confirm pump time sync"))) { self.viewModel.changeTimeZoneTapped() }, .cancel(Text(LocalizedString("No, Keep Pump As Is", comment: "Button text to cancel pump time sync"))) ] ) } private func errorText(_ error: Error) -> String { if let error = error as? LocalizedError { return [error.localizedDescription, error.recoverySuggestion].compactMap{$0}.joined(separator: ". ") } else { return error.localizedDescription } } var decimalFormatter: NumberFormatter = { let formatter = NumberFormatter() formatter.numberStyle = .decimal formatter.minimumFractionDigits = 0 formatter.maximumFractionDigits = 2 return formatter }() private func formatRSSI(rssi: Int?) -> String { if let rssi = rssi, let rssiStr = decimalFormatter.decibleString(from: rssi) { return rssiStr } else { return "" } } private var deletePumpButton: some View { Button(action: { showingDeletionSheet = true }, label: { Text(LocalizedString("Delete Pump", comment: "Button label for removing Pump")) .foregroundColor(.red) }).actionSheet(isPresented: $showingDeletionSheet) { ActionSheet( title: Text(LocalizedString("Are you sure you want to delete this Pump?", comment: "Text to confirm delete this pump")), buttons: [ .destructive(Text(LocalizedString("Delete Pump", comment: "Text to delete pump"))) { viewModel.deletePump() }, .cancel(), ] ) } } private var headerImage: some View { VStack(alignment: .center) { Image(uiImage: viewModel.pumpImage) .resizable() .aspectRatio(contentMode: ContentMode.fit) .frame(height: 150) .padding(.horizontal) } .frame(maxWidth: .infinity) } private var doneButton: some View { Button("Done", action: { viewModel.doneButtonPressed() }) } }