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

72 lines
2.7 KiB
Swift

import Foundation
/// Eine kurze Notiz.
///
/// Kein Titel als eigenes Feld: wer eine Notiz schreibt, will schreiben und
/// nicht erst benennen. Die Überschrift ist die erste Zeile — dieselbe
/// Verabredung, die Apples Notizen seit jeher treffen.
public struct Note: Identifiable, Equatable, Sendable, Codable {
public let id: UUID
public var text: String
public var updatedAt: Date
public init(id: UUID = UUID(), text: String = "", updatedAt: Date = Date()) {
self.id = id
self.text = text
self.updatedAt = updatedAt
}
}
/// Die Regeln der Notizen — ohne Dateisystem und ohne Uhr.
public enum NoteRules {
/// Wie viele Notizen höchstens.
///
/// Die Kachel ist klein und die Liste soll überschaubar bleiben. Wer mehr
/// braucht, braucht ein Notizprogramm und keine Notch.
public static let limit = 50
/// Wie lang eine Überschrift höchstens ist.
public static let titleLength = 60
/// Die erste nichtleere Zeile, gekürzt. Leer bleibt leer — daraus macht die
/// Anzeige „Neue Notiz", und das ist ihre Sache, nicht unsere.
public static func title(of note: Note) -> String {
let line = note.text
.split(whereSeparator: \.isNewline)
.first { !$0.trimmingCharacters(in: .whitespaces).isEmpty }
guard let line else { return "" }
let trimmed = line.trimmingCharacters(in: .whitespaces)
guard trimmed.count > titleLength else { return trimmed }
return String(trimmed.prefix(titleLength)) + "…"
}
/// Ob eine Notiz überhaupt etwas enthält.
///
/// Eine Notiz aus lauter Leerzeichen ist keine — sie entsteht, wenn man auf
/// „Neu" drückt und es sich anders überlegt, und soll dann von selbst
/// verschwinden statt als leere Zeile stehen zu bleiben.
public static func isEmpty(_ note: Note) -> Bool {
note.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
}
/// Zuletzt geändert zuerst, mit fester Reihenfolge bei Gleichstand.
public static func sorted(_ notes: [Note]) -> [Note] {
notes.sorted {
$0.updatedAt == $1.updatedAt
? $0.id.uuidString > $1.id.uuidString
: $0.updatedAt > $1.updatedAt
}
}
/// Nimmt eine geänderte Notiz auf: leere fallen weg, über der Grenze fällt
/// die älteste heraus.
public static func merge(_ notes: [Note], updating note: Note,
limit: Int = limit) -> [Note] {
var kept = notes.filter { $0.id != note.id }
if !isEmpty(note) { kept.append(note) }
let sorted = sorted(kept)
return Array(sorted.prefix(limit))
}
}