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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
f289d02c37
commit
3da673662f
@@ -0,0 +1,106 @@
|
||||
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))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user