import IndieSync import SwiftData import SwiftUI /// Sheet for filing one note under a project: pick an existing one, clear the /// current assignment, or create a new one on the spot. Opened from a leading /// swipe in `NotesListView`/`ProjectNotesView`. struct AssignProjectSheet: View { let note: NoteEntity @Environment(SyncEngine.self) private var syncEngine @Environment(\.dismiss) private var dismiss @Query(sort: \NoteEntity.createdAt, order: .reverse) private var notes: [NoteEntity] @State private var newName = "" @AppStorage(ProjectsNaming.storageKey) private var namingRaw = ProjectsNaming.projects.rawValue private var naming: ProjectsNaming { ProjectsNaming(rawValue: namingRaw) ?? .projects } var body: some View { NavigationStack { List { if note.project != nil { Section { Button(role: .destructive) { assign(nil) } label: { Label("Remove from \(naming.singular)", systemImage: "folder.badge.minus") } } } if !existingProjects.isEmpty { Section(naming.plural) { ForEach(existingProjects, id: \.project) { entry in Button { assign(entry.project) } label: { HStack { Label(entry.project, systemImage: "folder") Spacer() Text("\(entry.count)") .foregroundStyle(.secondary) .monospacedDigit() if note.project == entry.project { Image(systemName: "checkmark") .foregroundStyle(.tint) } } } .buttonStyle(.plain) } } } Section("New \(naming.singular)") { TextField("Name", text: $newName) .autocorrectionDisabled() #if os(iOS) .textInputAutocapitalization(.words) #endif Button("File Under New \(naming.singular)") { assign(trimmedNewName) } .disabled(trimmedNewName.isEmpty) } } .navigationTitle("File Under…") .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { dismiss() } } } } .presentationDetents([.medium, .large]) } private var trimmedNewName: String { newName.trimmingCharacters(in: .whitespacesAndNewlines) } /// Distinct project names across live notes, count-sorted like /// `ProjectsView` so the most-used projects surface first. private var existingProjects: [(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 } } private func assign(_ project: String?) { guard let id = ULID(string: note.id) else { return } Task { try? await syncEngine.assignProject(id: id, project: project) dismiss() } } }