diff --git a/clients/apple/App/Assets.xcassets/AccentColor.colorset/Contents.json b/clients/apple/App/Assets.xcassets/AccentColor.colorset/Contents.json index 13e8bf2..5849e7a 100644 --- a/clients/apple/App/Assets.xcassets/AccentColor.colorset/Contents.json +++ b/clients/apple/App/Assets.xcassets/AccentColor.colorset/Contents.json @@ -11,6 +11,24 @@ } }, "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xF5", + "green" : "0x78", + "red" : "0x86" + } + }, + "idiom" : "universal" } ], "info" : { diff --git a/clients/apple/Sources/PunktfunkClient/ContentView.swift b/clients/apple/Sources/PunktfunkClient/ContentView.swift index ca1d865..e5731a4 100644 --- a/clients/apple/Sources/PunktfunkClient/ContentView.swift +++ b/clients/apple/Sources/PunktfunkClient/ContentView.swift @@ -44,6 +44,13 @@ struct ContentView: View { seedDefaultModeIfNeeded() autoConnectIfAsked() } + .onChange(of: model.phase) { _, phase in + // A session actually started — remember it on the card ("Connected … ago" + // plus the accent ring on the most recent host). + if case .streaming = phase, let host = model.activeHost { + store.markConnected(host.id) + } + } .onDisappear { model.disconnect() } // window closed mid-session (Cmd+N spawns more) // On the outer Group so the sheet survives the trust-prompt → home transition // (the "Pair with PIN instead" path disconnects first — the host's accept loop @@ -244,12 +251,24 @@ struct ContentView: View { .foregroundStyle(.secondary) .lineLimit(1) } + if let last = host.lastConnected { + Text("Connected \(last, format: .relative(presentation: .named))") + .font(.caption2) + .foregroundStyle(.tertiary) + .lineLimit(1) + } } } .frame(maxWidth: .infinity) .padding(.vertical, cardPadding) .padding(.horizontal, 12) .background(.regularMaterial, in: RoundedRectangle(cornerRadius: 14)) + .overlay { + if host.id == mostRecentHostID { + RoundedRectangle(cornerRadius: 14) + .strokeBorder(Color.accentColor.opacity(0.35), lineWidth: 1.5) + } + } } .buttonStyle(.plain) .disabled(model.isBusy) @@ -280,6 +299,13 @@ struct ContentView: View { #endif } + /// The host of the most recent session — its card carries the accent ring. + private var mostRecentHostID: UUID? { + store.hosts + .compactMap { host in host.lastConnected.map { (host.id, $0) } } + .max { $0.1 < $1.1 }?.0 + } + private func connect(_ host: StoredHost) { model.connect( to: host, @@ -295,6 +321,7 @@ struct ContentView: View { VStack(spacing: 14) { Image(systemName: "lock.shield") .font(.system(size: 36, weight: .light)) + .foregroundStyle(.tint) Text("Verify \(model.activeHost?.displayName ?? "host")") .font(.title3.weight(.semibold)) Text("First connection. Compare this fingerprint with the one " @@ -377,8 +404,13 @@ struct ContentView: View { private func hud(_ conn: PunktfunkConnection) -> some View { VStack(alignment: .trailing, spacing: 4) { - Text("\(conn.width)×\(conn.height)@\(conn.refreshHz) \(model.fps) fps \(model.mbps, specifier: "%.1f") Mb/s") - .font(.system(.caption, design: .monospaced)) + HStack(spacing: 6) { + Circle() + .fill(Color.accentColor) + .frame(width: 7, height: 7) + Text("\(conn.width)×\(conn.height)@\(conn.refreshHz) \(model.fps) fps \(model.mbps, specifier: "%.1f") Mb/s") + .font(.system(.caption, design: .monospaced)) + } // While captured the cursor is hidden+frozen, so the button is keyboard-only // (⌘⎋ or Cmd+Tab release the cursor; released, it's clickable again). #if os(macOS) @@ -386,22 +418,21 @@ struct ContentView: View { ? "⌘⎋ releases the mouse" : "Click the stream to capture input") .font(.caption2) - .opacity(0.8) + .foregroundStyle(.secondary) #else // Touch always plays directly; ⌘⎋ (hardware keyboard) toggles kb/mouse. Text(model.mouseCaptured ? "⌘⎋ releases keyboard & mouse" : "⌘⎋ captures keyboard & mouse") .font(.caption2) - .opacity(0.8) + .foregroundStyle(.secondary) #endif Button("Disconnect (⌘D)") { model.disconnect() } .font(.caption) .keyboardShortcut("d", modifiers: .command) } - .padding(8) - .background(.black.opacity(0.5), in: RoundedRectangle(cornerRadius: 6)) - .foregroundStyle(.white) + .padding(10) + .background(.regularMaterial, in: RoundedRectangle(cornerRadius: 10)) .padding(10) } diff --git a/clients/apple/Sources/PunktfunkClient/HostStore.swift b/clients/apple/Sources/PunktfunkClient/HostStore.swift index 8d52707..a60319c 100644 --- a/clients/apple/Sources/PunktfunkClient/HostStore.swift +++ b/clients/apple/Sources/PunktfunkClient/HostStore.swift @@ -18,6 +18,8 @@ struct StoredHost: Identifiable, Codable, Hashable { var port: UInt16 = 9777 /// SHA-256 of the host's certificate, set after the user explicitly trusted it. var pinnedSHA256: Data? + /// Last time a streaming session actually started (nil until the first one). + var lastConnected: Date? var displayName: String { name.isEmpty ? address : name } } @@ -47,6 +49,11 @@ final class HostStore: ObservableObject { hosts.removeAll { $0.id == host.id } } + func markConnected(_ hostID: UUID) { + guard let i = hosts.firstIndex(where: { $0.id == hostID }) else { return } + hosts[i].lastConnected = Date() + } + func pin(_ hostID: UUID, fingerprint: Data) { guard let i = hosts.firstIndex(where: { $0.id == hostID }) else { return } hosts[i].pinnedSHA256 = fingerprint diff --git a/clients/apple/Sources/PunktfunkClient/PairSheet.swift b/clients/apple/Sources/PunktfunkClient/PairSheet.swift index 5e030ac..9d9f5f3 100644 --- a/clients/apple/Sources/PunktfunkClient/PairSheet.swift +++ b/clients/apple/Sources/PunktfunkClient/PairSheet.swift @@ -39,12 +39,16 @@ struct PairSheet: View { Form { Section { TextField("PIN", text: $pin, prompt: Text("Shown in the host's log")) - .font(.system(.body, design: .monospaced)) + .font(.system(.title3, design: .monospaced)) + #if os(iOS) + .keyboardType(.numberPad) + #endif TextField( "Client name", text: $clientName, prompt: Text("How the host lists this Mac")) } header: { - Text("Pair with \(host.displayName)") + Label("Pair with \(host.displayName)", systemImage: "lock.shield") + .foregroundStyle(.tint) } footer: { Text("The host prints the PIN when pairing is armed " + "(--allow-pairing, \u{201C}PAIRING ARMED\u{201D} in its log). "