XcodeGen project (Notes / NotesMac / NotesTests), Swift 6 strict concurrency, iCloud-document storage via IndieSync with a SwiftData cache, automatic capture context (location, place, time zone, device) on every note, context-based relevance ranking with a 'Right here, right now' shelf, tags, search, and the square.and.pencil-on-red app icon.
98 lines
3.1 KiB
Swift
98 lines
3.1 KiB
Swift
import Foundation
|
|
import IndieSync
|
|
|
|
/// The context snapshot captured at the moment a note is taken — the raw
|
|
/// material the relevance ranking later matches against "where am I, what
|
|
/// time is it" at recall time. Every field is optional: capture is
|
|
/// best-effort and a note taken with location off is still a note.
|
|
struct CaptureContext: Codable, Sendable, Equatable {
|
|
var latitude: Double?
|
|
var longitude: Double?
|
|
var horizontalAccuracy: Double?
|
|
|
|
/// Point-of-interest or venue name from reverse geocoding ("Blue Bottle
|
|
/// Coffee"). Stored as text so recall works even when geocoding is
|
|
/// unavailable later.
|
|
var placeName: String?
|
|
var thoroughfare: String?
|
|
var locality: String?
|
|
var administrativeArea: String?
|
|
var country: String?
|
|
|
|
/// Time zone the note was taken in; local hour / weekday are derived
|
|
/// from `meta.createdAt` under this zone rather than stored.
|
|
var timeZoneID: String?
|
|
|
|
/// "iPhone" / "Mac" — which device captured the note.
|
|
var device: String?
|
|
|
|
static let empty = CaptureContext()
|
|
|
|
var hasCoordinate: Bool { latitude != nil && longitude != nil }
|
|
|
|
/// Short human summary for rows and the composer ("Blue Bottle Coffee · Oakland").
|
|
var summary: String? {
|
|
let parts = [placeName, locality].compactMap { $0 }
|
|
return parts.isEmpty ? nil : parts.joined(separator: " · ")
|
|
}
|
|
}
|
|
|
|
/// One note = one JSON file in the iCloud container, filed under
|
|
/// `Records/YYYY/MM/<ULID>.json` (UTC-bucketed by the ULID's timestamp).
|
|
struct Note: SyncDocument, VersionedDocument, Equatable {
|
|
static let currentSchemaVersion = 1
|
|
|
|
struct Meta: Codable, Sendable, Equatable {
|
|
var id: ULID
|
|
var schemaVersion: Int
|
|
var createdAt: Date
|
|
var modifiedAt: Date
|
|
}
|
|
|
|
/// How the note's text came to be. `transcribed` is reserved for the
|
|
/// planned voice-capture path.
|
|
enum Source: String, Codable, Sendable {
|
|
case typed
|
|
case transcribed
|
|
}
|
|
|
|
struct Payload: Codable, Sendable, Equatable {
|
|
var text: String
|
|
var source: Source
|
|
var tags: [String]
|
|
var context: CaptureContext
|
|
}
|
|
|
|
var meta: Meta
|
|
var payload: Payload
|
|
|
|
var id: ULID { meta.id }
|
|
var schemaVersion: Int { meta.schemaVersion }
|
|
|
|
var relativePath: String { Self.relativePath(forID: meta.id) }
|
|
|
|
static func relativePath(forID id: ULID) -> String {
|
|
TimeBucketedLayout.relativePath(for: id)
|
|
}
|
|
|
|
static func create(
|
|
text: String,
|
|
source: Source = .typed,
|
|
tags: [String] = [],
|
|
context: CaptureContext
|
|
) -> Note {
|
|
let now = Date()
|
|
return Note(
|
|
meta: Meta(id: ULID(), schemaVersion: currentSchemaVersion, createdAt: now, modifiedAt: now),
|
|
payload: Payload(text: text, source: source, tags: tags, context: context)
|
|
)
|
|
}
|
|
|
|
/// First line of the text, used as the display title.
|
|
var title: String {
|
|
payload.text
|
|
.split(separator: "\n", omittingEmptySubsequences: true)
|
|
.first.map(String.init) ?? ""
|
|
}
|
|
}
|