fix: keep the stream view's identity stable across the trust prompt
ci / rust (push) Has been cancelled
ci / rust (push) Has been cancelled
The awaiting-trust and streaming phases rendered StreamView in different switch branches, so confirming trust dismantled and recreated the NSView — the fresh pump had already missed the opening IDR (infinite GOP: no other keyframe ever comes) and decoded nothing. One session branch now hosts a single StreamView; the trust card is an overlay on the blurred stream and only the capturesCursor flag flips on confirmation. Verified live against the box (gamescope+vkcube at 720p60, 11.7 Mb/s on glass). Note for host runs: without PUNKTFUNK_COMPOSITOR=gamescope + PUNKTFUNK_GAMESCOPE_APP, m3-host auto-picks KWin and streams its (black, empty) session — looks identical to a client bug but isn't one. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -19,19 +19,42 @@ struct ContentView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Group {
|
Group {
|
||||||
switch model.phase {
|
// The stream view's structural identity MUST be stable across the
|
||||||
case .idle, .connecting:
|
// awaiting-trust → streaming transition: recreating it restarts the pump,
|
||||||
|
// which has then already missed the opening IDR (infinite GOP — no other
|
||||||
|
// keyframe ever comes) and decodes nothing. So: one branch per connection,
|
||||||
|
// trust prompt as an overlay.
|
||||||
|
if model.connection != nil {
|
||||||
|
sessionView
|
||||||
|
} else {
|
||||||
home
|
home
|
||||||
case .awaitingTrust(let fingerprint):
|
|
||||||
trustPrompt(fingerprint)
|
|
||||||
case .streaming:
|
|
||||||
stream(capturesCursor: true)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear { autoConnectIfAsked() }
|
.onAppear { autoConnectIfAsked() }
|
||||||
.onDisappear { model.disconnect() } // window closed mid-session (Cmd+N spawns more)
|
.onDisappear { model.disconnect() } // window closed mid-session (Cmd+N spawns more)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var sessionView: some View {
|
||||||
|
let pendingFingerprint: Data? = {
|
||||||
|
if case .awaitingTrust(let fp) = model.phase { return fp }
|
||||||
|
return nil
|
||||||
|
}()
|
||||||
|
return ZStack {
|
||||||
|
stream(capturesCursor: pendingFingerprint == nil)
|
||||||
|
.blur(radius: pendingFingerprint != nil ? 32 : 0)
|
||||||
|
.overlay {
|
||||||
|
if pendingFingerprint != nil {
|
||||||
|
Color.black.opacity(0.45)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let fp = pendingFingerprint {
|
||||||
|
trustCard(fp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(minWidth: 640, minHeight: 360)
|
||||||
|
.background(Color.black)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Home (hosts grid)
|
// MARK: - Home (hosts grid)
|
||||||
|
|
||||||
private var home: some View {
|
private var home: some View {
|
||||||
@@ -156,47 +179,38 @@ struct ContentView: View {
|
|||||||
|
|
||||||
// MARK: - Trust on first use
|
// MARK: - Trust on first use
|
||||||
|
|
||||||
private func trustPrompt(_ fingerprint: Data) -> some View {
|
private func trustCard(_ fingerprint: Data) -> some View {
|
||||||
ZStack {
|
VStack(spacing: 14) {
|
||||||
// Keep the stream pumping (the opening IDR must be consumed) but blurred and
|
Image(systemName: "lock.shield")
|
||||||
// cursor-free until the host is verified.
|
.font(.system(size: 36, weight: .light))
|
||||||
stream(capturesCursor: false)
|
Text("Verify \(model.activeHost?.displayName ?? "host")")
|
||||||
.blur(radius: 32)
|
.font(.title3.weight(.semibold))
|
||||||
.overlay(.black.opacity(0.45))
|
Text("First connection. Compare this fingerprint with the one "
|
||||||
VStack(spacing: 14) {
|
+ "punktfunk-host logged at startup (\u{201C}clients pin this "
|
||||||
Image(systemName: "lock.shield")
|
+ "fingerprint\u{201D}):")
|
||||||
.font(.system(size: 36, weight: .light))
|
.font(.callout)
|
||||||
Text("Verify \(model.activeHost?.displayName ?? "host")")
|
.foregroundStyle(.secondary)
|
||||||
.font(.title3.weight(.semibold))
|
.multilineTextAlignment(.center)
|
||||||
Text("First connection. Compare this fingerprint with the one "
|
Text(Self.format(fingerprint: fingerprint))
|
||||||
+ "punktfunk-host logged at startup (\u{201C}clients pin this "
|
.font(.system(.callout, design: .monospaced))
|
||||||
+ "fingerprint\u{201D}):")
|
.textSelection(.enabled)
|
||||||
.font(.callout)
|
.padding(10)
|
||||||
.foregroundStyle(.secondary)
|
.background(.quaternary, in: RoundedRectangle(cornerRadius: 8))
|
||||||
.multilineTextAlignment(.center)
|
HStack(spacing: 12) {
|
||||||
Text(Self.format(fingerprint: fingerprint))
|
Button("Cancel", role: .cancel) { model.rejectTrust() }
|
||||||
.font(.system(.callout, design: .monospaced))
|
.keyboardShortcut(.cancelAction)
|
||||||
.textSelection(.enabled)
|
Button("Trust & Connect") {
|
||||||
.padding(10)
|
if let fp = model.confirmTrust(), let host = model.activeHost {
|
||||||
.background(.quaternary, in: RoundedRectangle(cornerRadius: 8))
|
store.pin(host.id, fingerprint: fp)
|
||||||
HStack(spacing: 12) {
|
|
||||||
Button("Cancel", role: .cancel) { model.rejectTrust() }
|
|
||||||
.keyboardShortcut(.cancelAction)
|
|
||||||
Button("Trust & Connect") {
|
|
||||||
if let fp = model.confirmTrust(), let host = model.activeHost {
|
|
||||||
store.pin(host.id, fingerprint: fp)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.buttonStyle(.borderedProminent)
|
|
||||||
.keyboardShortcut(.defaultAction)
|
|
||||||
}
|
}
|
||||||
|
.buttonStyle(.borderedProminent)
|
||||||
|
.keyboardShortcut(.defaultAction)
|
||||||
}
|
}
|
||||||
.padding(28)
|
|
||||||
.frame(maxWidth: 440)
|
|
||||||
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: 18))
|
|
||||||
}
|
}
|
||||||
.frame(minWidth: 640, minHeight: 360)
|
.padding(28)
|
||||||
.background(Color.black)
|
.frame(maxWidth: 440)
|
||||||
|
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: 18))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 64 hex chars → four groups per line, two lines — easy to eyeball against the log.
|
/// 64 hex chars → four groups per line, two lines — easy to eyeball against the log.
|
||||||
@@ -226,8 +240,6 @@ struct ContentView: View {
|
|||||||
.overlay(alignment: .topTrailing) {
|
.overlay(alignment: .topTrailing) {
|
||||||
if capturesCursor { hud(conn) }
|
if capturesCursor { hud(conn) }
|
||||||
}
|
}
|
||||||
.frame(minWidth: 640, minHeight: 360)
|
|
||||||
.background(Color.black)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user