DeviceScanView.swift 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. import SwiftUI
  2. /// Modal sheet for scanning nearby BLE audio recording devices and binding them to the logged-in user.
  3. struct DeviceScanView: View {
  4. @Environment(\.dismiss) private var dismiss
  5. @ObservedObject var bleManager: BLEManager = .shared
  6. @ObservedObject var authManager: AuthManager = .shared
  7. @State private var boundSuccessMessage: String?
  8. var body: some View {
  9. ZStack {
  10. Color.spaceBlack.ignoresSafeArea()
  11. VStack(spacing: 20) {
  12. // Top Header
  13. HStack {
  14. VStack(alignment: .leading, spacing: 4) {
  15. Text("搜索与绑定录音设备")
  16. .font(.system(size: 18, weight: .bold))
  17. .foregroundStyle(Color.primary)
  18. Text("请确保设备已开启蓝牙广播模式并靠拢 iPhone")
  19. .font(.system(size: 12))
  20. .foregroundStyle(Color.secondary)
  21. }
  22. Spacer()
  23. Button {
  24. bleManager.stopScanning()
  25. dismiss()
  26. } label: {
  27. Image(systemName: "xmark.circle.fill")
  28. .font(.system(size: 22))
  29. .foregroundStyle(Color.secondary.opacity(0.6))
  30. }
  31. }
  32. .padding(.horizontal, 20)
  33. .padding(.top, 20)
  34. // Scanning Status Indicator
  35. HStack(spacing: 12) {
  36. if bleManager.isScanning {
  37. ProgressView()
  38. .tint(Color.primary)
  39. } else {
  40. Image(systemName: "radiowaves.left.and.right")
  41. .font(.system(size: 16))
  42. .foregroundStyle(Color.primary)
  43. }
  44. Text(bleManager.isScanning ? "正在扫描附近的蓝牙录音设备..." : "扫描已停止")
  45. .font(.system(size: 13, weight: .medium))
  46. .foregroundStyle(Color.primary)
  47. Spacer()
  48. Button {
  49. if bleManager.isScanning {
  50. bleManager.stopScanning()
  51. } else {
  52. bleManager.startScanning()
  53. }
  54. } label: {
  55. Text(bleManager.isScanning ? "停止扫描" : "重新扫描")
  56. .font(.system(size: 12, weight: .regular))
  57. .foregroundStyle(Color.primary)
  58. .padding(.horizontal, 10)
  59. .padding(.vertical, 6)
  60. .overlay(
  61. RoundedRectangle(cornerRadius: 6)
  62. .stroke(Color.primary.opacity(0.3), lineWidth: 1)
  63. )
  64. }
  65. }
  66. .padding(14)
  67. .background(Color.cardBackground.opacity(0.4))
  68. .businessBorder(cornerRadius: 10)
  69. .padding(.horizontal, 20)
  70. // Success Toast Banner
  71. if let successMsg = boundSuccessMessage {
  72. HStack(spacing: 8) {
  73. Image(systemName: "checkmark.circle.fill")
  74. .foregroundStyle(Color.primary)
  75. Text(successMsg)
  76. .font(.system(size: 13, weight: .medium))
  77. .foregroundStyle(Color.primary)
  78. }
  79. .padding(.vertical, 10)
  80. .padding(.horizontal, 16)
  81. .background(Color.primary.opacity(0.1))
  82. .cornerRadius(8)
  83. .transition(.move(edge: .top).combined(with: .opacity))
  84. }
  85. // Discovered Devices List
  86. if bleManager.discoveredDevices.isEmpty {
  87. VStack(spacing: 12) {
  88. Spacer()
  89. Image(systemName: "antenna.radiowaves.left.and.right")
  90. .font(.system(size: 36, weight: .ultraLight))
  91. .foregroundStyle(Color.secondary.opacity(0.4))
  92. Text("未搜索到可用的蓝牙录音设备")
  93. .font(.system(size: 13))
  94. .foregroundStyle(Color.secondary)
  95. Spacer()
  96. }
  97. } else {
  98. ScrollView {
  99. VStack(spacing: 12) {
  100. ForEach(bleManager.discoveredDevices) { device in
  101. discoveredDeviceRow(device)
  102. }
  103. }
  104. .padding(.horizontal, 20)
  105. .padding(.bottom, 20)
  106. }
  107. }
  108. }
  109. }
  110. .onAppear {
  111. bleManager.startScanning()
  112. }
  113. .onDisappear {
  114. bleManager.stopScanning()
  115. }
  116. }
  117. private func discoveredDeviceRow(_ device: DiscoveredBLEDevice) -> some View {
  118. let isAlreadyBound = bleManager.boundDevices.contains(where: { $0.id == device.id })
  119. return HStack(spacing: 14) {
  120. ZStack {
  121. Circle()
  122. .fill(Color.primary.opacity(0.06))
  123. .frame(width: 42, height: 42)
  124. Image(systemName: "mic.fill")
  125. .font(.system(size: 16))
  126. .foregroundStyle(Color.primary)
  127. }
  128. VStack(alignment: .leading, spacing: 4) {
  129. Text(device.name)
  130. .font(.system(size: 14, weight: .medium))
  131. .foregroundStyle(Color.primary)
  132. HStack(spacing: 8) {
  133. Text("信号强度: \(device.rssi) dBm")
  134. .font(.system(size: 11, design: .monospaced))
  135. .foregroundStyle(Color.secondary)
  136. Text("•")
  137. .font(.system(size: 10))
  138. .foregroundStyle(Color.secondary)
  139. Text("设备标识: " + String(device.peripheralUUID.prefix(8)))
  140. .font(.system(size: 11, design: .monospaced))
  141. .foregroundStyle(Color.secondary)
  142. }
  143. }
  144. Spacer()
  145. if isAlreadyBound {
  146. Text("已绑定")
  147. .font(.system(size: 12, weight: .regular))
  148. .foregroundStyle(Color.secondary)
  149. .padding(.horizontal, 12)
  150. .padding(.vertical, 6)
  151. } else {
  152. Button {
  153. guard let userId = authManager.currentUser?.id else { return }
  154. let bound = bleManager.bindDevice(device, userId: userId)
  155. withAnimation {
  156. boundSuccessMessage = "已成功绑定设备「\(bound.name)」"
  157. }
  158. DispatchQueue.main.asyncAfter(deadline: .now() + 1.2) {
  159. dismiss()
  160. }
  161. } label: {
  162. Text("绑定设备")
  163. .font(.system(size: 13, weight: .semibold))
  164. .foregroundStyle(Color.spaceBlack)
  165. .padding(.horizontal, 14)
  166. .padding(.vertical, 7)
  167. .background(Color.primary)
  168. .cornerRadius(6)
  169. }
  170. }
  171. }
  172. .padding(14)
  173. .background(Color.cardBackground.opacity(0.3))
  174. .businessBorder(cornerRadius: 10)
  175. }
  176. }
  177. #Preview {
  178. DeviceScanView()
  179. }