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:
@@ -136,9 +136,10 @@ final class SyncEngine {
|
||||
text: String,
|
||||
source: Note.Source = .typed,
|
||||
tags: [String] = [],
|
||||
context: CaptureContext
|
||||
context: CaptureContext,
|
||||
project: String? = nil
|
||||
) async throws -> Note {
|
||||
let note = Note.create(text: text, source: source, tags: tags, context: context)
|
||||
let note = Note.create(text: text, source: source, tags: tags, context: context, project: project)
|
||||
try await save(note)
|
||||
return note
|
||||
}
|
||||
@@ -202,7 +203,8 @@ final class SyncEngine {
|
||||
text: String = "",
|
||||
tags: [String] = [],
|
||||
context: CaptureContext,
|
||||
transcriberDeviceID: String
|
||||
transcriberDeviceID: String,
|
||||
project: String? = nil
|
||||
) async throws -> Note {
|
||||
guard let store else {
|
||||
report("Note not saved — iCloud not connected")
|
||||
@@ -215,7 +217,7 @@ final class SyncEngine {
|
||||
transcriptionLocaleID: nil,
|
||||
transcribedAt: nil
|
||||
)
|
||||
let note = Note.create(text: text, source: .transcribed, tags: tags, context: context, audio: audio)
|
||||
let note = Note.create(text: text, source: .transcribed, tags: tags, context: context, audio: audio, project: project)
|
||||
do {
|
||||
let audioData = try Data(contentsOf: audioFileURL)
|
||||
try await store.writeData(audioData, to: Self.dataPath(Note.audioRelativePath(forID: note.id)))
|
||||
@@ -322,6 +324,20 @@ final class SyncEngine {
|
||||
try await updateNote(note)
|
||||
}
|
||||
|
||||
// MARK: - Organization
|
||||
|
||||
/// Assign a note to a project (or clear it with nil). Re-reads the file —
|
||||
/// the authority — so a concurrent edit isn't clobbered; the cache catches
|
||||
/// up via the monitor as usual.
|
||||
func assignProject(id: ULID, project: String?) async throws {
|
||||
guard let store else { throw SyncError.iCloudUnavailable }
|
||||
guard let raw = try? await store.read(Note.self, from: Self.dataPath(Note.relativePath(forID: id))),
|
||||
raw.isReadable else { return }
|
||||
var note = migrateIfNeeded(raw)
|
||||
note.payload.project = project
|
||||
try await updateNote(note)
|
||||
}
|
||||
|
||||
// MARK: - Watch ingestion
|
||||
|
||||
/// Local directory where the phone receiver (`PhoneWatchReceiver`, a later
|
||||
@@ -763,6 +779,8 @@ final class SyncEngine {
|
||||
/// the transcript merged with user typing — indistinguishable by then) in
|
||||
/// `text`; move that whole text into the first attempt so the v3 invariant
|
||||
/// "text is user writing only" holds. Confidence was never recorded ⇒ nil.
|
||||
/// v3 → v4 is additive-only (new optional `payload.project`), so no
|
||||
/// migration action is needed beyond the version stamp.
|
||||
/// Internal (not private) so the migration policy is unit-testable.
|
||||
func migrateIfNeeded(_ note: Note) -> Note {
|
||||
guard note.meta.schemaVersion < Note.currentSchemaVersion else { return note }
|
||||
|
||||
Reference in New Issue
Block a user