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>
107 lines
4.0 KiB
Swift
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))
|
|
}
|
|
}
|