import SwiftUI /// Modal sheet for scanning nearby BLE audio recording devices and binding them to the logged-in user. struct DeviceScanView: View { @Environment(\.dismiss) private var dismiss @ObservedObject var bleManager: BLEManager = .shared @ObservedObject var authManager: AuthManager = .shared @State private var boundSuccessMessage: String? @State private var bindingDeviceID: String? @State private var bindingErrorMessage: String? var body: some View { ZStack { Color.spaceBlack.ignoresSafeArea() VStack(spacing: 20) { // Top Header HStack { VStack(alignment: .leading, spacing: 4) { Text("搜索与绑定录音设备") .font(.system(size: 18, weight: .bold)) .foregroundStyle(Color.primary) Text("请确保设备已开启蓝牙广播模式并靠拢 iPhone") .font(.system(size: 12)) .foregroundStyle(Color.secondary) } Spacer() Button { bleManager.stopScanning() dismiss() } label: { Image(systemName: "xmark.circle.fill") .font(.system(size: 22)) .foregroundStyle(Color.secondary.opacity(0.6)) } } .padding(.horizontal, 20) .padding(.top, 20) // Scanning Status Indicator HStack(spacing: 12) { if bleManager.isScanning { ProgressView() .tint(Color.primary) } else { Image(systemName: "radiowaves.left.and.right") .font(.system(size: 16)) .foregroundStyle(Color.primary) } Text(bleManager.isScanning ? "正在扫描附近的蓝牙录音设备..." : "扫描已停止") .font(.system(size: 13, weight: .medium)) .foregroundStyle(Color.primary) Spacer() Button { if bleManager.isScanning { bleManager.stopScanning() } else { bleManager.startScanning() } } label: { Text(bleManager.isScanning ? "停止扫描" : "重新扫描") .font(.system(size: 12, weight: .regular)) .foregroundStyle(Color.primary) .padding(.horizontal, 10) .padding(.vertical, 6) .overlay( RoundedRectangle(cornerRadius: 6) .stroke(Color.primary.opacity(0.3), lineWidth: 1) ) } } .padding(14) .background(Color.cardBackground.opacity(0.4)) .businessBorder(cornerRadius: 10) .padding(.horizontal, 20) // Success Toast Banner if let successMsg = boundSuccessMessage { HStack(spacing: 8) { Image(systemName: "checkmark.circle.fill") .foregroundStyle(Color.primary) Text(successMsg) .font(.system(size: 13, weight: .medium)) .foregroundStyle(Color.primary) } .padding(.vertical, 10) .padding(.horizontal, 16) .background(Color.primary.opacity(0.1)) .cornerRadius(8) .transition(.move(edge: .top).combined(with: .opacity)) } // Discovered Devices List if bleManager.discoveredDevices.isEmpty { VStack(spacing: 12) { Spacer() Image(systemName: "antenna.radiowaves.left.and.right") .font(.system(size: 36, weight: .ultraLight)) .foregroundStyle(Color.secondary.opacity(0.4)) Text("未搜索到可用的蓝牙录音设备") .font(.system(size: 13)) .foregroundStyle(Color.secondary) Spacer() } } else { ScrollView { VStack(spacing: 12) { ForEach(bleManager.discoveredDevices) { device in discoveredDeviceRow(device) } } .padding(.horizontal, 20) .padding(.bottom, 20) } } } } .onAppear { bleManager.startScanning() } .onDisappear { bleManager.stopScanning() } .alert("无法绑定微光", isPresented: Binding( get: { bindingErrorMessage != nil }, set: { if !$0 { bindingErrorMessage = nil } } )) { Button("知道了", role: .cancel) { bindingErrorMessage = nil } } message: { Text(bindingErrorMessage ?? "请稍后重试。") } } private func discoveredDeviceRow(_ device: DiscoveredBLEDevice) -> some View { let isAlreadyBound = bleManager.boundDevices.contains(where: { $0.id == device.id }) return HStack(spacing: 14) { ZStack { Circle() .fill(Color.primary.opacity(0.06)) .frame(width: 42, height: 42) Image(systemName: "mic.fill") .font(.system(size: 16)) .foregroundStyle(Color.primary) } VStack(alignment: .leading, spacing: 4) { Text("微光(Spark)") .font(.system(size: 14, weight: .medium)) .foregroundStyle(Color.primary) HStack(spacing: 8) { Text("信号强度: \(device.rssi) dBm") .font(.system(size: 11, design: .monospaced)) .foregroundStyle(Color.secondary) Text("•") .font(.system(size: 10)) .foregroundStyle(Color.secondary) Text("设备标识: " + String(device.peripheralUUID.prefix(8))) .font(.system(size: 11, design: .monospaced)) .foregroundStyle(Color.secondary) } Text("广播名称: \(device.name)") .font(.system(size: 10)) .foregroundStyle(Color.secondary.opacity(0.8)) } Spacer() if isAlreadyBound { Text("已绑定") .font(.system(size: 12, weight: .regular)) .foregroundStyle(Color.secondary) .padding(.horizontal, 12) .padding(.vertical, 6) } else { Button { guard let userId = authManager.currentUser?.id else { return } bindingDeviceID = device.id bleManager.connectAndBind(device, userId: userId) { result in bindingDeviceID = nil switch result { case .success(let bound): withAnimation { boundSuccessMessage = "已成功绑定设备「\(bound.name)」" } DispatchQueue.main.asyncAfter(deadline: .now() + 1.2) { dismiss() } case .failure(let error): bindingErrorMessage = error.localizedDescription } } } label: { Group { if bindingDeviceID == device.id { ProgressView() .tint(Color.spaceBlack) } else { Text("绑定设备") .font(.system(size: 13, weight: .semibold)) } } .foregroundStyle(Color.spaceBlack) .frame(minWidth: 72) .padding(.horizontal, 14) .padding(.vertical, 7) .background(Color.primary) .cornerRadius(6) } .disabled(bindingDeviceID != nil) } } .padding(14) .background(Color.cardBackground.opacity(0.3)) .businessBorder(cornerRadius: 10) } } #Preview { DeviceScanView() }