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)
)
}
+2
View File
@@ -10,6 +10,8 @@ final class NoteEntity {
var text: String = ""
var sourceRaw: String = Note.Source.typed.rawValue
var tags: [String] = []
/// The project (or "category") this note is filed under, nil when unfiled.
var project: String?
var createdAt: Date = Date()
var modifiedAt: Date = Date()
+3 -1
View File
@@ -31,6 +31,7 @@ enum NoteMapper {
entity.text = note.payload.text
entity.sourceRaw = note.payload.source.rawValue
entity.tags = note.payload.tags
entity.project = note.payload.project
entity.createdAt = note.meta.createdAt
entity.modifiedAt = note.meta.modifiedAt
entity.jsonRelativePath = relativePath
@@ -54,7 +55,8 @@ enum NoteMapper {
source: entity.source,
tags: entity.tags,
context: entity.context,
audio: entity.audio
audio: entity.audio,
project: entity.project
)
)
}