소스 검색

Add Recently Deleted and unarchive actions.

Soft-delete notes into a Recently Deleted collection with restore and permanent delete actions, and allow unarchiving notes from the Archive view.

Made-with: Cursor
huzaifahayat12 4 달 전
부모
커밋
aa7180b18b
1개의 변경된 파일118개의 추가작업 그리고 10개의 파일을 삭제
  1. 118 10
      notes_app/NotesAppRootView.swift

+ 118 - 10
notes_app/NotesAppRootView.swift

@@ -12,6 +12,7 @@ private struct NoteCollection: Identifiable, Hashable {
     enum Kind: Hashable {
         case notes
         case archive
+        case recentlyDeleted
     }
 
     let id: UUID
@@ -26,6 +27,8 @@ private struct Note: Identifiable, Equatable, Hashable {
     var body: String
     var updatedAt: Date
     var isArchived: Bool
+    var isDeleted: Bool
+    var deletedAt: Date?
 }
 
 struct NotesAppRootView: View {
@@ -37,6 +40,7 @@ struct NotesAppRootView: View {
     @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")!
+    private static let recentlyDeletedCollectionID: UUID = UUID(uuidString: "2E6A2D4E-0B3D-4E8A-A5B7-8D9E0C9E2A04")!
     @State private var selectedCollectionID: UUID? = NotesAppRootView.defaultCollectionID
     @State private var selectedNoteID: UUID? = nil
     @State private var editorFocus: EditorFocusField? = nil
@@ -46,7 +50,8 @@ struct NotesAppRootView: View {
 
     private var collections: [NoteCollection] = [
         NoteCollection(id: NotesAppRootView.defaultCollectionID, provider: .iCloud, name: "Notes", kind: .notes),
-        NoteCollection(id: NotesAppRootView.archiveCollectionID, provider: .iCloud, name: "Archive", kind: .archive)
+        NoteCollection(id: NotesAppRootView.archiveCollectionID, provider: .iCloud, name: "Archive", kind: .archive),
+        NoteCollection(id: NotesAppRootView.recentlyDeletedCollectionID, provider: .iCloud, name: "Recently Deleted", kind: .recentlyDeleted)
     ]
 
     private var selectedCollection: NoteCollection? {
@@ -63,9 +68,11 @@ struct NotesAppRootView: View {
         } else {
             switch selectedCollection.kind {
             case .notes:
-                scoped = notes.filter { !$0.isArchived }
+                scoped = notes.filter { !$0.isDeleted && !$0.isArchived }
             case .archive:
-                scoped = notes.filter { $0.isArchived }
+                scoped = notes.filter { !$0.isDeleted && $0.isArchived }
+            case .recentlyDeleted:
+                scoped = notes.filter { $0.isDeleted }
             }
         }
 
@@ -84,7 +91,9 @@ struct NotesAppRootView: View {
             title: "New Note",
             body: "",
             updatedAt: now,
-            isArchived: false
+            isArchived: false,
+            isDeleted: false,
+            deletedAt: nil
         )
 
         selectedCollectionID = NotesAppRootView.defaultCollectionID
@@ -98,17 +107,69 @@ struct NotesAppRootView: View {
         return notes.contains { $0.id == selectedNoteID }
     }
 
-    private func deleteSelectedNote() {
+    private var canMoveToRecentlyDeleted: Bool {
+        guard let selectedCollection else { return false }
+        guard let selectedNoteID else { return false }
+        guard let note = notes.first(where: { $0.id == selectedNoteID }) else { return false }
+        if note.isDeleted { return false }
+        return selectedCollection.kind == .notes || selectedCollection.kind == .archive
+    }
+
+    private func moveSelectedNoteToRecentlyDeleted() {
+        guard let selectedNoteID else { return }
+        guard let index = notes.firstIndex(where: { $0.id == selectedNoteID }) else { return }
+
+        notes[index].isDeleted = true
+        notes[index].deletedAt = Date()
+
+        selectedCollectionID = NotesAppRootView.recentlyDeletedCollectionID
+        self.selectedNoteID = notes[index].id
+        editorFocus = nil
+    }
+
+    private var canPermanentlyDeleteSelectedNote: Bool {
+        guard let selectedCollection else { return false }
+        guard let selectedNoteID else { return false }
+        guard let note = notes.first(where: { $0.id == selectedNoteID }) else { return false }
+        return selectedCollection.kind == .recentlyDeleted && note.isDeleted
+    }
+
+    private func permanentlyDeleteSelectedNote() {
         guard let selectedNoteID else { return }
         notes.removeAll { $0.id == selectedNoteID }
         self.selectedNoteID = nil
         editorFocus = nil
     }
 
+    private var canRestoreSelectedNote: Bool {
+        guard let selectedCollection else { return false }
+        guard let selectedNoteID else { return false }
+        guard let note = notes.first(where: { $0.id == selectedNoteID }) else { return false }
+        return selectedCollection.kind == .recentlyDeleted && note.isDeleted
+    }
+
+    private func restoreSelectedNote() {
+        guard let selectedNoteID else { return }
+        guard let index = notes.firstIndex(where: { $0.id == selectedNoteID }) else { return }
+
+        notes[index].isDeleted = false
+        notes[index].deletedAt = nil
+
+        if notes[index].isArchived {
+            selectedCollectionID = NotesAppRootView.archiveCollectionID
+        } else {
+            selectedCollectionID = NotesAppRootView.defaultCollectionID
+        }
+
+        self.selectedNoteID = notes[index].id
+    }
+
     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
+        guard let selectedCollection else { return false }
+        if note.isDeleted { return false }
+        return selectedCollection.kind == .notes && !note.isArchived
     }
 
     private func archiveSelectedNote() {
@@ -121,6 +182,24 @@ struct NotesAppRootView: View {
         editorFocus = nil
     }
 
+    private var canUnarchiveSelectedNote: Bool {
+        guard let selectedNoteID else { return false }
+        guard let note = notes.first(where: { $0.id == selectedNoteID }) else { return false }
+        guard let selectedCollection else { return false }
+        if note.isDeleted { return false }
+        return selectedCollection.kind == .archive && note.isArchived
+    }
+
+    private func unarchiveSelectedNote() {
+        guard let selectedNoteID else { return }
+        guard let index = notes.firstIndex(where: { $0.id == selectedNoteID }) else { return }
+
+        notes[index].isArchived = false
+        selectedCollectionID = NotesAppRootView.defaultCollectionID
+        self.selectedNoteID = notes[index].id
+        editorFocus = nil
+    }
+
     var body: some View {
         NavigationSplitView {
             SidebarView(
@@ -141,14 +220,32 @@ struct NotesAppRootView: View {
                     .keyboardShortcut("n", modifiers: [.command])
                     .help("New Note")
 
-                    Button(action: deleteSelectedNote) {
+                    Button(action: moveSelectedNoteToRecentlyDeleted) {
                         Image(systemName: "trash")
                             .font(.system(size: 14, weight: .semibold))
                     }
                     .buttonStyle(.borderless)
                     .keyboardShortcut(.delete, modifiers: [.command])
                     .help("Delete Note")
-                    .disabled(!canDeleteSelectedNote)
+                    .disabled(!canMoveToRecentlyDeleted)
+
+                    Button(action: restoreSelectedNote) {
+                        Image(systemName: "arrow.uturn.left")
+                            .font(.system(size: 14, weight: .semibold))
+                    }
+                    .buttonStyle(.borderless)
+                    .keyboardShortcut("z", modifiers: [.command, .shift])
+                    .help("Restore Note")
+                    .disabled(!canRestoreSelectedNote)
+
+                    Button(action: permanentlyDeleteSelectedNote) {
+                        Image(systemName: "trash.slash")
+                            .font(.system(size: 14, weight: .semibold))
+                    }
+                    .buttonStyle(.borderless)
+                    .keyboardShortcut(.delete, modifiers: [.command, .option])
+                    .help("Delete Permanently")
+                    .disabled(!canPermanentlyDeleteSelectedNote)
 
                     Button(action: archiveSelectedNote) {
                         Image(systemName: "archivebox")
@@ -158,6 +255,15 @@ struct NotesAppRootView: View {
                     .keyboardShortcut("a", modifiers: [.command, .shift])
                     .help("Archive Note")
                     .disabled(!canArchiveSelectedNote)
+
+                    Button(action: unarchiveSelectedNote) {
+                        Image(systemName: "tray.and.arrow.up")
+                            .font(.system(size: 14, weight: .semibold))
+                    }
+                    .buttonStyle(.borderless)
+                    .keyboardShortcut("u", modifiers: [.command, .shift])
+                    .help("Unarchive Note")
+                    .disabled(!canUnarchiveSelectedNote)
                     .padding(.trailing, 12)
                 }
 
@@ -196,9 +302,11 @@ private struct SidebarView: View {
         if collection.provider != .iCloud { return 0 }
         switch collection.kind {
         case .notes:
-            return notes.filter { !$0.isArchived }.count
+            return notes.filter { !$0.isDeleted && !$0.isArchived }.count
         case .archive:
-            return notes.filter { $0.isArchived }.count
+            return notes.filter { !$0.isDeleted && $0.isArchived }.count
+        case .recentlyDeleted:
+            return notes.filter { $0.isDeleted }.count
         }
     }