Add camera QR pairing, Bonjour discovery and pinned address recovery

This commit is contained in:
2026-09-12 15:52:31 +03:00
parent 5f16ec48aa
commit 397caf23ce
12 changed files with 451 additions and 20 deletions
+53 -8
View File
@@ -11,7 +11,7 @@ struct ContentView: View {
Label(store.status, systemImage: store.connected ? "wifi" : "wifi.slash")
.font(.subheadline).foregroundStyle(.secondary)
if store.connection == nil {
PairingView(store: store)
PairingView(store: store, discovery: store.discovery)
} else {
PlayerView(store: store)
Text(store.connection?.endpoint.host ?? "")
@@ -40,10 +40,12 @@ struct ContentView: View {
private struct PairingView: View {
@ObservedObject var store: PlayerStore
@ObservedObject var discovery: DiscoveryStore
@State private var endpoint = "https://"
@State private var fingerprint = ""
@State private var code = ""
@State private var invitation = ""
@State private var showScanner = false
var body: some View {
VStack(alignment: .leading, spacing: 18) {
@@ -53,6 +55,36 @@ private struct PairingView: View {
Text("Музыка на ПК. Управление здесь.").font(.title2.bold())
Text("Откройте сопряжение в MusicBridge на Windows. Подключите iPhone к той же домашней сети.")
.foregroundStyle(.secondary)
Button { showScanner = true } label: {
Label("Сканировать QR с ПК", systemImage: "qrcode.viewfinder")
.frame(maxWidth: .infinity)
}.buttonStyle(.borderedProminent).controlSize(.large).disabled(store.pairing)
Text("QR заполнит адрес, отпечаток и код. Затем нажмите «Подключить компьютер».")
.font(.caption).foregroundStyle(.secondary)
DisclosureGroup("Найти ПК в сети") {
VStack(alignment: .leading, spacing: 12) {
Text(discovery.message).font(.footnote).foregroundStyle(.secondary)
ForEach(discovery.computers) { computer in
Button {
endpoint = computer.endpoint
fingerprint = computer.fingerprint
code = ""
store.error = nil
} label: {
VStack(alignment: .leading) {
Label(computer.name, systemImage: "desktopcomputer")
Text(computer.endpoint).font(.caption)
}
}
}
Text("Выберите ПК, сверьте весь отпечаток с его экраном и введите свежий код сопряжения.")
.font(.caption).foregroundStyle(.secondary)
Button("Повторить поиск") { discovery.restart() }
Button("Настройки доступа к сети") {
if let url = URL(string: UIApplication.openSettingsURLString) { UIApplication.shared.open(url) }
}
}.padding(.top, 8)
}.disabled(store.pairing)
Group {
TextField("Адрес ПК: https://192.168.1.10:8765", text: $endpoint).keyboardType(.URL)
TextField("SHA-256 сертификата с экрана ПК", text: $fingerprint)
@@ -74,17 +106,30 @@ private struct PairingView: View {
TextField("musicbridge://pair?data=…", text: $invitation, axis: .vertical)
.lineLimit(3...5).textInputAutocapitalization(.never).autocorrectionDisabled()
Button("Заполнить из приглашения") {
do {
let parsed = try Invitation.parse(invitation.trimmingCharacters(in: .whitespacesAndNewlines))
endpoint = parsed.endpoint
fingerprint = parsed.certificateSha256
code = parsed.code
store.error = nil
} catch { store.error = error.localizedDescription }
importText(invitation)
}
}.padding(.top, 12)
}.disabled(store.pairing)
}
.sheet(isPresented: $showScanner) { QRScannerSheet(receive: importText) }
.onAppear { consumeIncoming() }
.onChange(of: store.incomingInvitation) { _, _ in consumeIncoming() }
}
private func consumeIncoming() {
guard let text = store.incomingInvitation else { return }
store.incomingInvitation = nil
importText(text)
}
private func importText(_ text: String) {
do {
let parsed = try Invitation.parse(text.trimmingCharacters(in: .whitespacesAndNewlines))
endpoint = parsed.endpoint
fingerprint = parsed.certificateSha256
code = parsed.code
store.error = nil
} catch { store.error = error.localizedDescription }
}
}