The app grows from a dev connect form into a real client shell: - Home is a grid of saved hosts (UserDefaults-persisted; context menu: Remove / Forget Identity), "+" in the toolbar opens the add-host sheet, the stream mode moved into Settings (⌘, / gear) — native resolution stays the only mode, no scaling. - Trust is now explicit: the protocol always supported certificate pinning, but the app passed no pin and discarded the observed fingerprint — silently trusting any host. First connect now shows the host's SHA-256 fingerprint (compare with the "clients pin this fingerprint" line in the host log) over the live-but-blurred stream; the stream must pump immediately (the opening IDR is the only guaranteed one), so StreamView gains a capturesCursor switch to keep the cursor free while the prompt needs clicking, and input capture starts only after confirmation. Trusting pins the fingerprint per host; a changed host identity then refuses to connect. - PUNKTFUNK_AUTOCONNECT keeps working (auto-trusts, doesn't touch the saved hosts). Host→client authorization (pairing PIN) remains a punktfunk-core roadmap item — the host still accepts any client that can reach its port. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -44,27 +44,34 @@ private final class CursorCapture {
|
||||
|
||||
public struct StreamView: NSViewRepresentable {
|
||||
private let connection: PunktfunkConnection
|
||||
private let capturesCursor: Bool
|
||||
private let onFrame: (@Sendable (AccessUnit) -> Void)?
|
||||
private let onSessionEnd: (@Sendable () -> Void)?
|
||||
|
||||
/// `onFrame`/`onSessionEnd` fire on the pump thread — hop to the main actor for UI.
|
||||
/// `capturesCursor: false` keeps the local cursor usable while UI (e.g. a trust
|
||||
/// prompt) is layered over the stream; flip it to true to enter capture.
|
||||
public init(
|
||||
connection: PunktfunkConnection,
|
||||
capturesCursor: Bool = true,
|
||||
onFrame: (@Sendable (AccessUnit) -> Void)? = nil,
|
||||
onSessionEnd: (@Sendable () -> Void)? = nil
|
||||
) {
|
||||
self.connection = connection
|
||||
self.capturesCursor = capturesCursor
|
||||
self.onFrame = onFrame
|
||||
self.onSessionEnd = onSessionEnd
|
||||
}
|
||||
|
||||
public func makeNSView(context: Context) -> StreamLayerView {
|
||||
let view = StreamLayerView()
|
||||
view.capturesCursor = capturesCursor
|
||||
view.start(connection: connection, onFrame: onFrame, onSessionEnd: onSessionEnd)
|
||||
return view
|
||||
}
|
||||
|
||||
public func updateNSView(_ view: StreamLayerView, context: Context) {
|
||||
view.capturesCursor = capturesCursor
|
||||
// SwiftUI reuses the NSView across state changes — repoint the pump only when the
|
||||
// connection identity actually changed.
|
||||
if view.connection !== connection {
|
||||
@@ -101,6 +108,18 @@ public final class StreamLayerView: NSView {
|
||||
private let cursorCapture = CursorCapture()
|
||||
private var appObservers: [NSObjectProtocol] = []
|
||||
|
||||
/// Main-thread only. False = leave the local cursor alone (UI layered over the
|
||||
/// stream); switching back to true re-enters capture immediately.
|
||||
public var capturesCursor = true {
|
||||
didSet {
|
||||
if capturesCursor {
|
||||
captureCursorIfStreaming()
|
||||
} else {
|
||||
cursorCapture.release()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override init(frame: NSRect) {
|
||||
super.init(frame: frame)
|
||||
displayLayer.videoGravity = .resizeAspect
|
||||
@@ -132,7 +151,7 @@ public final class StreamLayerView: NSView {
|
||||
}
|
||||
|
||||
private func captureCursorIfStreaming() {
|
||||
guard token != nil, NSApp.isActive else { return }
|
||||
guard capturesCursor, token != nil, NSApp.isActive else { return }
|
||||
cursorCapture.capture(in: self)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user