Projects tab: create via +, sort by activity/name/count

Declared-but-empty projects persist locally (UserDefaults) until a
note carries the name, at which point the project syncs via notes as
usual. Empty declared projects can be swipe-deleted.

Claude-Session: https://claude.ai/code/session_014esDWi42URLEC6Cj17hGQ3
This commit is contained in:
2026-07-16 23:22:25 -04:00
parent f877941c0d
commit c5e8aea522
4 changed files with 137 additions and 12 deletions
@@ -17,6 +17,8 @@ struct AssignProjectSheet: View {
@AppStorage(ProjectsNaming.storageKey) private var namingRaw = ProjectsNaming.projects.rawValue
private var naming: ProjectsNaming { ProjectsNaming(rawValue: namingRaw) ?? .projects }
@AppStorage(ProjectsPrefs.declaredKey) private var declaredRaw = ""
var body: some View {
NavigationStack {
List {
@@ -79,14 +81,19 @@ struct AssignProjectSheet: View {
newName.trimmingCharacters(in: .whitespacesAndNewlines)
}
/// Distinct project names across live notes, count-sorted like
/// `ProjectsView` so the most-used projects surface first.
/// Distinct project names across live notes, unioned with names declared
/// but not yet used (count 0) so a freshly "+"-created project is
/// immediately offered here, 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
}
for name in ProjectsPrefs.decodeDeclared(declaredRaw) where counts[name] == nil {
counts[name] = 0
}
return counts
.map { (project: $0.key, count: $0.value) }
.sorted { $0.count == $1.count ? $0.project < $1.project : $0.count > $1.count }