DeviceListView.swift 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. import SwiftUI
  2. /// Component embedded in ProfileView displaying user's bound BLE recording devices and empty state.
  3. struct DeviceListView: View {
  4. @ObservedObject var bleManager: BLEManager = .shared
  5. @ObservedObject var authManager: AuthManager = .shared
  6. var onAddDeviceClicked: () -> Void
  7. @State private var deviceToUnbind: BoundDevice?
  8. @State private var deviceToRename: BoundDevice?
  9. @State private var proposedDeviceName = ""
  10. private var userBoundDevices: [BoundDevice] {
  11. guard let userId = authManager.currentUser?.id else { return [] }
  12. return bleManager.devices(forUserId: userId)
  13. }
  14. var body: some View {
  15. VStack(alignment: .leading, spacing: 14) {
  16. // Header Row
  17. HStack {
  18. HStack(spacing: 8) {
  19. Image(systemName: "mic.badge.plus")
  20. .font(.system(size: 14, weight: .light))
  21. .foregroundStyle(Color.secondary)
  22. Text("我的录音设备")
  23. .font(.system(size: 15, weight: .semibold))
  24. .foregroundStyle(Color.primary)
  25. }
  26. Spacer()
  27. Button {
  28. onAddDeviceClicked()
  29. } label: {
  30. HStack(spacing: 4) {
  31. Image(systemName: "plus")
  32. .font(.system(size: 11, weight: .bold))
  33. Text("添加设备")
  34. .font(.system(size: 12, weight: .medium))
  35. }
  36. .foregroundStyle(Color.primary)
  37. .padding(.horizontal, 10)
  38. .padding(.vertical, 5)
  39. .background(Color.primary.opacity(0.08))
  40. .cornerRadius(6)
  41. }
  42. }
  43. // Content State
  44. if userBoundDevices.isEmpty {
  45. emptyDeviceCard
  46. } else {
  47. VStack(spacing: 10) {
  48. ForEach(userBoundDevices) { device in
  49. boundDeviceCard(device)
  50. }
  51. }
  52. }
  53. }
  54. .confirmationDialog(
  55. "确定要解绑设备「\(deviceToUnbind?.name ?? "")」吗?",
  56. isPresented: Binding(
  57. get: { deviceToUnbind != nil },
  58. set: { if !$0 { deviceToUnbind = nil } }
  59. ),
  60. titleVisibility: .visible
  61. ) {
  62. Button("解绑设备", role: .destructive) {
  63. if let dev = deviceToUnbind {
  64. bleManager.unbindDevice(id: dev.id)
  65. if let cloudID = dev.cloudID {
  66. Task { try? await DeviceCloudService.shared.remove(cloudID: cloudID) }
  67. }
  68. }
  69. }
  70. Button("取消", role: .cancel) { }
  71. } message: {
  72. Text("解绑后该设备将从你的账号名下移除。")
  73. }
  74. .alert(
  75. "给微光取个名字",
  76. isPresented: Binding(
  77. get: { deviceToRename != nil },
  78. set: { if !$0 { deviceToRename = nil } }
  79. )
  80. ) {
  81. TextField("设备名称", text: $proposedDeviceName)
  82. Button("保存") {
  83. if let device = deviceToRename {
  84. bleManager.renameDevice(id: device.id, to: proposedDeviceName)
  85. var updated = device
  86. updated.name = proposedDeviceName.trimmingCharacters(in: .whitespacesAndNewlines)
  87. Task {
  88. do {
  89. let remote = try await DeviceCloudService.shared.update(updated)
  90. bleManager.updateCloudRegistration(deviceID: updated.id, cloudID: remote.id, error: nil)
  91. } catch {
  92. bleManager.updateCloudRegistration(deviceID: updated.id, cloudID: updated.cloudID, error: error.localizedDescription)
  93. }
  94. }
  95. }
  96. deviceToRename = nil
  97. }
  98. Button("取消", role: .cancel) {
  99. deviceToRename = nil
  100. }
  101. } message: {
  102. Text("这个名称只保存在你的 App 中,不会修改设备的蓝牙广播名。")
  103. }
  104. }
  105. // MARK: - Empty Device Card
  106. private var emptyDeviceCard: some View {
  107. VStack(spacing: 14) {
  108. ZStack {
  109. Circle()
  110. .fill(Color.primary.opacity(0.04))
  111. .frame(width: 56, height: 56)
  112. Image(systemName: "mic.slash")
  113. .font(.system(size: 24, weight: .light))
  114. .foregroundStyle(Color.secondary.opacity(0.6))
  115. }
  116. VStack(spacing: 4) {
  117. Text("暂未绑定录音设备")
  118. .font(.system(size: 14, weight: .medium))
  119. .foregroundStyle(Color.primary)
  120. Text("通过低功耗蓝牙绑定设备后,可同步硬件录音及状态管理")
  121. .font(.system(size: 11))
  122. .foregroundStyle(Color.secondary)
  123. .multilineTextAlignment(.center)
  124. }
  125. Button {
  126. onAddDeviceClicked()
  127. } label: {
  128. HStack(spacing: 6) {
  129. Image(systemName: "wave.3.right")
  130. .font(.system(size: 12))
  131. Text("搜索并绑定蓝牙设备")
  132. .font(.system(size: 13, weight: .medium))
  133. }
  134. .foregroundStyle(Color.spaceBlack)
  135. .padding(.horizontal, 16)
  136. .padding(.vertical, 9)
  137. .background(Color.primary)
  138. .cornerRadius(6)
  139. }
  140. .padding(.top, 4)
  141. }
  142. .frame(maxWidth: .infinity)
  143. .padding(.vertical, 24)
  144. .padding(.horizontal, 16)
  145. .background(Color.cardBackground.opacity(0.2))
  146. .businessBorder(cornerRadius: 10)
  147. }
  148. // MARK: - Bound Device Card
  149. private func boundDeviceCard(_ device: BoundDevice) -> some View {
  150. HStack(spacing: 14) {
  151. // Icon
  152. ZStack {
  153. RoundedRectangle(cornerRadius: 8)
  154. .fill(Color.primary.opacity(0.06))
  155. .frame(width: 44, height: 44)
  156. Image(systemName: "mic.fill")
  157. .font(.system(size: 18))
  158. .foregroundStyle(Color.primary)
  159. }
  160. // Device Details
  161. VStack(alignment: .leading, spacing: 4) {
  162. HStack(spacing: 8) {
  163. Text(device.name)
  164. .font(.system(size: 14, weight: .semibold))
  165. .foregroundStyle(Color.primary)
  166. // Connection Badge
  167. HStack(spacing: 4) {
  168. Circle()
  169. .fill(device.isConnected ? Color.green : Color.gray)
  170. .frame(width: 6, height: 6)
  171. Text(device.isConnected ? "已连接" : "未连接")
  172. .font(.system(size: 10, weight: .medium))
  173. .foregroundStyle(device.isConnected ? Color.green : Color.secondary)
  174. }
  175. }
  176. HStack(spacing: 12) {
  177. if let battery = device.batteryLevel {
  178. HStack(spacing: 3) {
  179. Image(systemName: "battery.75")
  180. .font(.system(size: 10))
  181. Text("\(battery)%")
  182. .font(.system(size: 11, design: .monospaced))
  183. }
  184. .foregroundStyle(Color.secondary)
  185. }
  186. if let fw = device.firmwareVersion {
  187. Text("固件: \(fw)")
  188. .font(.system(size: 11, design: .monospaced))
  189. .foregroundStyle(Color.secondary)
  190. }
  191. if let freeStorage = device.freeStorageMB {
  192. Text("剩余: \(freeStorage) MB")
  193. .font(.system(size: 11, design: .monospaced))
  194. .foregroundStyle(Color.secondary)
  195. }
  196. }
  197. if let cloudError = device.cloudSyncError {
  198. Text("云端登记待重试:\(cloudError)")
  199. .font(.system(size: 10))
  200. .foregroundStyle(Color.recordingRed)
  201. .lineLimit(1)
  202. }
  203. }
  204. Spacer()
  205. VStack(spacing: 8) {
  206. Button {
  207. proposedDeviceName = device.name
  208. deviceToRename = device
  209. } label: {
  210. Text("重命名")
  211. .font(.system(size: 12, weight: .regular))
  212. .foregroundStyle(Color.primary)
  213. }
  214. Button {
  215. deviceToUnbind = device
  216. } label: {
  217. Text("解绑")
  218. .font(.system(size: 12, weight: .regular))
  219. .foregroundStyle(Color.recordingRed)
  220. }
  221. }
  222. .padding(.horizontal, 10)
  223. .padding(.vertical, 7)
  224. .overlay(
  225. RoundedRectangle(cornerRadius: 6)
  226. .stroke(Color.primary.opacity(0.2), lineWidth: 1)
  227. )
  228. }
  229. .padding(14)
  230. .background(Color.cardBackground.opacity(0.35))
  231. .businessBorder(cornerRadius: 10)
  232. }
  233. }
  234. #Preview {
  235. DeviceListView(onAddDeviceClicked: {})
  236. }