- NotesListView: remove two-button header; trailing New Note toolbar item (Cmd-N) - NoteEditorView: text entry focused by default; extensible add-media bar (CaptureMedium enum, audio only for now) revealing the inline recorder; startRecording param removed - Changelog entry updated
110 lines
3.5 KiB
Swift
110 lines
3.5 KiB
Swift
import IndieSync
|
|
import SwiftData
|
|
import SwiftUI
|
|
|
|
/// The main list: a "Right here, right now" shelf of context-relevant notes
|
|
/// on top, then everything else newest-first. Search lives in its own tab
|
|
/// (`SearchView`, the tab bar's trailing magnifier).
|
|
struct NotesListView: View {
|
|
@Environment(SyncEngine.self) private var syncEngine
|
|
@Environment(ContextService.self) private var contextService
|
|
@Query(sort: \NoteEntity.createdAt, order: .reverse) private var notes: [NoteEntity]
|
|
|
|
@State private var composing = false
|
|
@State private var editingNote: NoteEntity?
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
list
|
|
.navigationTitle("Notes")
|
|
.toolbar {
|
|
ToolbarItem(placement: .primaryAction) {
|
|
Button {
|
|
composing = true
|
|
} label: {
|
|
Label("New Note", systemImage: "square.and.pencil")
|
|
}
|
|
.keyboardShortcut("n", modifiers: .command)
|
|
}
|
|
}
|
|
.sheet(isPresented: $composing) {
|
|
NoteEditorView(mode: .create)
|
|
}
|
|
.sheet(item: $editingNote) { note in
|
|
NoteEditorView(mode: .edit(note))
|
|
}
|
|
}
|
|
}
|
|
|
|
private var list: some View {
|
|
List {
|
|
if let error = syncEngine.lastSyncError {
|
|
Section {
|
|
Label(error, systemImage: "exclamationmark.icloud")
|
|
.foregroundStyle(.red)
|
|
.font(.footnote)
|
|
}
|
|
}
|
|
|
|
if !relevantNotes.isEmpty {
|
|
Section("Right here, right now") {
|
|
ForEach(relevantNotes) { note in
|
|
row(note)
|
|
}
|
|
}
|
|
}
|
|
|
|
Section("All notes") {
|
|
ForEach(notes) { note in
|
|
row(note)
|
|
}
|
|
}
|
|
}
|
|
.overlay {
|
|
if notes.isEmpty {
|
|
ContentUnavailableView(
|
|
"No notes yet",
|
|
systemImage: "square.and.pencil",
|
|
description: Text("Jot something down — Notes remembers where and when, so it can resurface at the right moment.")
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func row(_ note: NoteEntity) -> some View {
|
|
Button {
|
|
editingNote = note
|
|
} label: {
|
|
NoteRowView(note: note)
|
|
}
|
|
.buttonStyle(.plain)
|
|
.swipeActions(edge: .trailing) {
|
|
Button(role: .destructive) {
|
|
delete(note)
|
|
} label: {
|
|
Label("Delete", systemImage: "trash")
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Top context matches — notes whose capture context resembles "here and
|
|
/// now" strongly enough to earn the shelf.
|
|
private var relevantNotes: [NoteEntity] {
|
|
let query = RelevanceQuery(context: contextService)
|
|
return notes
|
|
.filter { $0.isLive }
|
|
.map { (note: $0, score: Relevance.score(RelevanceSignals(entity: $0), query: query)) }
|
|
.filter { $0.score >= Relevance.surfacingThreshold }
|
|
.sorted { $0.score > $1.score }
|
|
.prefix(5)
|
|
.map(\.note)
|
|
}
|
|
|
|
private func delete(_ note: NoteEntity) {
|
|
guard let id = ULID(string: note.id) else { return }
|
|
Task {
|
|
try? await syncEngine.deleteNote(id: id)
|
|
}
|
|
}
|
|
}
|