Sfoglia il codice sorgente

Add note create/delete/archive actions.

Add toolbar buttons and shortcuts to create, delete, and archive notes, plus an Archive collection in the sidebar with filtering.

Made-with: Cursor
huzaifahayat12 4 mesi fa
parent
commit
15ed5786f6
1 ha cambiato i file con 119 aggiunte e 6 eliminazioni
  1. 119 6
      notes_app/NotesAppRootView.swift

+ 119 - 6
notes_app/NotesAppRootView.swift

@@ -9,9 +9,15 @@ private enum Provider: String, CaseIterable, Identifiable {
 }
 
 private struct NoteCollection: Identifiable, Hashable {
+    enum Kind: Hashable {
+        case notes
+        case archive
+    }
+
     let id: UUID
     let provider: Provider
     let name: String
+    let kind: Kind
 }
 
 private struct Note: Identifiable, Equatable, Hashable {
@@ -19,19 +25,28 @@ private struct Note: Identifiable, Equatable, Hashable {
     var title: String
     var body: String
     var updatedAt: Date
+    var isArchived: Bool
 }
 
 struct NotesAppRootView: View {
+    fileprivate enum EditorFocusField: Hashable {
+        case title
+        case body
+    }
+
     @State private var searchText: String = ""
     private static let defaultCollectionID: UUID = UUID(uuidString: "2E6A2D4E-0B3D-4E8A-A5B7-8D9E0C9E2A01")!
+    private static let archiveCollectionID: UUID = UUID(uuidString: "2E6A2D4E-0B3D-4E8A-A5B7-8D9E0C9E2A03")!
     @State private var selectedCollectionID: UUID? = NotesAppRootView.defaultCollectionID
     @State private var selectedNoteID: UUID? = nil
+    @State private var editorFocus: EditorFocusField? = nil
 
     // Keep this local for now: the goal is to design/match the UI.
     @State private var notes: [Note] = []
 
     private var collections: [NoteCollection] = [
-        NoteCollection(id: NotesAppRootView.defaultCollectionID, provider: .iCloud, name: "Notes")
+        NoteCollection(id: NotesAppRootView.defaultCollectionID, provider: .iCloud, name: "Notes", kind: .notes),
+        NoteCollection(id: NotesAppRootView.archiveCollectionID, provider: .iCloud, name: "Archive", kind: .archive)
     ]
 
     private var selectedCollection: NoteCollection? {
@@ -42,8 +57,17 @@ struct NotesAppRootView: View {
         // Since this is a UI-first pass, we treat notes as belonging to the iCloud "Notes" collection only.
         guard let selectedCollection else { return [] }
 
-        // Currently notes are empty by default to match your screenshot.
-        let scoped = (selectedCollection.provider == .iCloud) ? notes : []
+        let scoped: [Note]
+        if selectedCollection.provider != .iCloud {
+            scoped = []
+        } else {
+            switch selectedCollection.kind {
+            case .notes:
+                scoped = notes.filter { !$0.isArchived }
+            case .archive:
+                scoped = notes.filter { $0.isArchived }
+            }
+        }
 
         let trimmed = searchText.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
         if trimmed.isEmpty { return scoped }
@@ -53,6 +77,50 @@ struct NotesAppRootView: View {
         }
     }
 
+    private func addNote() {
+        let now = Date()
+        let newNote = Note(
+            id: UUID(),
+            title: "New Note",
+            body: "",
+            updatedAt: now,
+            isArchived: false
+        )
+
+        selectedCollectionID = NotesAppRootView.defaultCollectionID
+        notes.insert(newNote, at: 0)
+        selectedNoteID = newNote.id
+        editorFocus = .title
+    }
+
+    private var canDeleteSelectedNote: Bool {
+        guard let selectedNoteID else { return false }
+        return notes.contains { $0.id == selectedNoteID }
+    }
+
+    private func deleteSelectedNote() {
+        guard let selectedNoteID else { return }
+        notes.removeAll { $0.id == selectedNoteID }
+        self.selectedNoteID = nil
+        editorFocus = nil
+    }
+
+    private var canArchiveSelectedNote: Bool {
+        guard let selectedNoteID else { return false }
+        guard let note = notes.first(where: { $0.id == selectedNoteID }) else { return false }
+        return !note.isArchived
+    }
+
+    private func archiveSelectedNote() {
+        guard let selectedNoteID else { return }
+        guard let index = notes.firstIndex(where: { $0.id == selectedNoteID }) else { return }
+
+        notes[index].isArchived = true
+        selectedCollectionID = NotesAppRootView.archiveCollectionID
+        self.selectedNoteID = notes[index].id
+        editorFocus = nil
+    }
+
     var body: some View {
         NavigationSplitView {
             SidebarView(
@@ -62,7 +130,36 @@ struct NotesAppRootView: View {
             )
         } content: {
             VStack(spacing: 0) {
-                SearchBar(text: $searchText)
+                HStack(spacing: 10) {
+                    SearchBar(text: $searchText)
+
+                    Button(action: addNote) {
+                        Image(systemName: "square.and.pencil")
+                            .font(.system(size: 14, weight: .semibold))
+                    }
+                    .buttonStyle(.borderless)
+                    .keyboardShortcut("n", modifiers: [.command])
+                    .help("New Note")
+
+                    Button(action: deleteSelectedNote) {
+                        Image(systemName: "trash")
+                            .font(.system(size: 14, weight: .semibold))
+                    }
+                    .buttonStyle(.borderless)
+                    .keyboardShortcut(.delete, modifiers: [.command])
+                    .help("Delete Note")
+                    .disabled(!canDeleteSelectedNote)
+
+                    Button(action: archiveSelectedNote) {
+                        Image(systemName: "archivebox")
+                            .font(.system(size: 14, weight: .semibold))
+                    }
+                    .buttonStyle(.borderless)
+                    .keyboardShortcut("a", modifiers: [.command, .shift])
+                    .help("Archive Note")
+                    .disabled(!canArchiveSelectedNote)
+                    .padding(.trailing, 12)
+                }
 
                 Group {
                     if filteredNotes.isEmpty {
@@ -80,7 +177,8 @@ struct NotesAppRootView: View {
         } detail: {
             NoteEditorView(
                 notes: $notes,
-                selectedNoteID: selectedNoteID
+                selectedNoteID: selectedNoteID,
+                focusField: $editorFocus
             )
             .background(Color(nsColor: .textBackgroundColor))
         }
@@ -96,7 +194,12 @@ private struct SidebarView: View {
 
     private func notesCount(for collection: NoteCollection) -> Int {
         if collection.provider != .iCloud { return 0 }
-        return notes.count
+        switch collection.kind {
+        case .notes:
+            return notes.filter { !$0.isArchived }.count
+        case .archive:
+            return notes.filter { $0.isArchived }.count
+        }
     }
 
     var body: some View {
@@ -180,6 +283,9 @@ private struct NotesListView: View {
 private struct NoteEditorView: View {
     @Binding var notes: [Note]
     let selectedNoteID: UUID?
+    @Binding var focusField: NotesAppRootView.EditorFocusField?
+
+    @FocusState private var focusedField: NotesAppRootView.EditorFocusField?
 
     var body: some View {
         VStack(spacing: 10) {
@@ -189,6 +295,7 @@ private struct NoteEditorView: View {
                     .textFieldStyle(.plain)
                     .font(.system(size: 22, weight: .semibold))
                     .padding(.horizontal, 14)
+                    .focused($focusedField, equals: .title)
 
                 Divider()
                     .padding(.horizontal, 14)
@@ -198,12 +305,18 @@ private struct NoteEditorView: View {
                     .foregroundStyle(.primary)
                     .padding(14)
                     .background(Color(nsColor: .textBackgroundColor))
+                    .focused($focusedField, equals: .body)
             } else {
                 // Keep the editor empty to match the screenshot's "nothing selected" state.
                 Spacer()
             }
         }
         .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
+        .onChange(of: focusField) { _, newValue in
+            guard let newValue else { return }
+            focusedField = newValue
+            focusField = nil
+        }
     }
 }