// LoopFollow // AlarmSelectionView.swift import SwiftUI struct AlarmSelectionView: View { @ObservedObject private var allAlarms = Storage.shared.alarms @State private var selectedAlarmIds: Set = [] @State private var showingCharacterLimitAlert = false let exportedAlarmIds: Set let onConfirm: ([Alarm]) -> Void let onCancel: () -> Void // QR Code character limits (calibrated for alarm exports) private let maxQRCharacters = 2000 private let maxRecommendedAlarms = 5 // Computed property for actual character count (used internally) private var actualCharacterCount: Int { let selectedAlarms = allAlarms.value.filter { selectedAlarmIds.contains($0.id) } let testExport = AlarmSettingsExport( version: AppVersionManager().version(), alarms: selectedAlarms, alarmConfiguration: Storage.shared.alarmConfiguration.value ) if let jsonString = testExport.encodeToJSON() { return jsonString.count } return 0 } private var exceedsCharacterLimit: Bool { return actualCharacterCount > maxQRCharacters } var body: some View { NavigationView { VStack { // Character count indicator characterCountView List { ForEach(allAlarms.value) { alarm in AlarmSelectionRow( alarm: alarm, isSelected: selectedAlarmIds.contains(alarm.id), isExported: exportedAlarmIds.contains(alarm.id), isDisabled: !canSelectAlarm(alarm), onToggle: { toggleAlarm(alarm) } ) } } } .navigationTitle("Select Alarms to Export") .navigationBarTitleDisplayMode(.inline) .navigationBarItems( leading: Button("Cancel") { onCancel() }, trailing: Button("Export") { let selectedAlarms = allAlarms.value.filter { selectedAlarmIds.contains($0.id) } onConfirm(selectedAlarms) } .disabled(selectedAlarmIds.isEmpty) ) } .alert("Character Limit Reached", isPresented: $showingCharacterLimitAlert) { Button("OK") {} } message: { Text("Adding this alarm would exceed the QR code character limit. Please remove some alarms first.") } } private var characterCountView: some View { VStack(spacing: 8) { HStack { Text("Selected Alarms: \(selectedAlarmIds.count)") .font(.headline) Spacer() Text("Max \(maxRecommendedAlarms) alarms at a time") .font(.caption) .foregroundColor(.secondary) } if selectedAlarmIds.count > 0 { ProgressView(value: Double(selectedAlarmIds.count), total: Double(maxRecommendedAlarms)) .progressViewStyle(LinearProgressViewStyle(tint: progressBarColor)) } } .padding() .background(Color(.systemGray6)) } private var progressBarColor: Color { if exceedsCharacterLimit { return .red } else if selectedAlarmIds.count >= maxRecommendedAlarms { return .blue } else { return .green } } private func canSelectAlarm(_ alarm: Alarm) -> Bool { let testSelection = selectedAlarmIds.union([alarm.id]) let testAlarms = allAlarms.value.filter { testSelection.contains($0.id) } // Block if we're at or over the recommended alarm limit if testAlarms.count > maxRecommendedAlarms { return false } // Test actual character count to prevent exceeding QR code limit let testExport = AlarmSettingsExport( version: AppVersionManager().version(), alarms: testAlarms, alarmConfiguration: Storage.shared.alarmConfiguration.value ) if let jsonString = testExport.encodeToJSON() { return jsonString.count <= maxQRCharacters } return false } private func toggleAlarm(_ alarm: Alarm) { if selectedAlarmIds.contains(alarm.id) { selectedAlarmIds.remove(alarm.id) } else { if canSelectAlarm(alarm) { selectedAlarmIds.insert(alarm.id) } else { showingCharacterLimitAlert = true } } } } struct AlarmSelectionRow: View { let alarm: Alarm let isSelected: Bool let isExported: Bool let isDisabled: Bool let onToggle: () -> Void var body: some View { Button(action: onToggle) { HStack { VStack(alignment: .leading, spacing: 4) { HStack { Text(alarm.name) .font(.headline) .foregroundColor(isDisabled ? .secondary : .primary) if isExported { Image(systemName: "checkmark.circle.fill") .foregroundColor(.green) .font(.caption) } } Text(alarmTypeDescription) .font(.caption) .foregroundColor(.secondary) if let aboveBG = alarm.aboveBG { Text("Above: \(String(format: "%.0f", aboveBG))") .font(.caption2) .foregroundColor(.secondary) } if let belowBG = alarm.belowBG { Text("Below: \(String(format: "%.0f", belowBG))") .font(.caption2) .foregroundColor(.secondary) } } Spacer() Image(systemName: isSelected ? "checkmark.circle.fill" : "circle") .foregroundColor(isSelected ? .blue : (isDisabled ? .secondary : .primary)) .font(.title2) } .padding(.vertical, 4) } .buttonStyle(.plain) .disabled(isDisabled && !isSelected) } private var alarmTypeDescription: String { switch alarm.type { case .temporary: return "Temporary Alert" case .iob: return "IOB Alert" case .cob: return "COB Alert" case .low: return "Low BG Alert" case .high: return "High BG Alert" case .fastDrop: return "Fast Drop Alert" case .fastRise: return "Fast Rise Alert" case .missedReading: return "Missed Reading Alert" case .notLooping: return "Not Looping Alert" case .missedBolus: return "Missed Bolus Alert" case .futureCarbs: return "Future Carbs Alert" case .sensorChange: return "Sensor Change Alert" case .pumpChange: return "Pump Change Alert" case .pump: return "Pump Insulin Alert" case .pumpBattery: return "Pump Battery Alert" case .battery: return "Low Battery" case .batteryDrop: return "Battery Drop" case .recBolus: return "Rec. Bolus" case .overrideStart: return "Override Started" case .overrideEnd: return "Override Ended" case .tempTargetStart: return "Temp Target Started" case .tempTargetEnd: return "Temp Target Ended" case .buildExpire: return "Looping app expiration" } } } #Preview { AlarmSelectionView( exportedAlarmIds: [], onConfirm: { _ in }, onCancel: {} ) }