feat(ios): add online device list and pairing code redemption

- Add API methods for fetching online devices and redeeming pairing codes
- Add OnlineDevice model and corresponding ViewModel state management
- Add UI sections for online device list and pairing code input in SetupPanel
- Ensure access token is verified before connecting to signaling server
This commit is contained in:
2026-08-03 21:42:31 +08:00
parent da0a3bf59d
commit 1bd0318d35
3 changed files with 179 additions and 7 deletions

View File

@@ -41,6 +41,14 @@ struct ResolutionOption: Identifiable, Equatable {
]
}
/// 线 Flutter BindingDevice.fromOnlineJson
struct OnlineDevice: Identifiable, Equatable {
let deviceId: String
let nickname: String
var id: String { deviceId }
}
/// ViewModelWebRTC UI
/// 线@Published SwiftUI
final class ControllerViewModel: NSObject, ObservableObject {
@@ -67,6 +75,17 @@ final class ControllerViewModel: NSObject, ObservableObject {
@Published var showAuthSheet: Bool = false
@Published var alertMessage: String?
// MARK: -
/// 线GET /api/client/devices/online
@Published var onlineDevices: [OnlineDevice] = []
/// 线
@Published var isLoadingOnlineDevices: Bool = false
///
@Published var pairingCode: String = ""
///
@Published var isRedeemingPairing: Bool = false
///
@Published var isRecording: Bool = false
///
@@ -156,10 +175,12 @@ final class ControllerViewModel: NSObject, ObservableObject {
alertMessage = "请先在右上角登录账号"
return
}
loadOnlineDevices()
showAuthSheet = true
}
/// accessToken
/// accessToken Flutter connectSignaling verify/refresh
/// 4001
func connect(authType: AuthType, authValue: String) {
showAuthSheet = false
pendingAuthType = authType
@@ -167,12 +188,25 @@ final class ControllerViewModel: NSObject, ObservableObject {
isConnecting = true
statusText = "正在连接信令服务器..."
let signaling = SignalingClient(
serverUrl: serverUrl.trimmingCharacters(in: .whitespaces),
token: accessToken)
signaling.delegate = self
signalingClient = signaling
signaling.connect()
Task {
do {
let token = try await ensureAccessToken()
accessToken = token
await MainActor.run { [weak self] in
guard let self else { return }
let signaling = SignalingClient(
serverUrl: self.serverUrl.trimmingCharacters(in: .whitespaces),
token: token)
signaling.delegate = self
self.signalingClient = signaling
signaling.connect()
}
} catch {
await MainActor.run { [weak self] in
self?.failConnection((error as? ApiError)?.message ?? "令牌失效,请重新登录")
}
}
}
}
func disconnect() {
@@ -433,6 +467,67 @@ final class ControllerViewModel: NSObject, ObservableObject {
}
}
/// 线GET /api/client/devices/online线
/// Flutter RemoteController.loadOnlineDevices / BindingDevice.fromOnlineJson
func loadOnlineDevices() {
guard let at = accessToken else { return }
isLoadingOnlineDevices = true
Task {
do {
let data = try await ApiClient.onlineDevices(accessToken: at)
let list = (data["devices"] as? [[String: Any]]) ?? []
let devices: [OnlineDevice] = list.compactMap { entry in
guard let id = entry["deviceId"] as? String, !id.isEmpty else { return nil }
let nickname = (entry["nickname"] as? String) ?? id
return OnlineDevice(deviceId: id, nickname: nickname)
}
await MainActor.run { [weak self] in
guard let self else { return }
self.onlineDevices = devices
self.isLoadingOnlineDevices = false
}
} catch {
await MainActor.run { [weak self] in
self?.isLoadingOnlineDevices = false
}
}
}
}
/// POST /api/client/pairing/redeem
/// Flutter RemoteController.redeemPairingCode
func redeemPairingCode() {
let code = pairingCode.trimmingCharacters(in: .whitespacesAndNewlines)
guard !code.isEmpty else {
alertMessage = "请输入配对码"
return
}
guard let at = accessToken else {
alertMessage = "请先登录账号"
return
}
isRedeemingPairing = true
Task {
do {
let data = try await ApiClient.redeemPairingCode(accessToken: at, code: code)
let deviceId = (data["deviceId"] as? String) ?? ""
let nickname = (data["nickname"] as? String) ?? deviceId
await MainActor.run { [weak self] in
guard let self else { return }
self.isRedeemingPairing = false
self.pairingCode = ""
self.alertMessage = "绑定成功:\(nickname)\(deviceId)"
self.loadOnlineDevices()
}
} catch {
await MainActor.run { [weak self] in
self?.isRedeemingPairing = false
self?.alertMessage = "配对失败:\((error as? ApiError)?.message ?? error.localizedDescription)"
}
}
}
}
/// TURN ICE WebRTCClient
private func loadTurnCredentials() async {
guard let at = accessToken else { return }