Initial scaffold: context-aware notes app for iOS + macOS
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.
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
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) ?? ""
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user