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>
72 lines
2.7 KiB
Swift
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))
|
|
}
|
|
}
|