Add Projects: third tab, swipe-to-file, per-project note capture

Notes gain an optional project (schema v4, additive). Projects derive
from note values like tags — no separate entity. Tab naming
(Projects/Categories) is a Settings choice applied across the UI.

Claude-Session: https://claude.ai/code/session_014esDWi42URLEC6Cj17hGQ3
This commit is contained in:
2026-07-16 18:24:11 -04:00
parent a8085865bc
commit b2f16e8600
14 changed files with 395 additions and 11 deletions
+11 -4
View File
@@ -45,8 +45,10 @@ struct Note: SyncDocument, VersionedDocument, Equatable {
/// own writing, and every transcription attempt is preserved. v1/v2 files
/// decode unchanged (missing keys nil/empty); `migrateIfNeeded` back-fills
/// v2 voice notes by moving a completed transcript from `text` into
/// `attempts` at read time.
static let currentSchemaVersion = 3
/// `attempts` at read time. v4 added optional `payload.project` purely
/// additive, so v1v3 files decode unchanged (missing key nil) with no
/// read-time transformation needed.
static let currentSchemaVersion = 4
struct Meta: Codable, Sendable, Equatable {
var id: ULID
@@ -149,6 +151,10 @@ struct Note: SyncDocument, VersionedDocument, Equatable {
/// Voice-note sidecar metadata, nil for a plain typed note. Optional so
/// a v1 document (no `audio` key) decodes as a text note unchanged.
var audio: AudioInfo?
/// The single project (or "category" user's choice, see
/// `ProjectsNaming`) this note is filed under. Nil means unfiled.
/// Optional so a pre-v4 document (no `project` key) decodes unchanged.
var project: String? = nil
}
var meta: Meta
@@ -186,12 +192,13 @@ struct Note: SyncDocument, VersionedDocument, Equatable {
source: Source = .typed,
tags: [String] = [],
context: CaptureContext,
audio: AudioInfo? = nil
audio: AudioInfo? = nil,
project: String? = nil
) -> 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, audio: audio)
payload: Payload(text: text, source: source, tags: tags, context: context, audio: audio, project: project)
)
}