Files
ScarriffleandClaude Opus 5 3da673662f Zwischenablage, Notizen und der Gesichtsvergleich
Drei neue Module nach dem Bauplan von `ShelfProvider`: reine Regeln getrennt vom
Zustand, versionierte Indexdatei, Inhalte in Dateien statt in `UserDefaults` —
was dort steht, liest jedes Programm mit `defaults read` mit.

Die Zwischenablage achtet `ConcealedType`, `TransientType` und
`AutoGeneratedType`. Diese Marken sind eine Verabredung zwischen Programmen und
kein Systemdienst; wer sie missachtet, sammelt fremde Passwörter ein.

Der Gesichtsvergleich stand im Plan als der eine echte Unsicherheitsfaktor. Ein
Spike hat ihn erledigt: `VNGenerateFaceprintRequest` gibt es nicht, aber
`VNGenerateImageFeaturePrintRequest` ist öffentlich, liefert 768 Elemente und
ein Abstandsmaß — Faktor 3,5 Trennschärfe. Kein eigenes CoreML-Modell nötig.

`FaceMatcher` entscheidet über mehrere Posen, mehrere Einzelbilder und eine
Sperre nach Fehlversuchen. Die Schwelle liegt bewusst nicht mittig: ein falsches
Ja ist ein Fremder am entsperrten Rechner, ein falsches Nein nur ein Passwort,
das man ohnehin kennt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 21:06:06 +02:00

107 lines
4.0 KiB
Swift

import SwiftUI
import AppKit
import OnyxDesign
import OnyxWidgetKit
/// Der Verlauf der Zwischenablage, zum Zurückholen.
public struct ClipboardView: View {
private let store: ClipboardStore
private let compact: Bool
@State private var hovered: UUID?
@State private var justCopied: UUID?
public init(store: ClipboardStore, compact: Bool = false) {
self.store = store
self.compact = compact
}
public var body: some View {
VStack(alignment: .leading, spacing: Onyx.Space.sm) {
if store.items.isEmpty {
OnyxNotice(symbol: "doc.on.clipboard",
text: store.isRecording
? String(localized: "clipboard.empty", bundle: .module)
: String(localized: "clipboard.paused", bundle: .module))
} else {
ScrollView {
LazyVStack(spacing: 0) {
ForEach(Array(store.items.prefix(compact ? 5 : 40).enumerated()),
id: \.element.id) { index, item in
if index > 0 { OnyxHairline() }
row(item)
}
}
}
.scrollIndicators(.never)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
}
private func row(_ item: ClipboardItem) -> some View {
// Die ganze Zeile ist der Knopf. Ein kleines Ziel neben dem Text wäre
// genauer zu treffen zu verlangen, als die Aufgabe es hergibt.
Button { copy(item) } label: {
HStack(spacing: Onyx.Space.row) {
Image(systemName: item.kind == .image ? "photo" : "text.alignleft")
.font(.system(size: Onyx.Symbol.small))
.foregroundStyle(Onyx.Color.textTertiary)
.frame(width: 16)
Text(item.preview)
.font(Onyx.Font.footnote)
.foregroundStyle(Onyx.Color.textPrimary)
.lineLimit(1)
.truncationMode(.tail)
Spacer(minLength: Onyx.Space.xs)
if justCopied == item.id {
// Eine Rückmeldung, die nichts verschiebt: ohne sie sieht
// ein Klick, der die Ablage füllt, wie ein Klick aus, der
// nichts getan hat.
Image(systemName: "checkmark")
.font(.system(size: Onyx.Symbol.caption))
.foregroundStyle(Onyx.Color.positive)
} else if hovered == item.id {
Button { store.remove(item) } label: {
Image(systemName: "xmark.circle.fill")
.font(.system(size: Onyx.Symbol.small))
.foregroundStyle(Onyx.Color.textTertiary)
}
.buttonStyle(.plain)
}
}
.padding(.horizontal, Onyx.Space.row)
.padding(.vertical, compact ? Onyx.Space.sm : Onyx.Space.md)
.contentShape(.rect)
}
.buttonStyle(.plain)
.background(hovered == item.id ? Onyx.Color.elevated : .clear)
.onHover { hovered = $0 ? item.id : (hovered == item.id ? nil : hovered) }
}
private func copy(_ item: ClipboardItem) {
store.copyBack(item)
justCopied = item.id
Task {
try? await Task.sleep(for: .seconds(1.2))
if justCopied == item.id { justCopied = nil }
}
}
}
public struct ClipboardWidget: OnyxWidget {
public let id = "clipboard"
public var displayName: String { String(localized: "clipboard.name", bundle: .module) }
public let symbolName = "doc.on.clipboard"
private let store: ClipboardStore
public init(store: ClipboardStore) { self.store = store }
public func makeView() -> AnyView {
AnyView(ClipboardView(store: store))
}
}