import IndieSync import SwiftData import SwiftUI /// Browse notes by project — the manual-organization counterpart to the /// automatic context ranking, and a sibling to `TagsView`. Unlike tags a note /// carries at most one project, so browsing here is filing rather than /// cross-cutting search. struct ProjectsView: View { @Query(sort: \NoteEntity.createdAt, order: .reverse) private var notes: [NoteEntity] @AppStorage(ProjectsNaming.storageKey) private var namingRaw = ProjectsNaming.projects.rawValue private var naming: ProjectsNaming { ProjectsNaming(rawValue: namingRaw) ?? .projects } var body: some View { NavigationStack { List { ForEach(projectCounts, id: \.project) { entry in NavigationLink(value: entry.project) { HStack { Label(entry.project, systemImage: "folder") Spacer() Text("\(entry.count)") .foregroundStyle(.secondary) .monospacedDigit() } } } } .navigationTitle(naming.plural) .navigationDestination(for: String.self) { project in ProjectNotesView(project: project) } .overlay { if projectCounts.isEmpty { ContentUnavailableView( "No \(naming.plural.lowercased()) yet", systemImage: "folder", description: Text("Swipe a note in the list to file it under a \(naming.singular.lowercased()).") ) } } } } private var projectCounts: [(project: String, count: Int)] { var counts: [String: Int] = [:] for note in notes where note.isLive { guard let project = note.project else { continue } counts[project, default: 0] += 1 } return counts .map { (project: $0.key, count: $0.value) } .sorted { $0.count == $1.count ? $0.project < $1.project : $0.count > $1.count } } } struct ProjectNotesView: View { let project: String @Environment(SyncEngine.self) private var syncEngine @Query(sort: \NoteEntity.createdAt, order: .reverse) private var notes: [NoteEntity] @State private var composing = false @State private var editingNote: NoteEntity? @State private var assigningNote: NoteEntity? @AppStorage(ProjectsNaming.storageKey) private var namingRaw = ProjectsNaming.projects.rawValue private var naming: ProjectsNaming { ProjectsNaming(rawValue: namingRaw) ?? .projects } var body: some View { List { ForEach(notes.filter { $0.isLive && $0.project == project }) { note in row(note) } } .navigationTitle(project) .toolbar { ToolbarItem(placement: .primaryAction) { Button { composing = true } label: { Label("New Note", systemImage: "square.and.pencil") } } } .sheet(isPresented: $composing) { NoteEditorView(mode: .create, defaultProject: project) } .sheet(item: $editingNote) { note in NoteEditorView(mode: .edit(note)) } .sheet(item: $assigningNote) { note in AssignProjectSheet(note: note) } } private func row(_ note: NoteEntity) -> some View { Button { editingNote = note } label: { NoteRowView(note: note) .frame(maxWidth: .infinity, alignment: .leading) .contentShape(Rectangle()) } .buttonStyle(.plain) .swipeActions(edge: .leading) { Button { assigningNote = note } label: { Label(naming.singular, systemImage: "folder") } .tint(.indigo) } .swipeActions(edge: .trailing) { Button(role: .destructive) { delete(note) } label: { Label("Delete", systemImage: "trash") } } } private func delete(_ note: NoteEntity) { guard let id = ULID(string: note.id) else { return } Task { try? await syncEngine.deleteNote(id: id) } } }