Ver código fonte

Polish notebook branding and note lifecycle UX.

Rename window/sidebar labels to Keep Notebook, keep users on Notes after delete/archive actions, add permanent-delete confirmation, and auto-purge Recently Deleted notes older than 30 days.

Made-with: Cursor
huzaifahayat12 4 meses atrás
pai
commit
0402ef02ac

+ 1 - 1
notes_app/Base.lproj/Main.storyboard

@@ -683,7 +683,7 @@
         <scene sceneID="R2V-B0-nI4">
             <objects>
                 <windowController id="B8D-0N-5wS" sceneMemberID="viewController">
-                    <window key="window" title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="default" id="IQv-IB-iLA">
+                    <window key="window" title="Keep Notebook" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="default" id="IQv-IB-iLA">
                         <windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
                         <windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
                         <rect key="contentRect" x="196" y="240" width="480" height="270"/>

+ 37 - 41
notes_app/NotesAppRootView.swift

@@ -1,13 +1,6 @@
 import SwiftUI
 import AppKit
 
-private enum Provider: String, CaseIterable, Identifiable {
-    case iCloud = "iCloud"
-    case google = "Google"
-
-    var id: String { rawValue }
-}
-
 private struct NoteCollection: Identifiable, Hashable {
     enum Kind: Hashable {
         case notes
@@ -16,7 +9,6 @@ private struct NoteCollection: Identifiable, Hashable {
     }
 
     let id: UUID
-    let provider: Provider
     let name: String
     let kind: Kind
 }
@@ -44,13 +36,14 @@ struct NotesAppRootView: View {
     @State private var selectedCollectionID: UUID? = NotesAppRootView.defaultCollectionID
     @State private var selectedNoteID: UUID? = nil
     @State private var editorFocus: EditorFocusField? = nil
+    @State private var showPermanentDeleteConfirmation = false
 
     @StateObject private var store = NotesStore()
 
     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.recentlyDeletedCollectionID, provider: .iCloud, name: "Recently Deleted", kind: .recentlyDeleted)
+        NoteCollection(id: NotesAppRootView.defaultCollectionID, name: "Notes", kind: .notes),
+        NoteCollection(id: NotesAppRootView.archiveCollectionID, name: "Archive", kind: .archive),
+        NoteCollection(id: NotesAppRootView.recentlyDeletedCollectionID, name: "Recently Deleted", kind: .recentlyDeleted)
     ]
 
     private var selectedCollection: NoteCollection? {
@@ -58,21 +51,16 @@ struct NotesAppRootView: View {
     }
 
     private var filteredNotes: [Note] {
-        // Since this is a UI-first pass, we treat notes as belonging to the iCloud "Notes" collection only.
         guard let selectedCollection else { return [] }
 
         let scoped: [Note]
-        if selectedCollection.provider != .iCloud {
-            scoped = []
-        } else {
-            switch selectedCollection.kind {
-            case .notes:
-                scoped = store.notes.filter { !$0.isDeleted && !$0.isArchived }
-            case .archive:
-                scoped = store.notes.filter { !$0.isDeleted && $0.isArchived }
-            case .recentlyDeleted:
-                scoped = store.notes.filter { $0.isDeleted }
-            }
+        switch selectedCollection.kind {
+        case .notes:
+            scoped = store.notes.filter { !$0.isDeleted && !$0.isArchived }
+        case .archive:
+            scoped = store.notes.filter { !$0.isDeleted && $0.isArchived }
+        case .recentlyDeleted:
+            scoped = store.notes.filter { $0.isDeleted }
         }
 
         let trimmed = searchText.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
@@ -121,8 +109,8 @@ struct NotesAppRootView: View {
         store.notes[index].isDeleted = true
         store.notes[index].deletedAt = Date()
 
-        selectedCollectionID = NotesAppRootView.recentlyDeletedCollectionID
-        self.selectedNoteID = store.notes[index].id
+        // Stay on the current sidebar section (e.g. Notes); clear selection since the note left this list.
+        self.selectedNoteID = nil
         editorFocus = nil
     }
 
@@ -176,8 +164,8 @@ struct NotesAppRootView: View {
         guard let index = store.notes.firstIndex(where: { $0.id == selectedNoteID }) else { return }
 
         store.notes[index].isArchived = true
-        selectedCollectionID = NotesAppRootView.archiveCollectionID
-        self.selectedNoteID = store.notes[index].id
+        // Stay on Notes; clear selection since archived notes no longer appear in this list.
+        self.selectedNoteID = nil
         editorFocus = nil
     }
 
@@ -237,7 +225,7 @@ struct NotesAppRootView: View {
                     .help("Restore Note")
                     .disabled(!canRestoreSelectedNote)
 
-                    Button(action: permanentlyDeleteSelectedNote) {
+                    Button(action: { showPermanentDeleteConfirmation = true }) {
                         Image(systemName: "trash.slash")
                             .font(.system(size: 14, weight: .semibold))
                     }
@@ -288,6 +276,17 @@ struct NotesAppRootView: View {
             .background(Color(nsColor: .textBackgroundColor))
         }
         .preferredColorScheme(.dark)
+        .onAppear {
+            store.purgeExpiredRecentlyDeletedIfNeeded()
+        }
+        .alert("Delete permanently?", isPresented: $showPermanentDeleteConfirmation) {
+            Button("Cancel", role: .cancel) {}
+            Button("Delete Permanently", role: .destructive) {
+                permanentlyDeleteSelectedNote()
+            }
+        } message: {
+            Text("This note will be permanently removed from this device. Notes in Recently Deleted are also removed automatically after 30 days. You cannot undo this action.")
+        }
     }
 }
 
@@ -298,7 +297,6 @@ private struct SidebarView: View {
     @Binding var selectedCollectionID: UUID?
 
     private func notesCount(for collection: NoteCollection) -> Int {
-        if collection.provider != .iCloud { return 0 }
         switch collection.kind {
         case .notes:
             return notes.filter { !$0.isDeleted && !$0.isArchived }.count
@@ -311,19 +309,17 @@ private struct SidebarView: View {
 
     var body: some View {
         List(selection: $selectedCollectionID) {
-            ForEach(Provider.allCases) { provider in
-                Section(header: Text(provider == .iCloud ? "iCloud" : "Google")) {
-                    ForEach(collections.filter { $0.provider == provider }) { collection in
-                        HStack(spacing: 8) {
-                            Text(collection.name)
-                                .foregroundStyle(.primary)
-                            Spacer(minLength: 8)
-                            Text("\(notesCount(for: collection))")
-                                .foregroundStyle(.secondary)
-                                .font(.system(size: 12, weight: .semibold))
-                        }
-                        .tag(collection.id)
+            Section(header: Text("Keep Notebook")) {
+                ForEach(collections) { collection in
+                    HStack(spacing: 8) {
+                        Text(collection.name)
+                            .foregroundStyle(.primary)
+                        Spacer(minLength: 8)
+                        Text("\(notesCount(for: collection))")
+                            .foregroundStyle(.secondary)
+                            .font(.system(size: 12, weight: .semibold))
                     }
+                    .tag(collection.id)
                 }
             }
         }

+ 13 - 0
notes_app/NotesStore.swift

@@ -4,6 +4,9 @@ import Combine
 final class NotesStore: ObservableObject {
     @Published var notes: [Note] = []
 
+    /// Notes in Recently Deleted are removed automatically after this many days.
+    static let recentlyDeletedRetentionSeconds: TimeInterval = 30 * 24 * 60 * 60
+
     private let queue = DispatchQueue(label: "notes_app.NotesStore", qos: .utility)
     private var cancellables = Set<AnyCancellable>()
 
@@ -20,6 +23,15 @@ final class NotesStore: ObservableObject {
             .store(in: &cancellables)
     }
 
+    /// Removes soft-deleted notes whose `deletedAt` is older than 30 days. Call after load and on app foreground if desired.
+    func purgeExpiredRecentlyDeletedIfNeeded() {
+        let cutoff = Date().addingTimeInterval(-Self.recentlyDeletedRetentionSeconds)
+        notes.removeAll { note in
+            guard note.isDeleted, let deletedAt = note.deletedAt else { return false }
+            return deletedAt < cutoff
+        }
+    }
+
     private var fileURL: URL {
         let fm = FileManager.default
         let appSupport = fm.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
@@ -50,6 +62,7 @@ final class NotesStore: ObservableObject {
 
                 DispatchQueue.main.async {
                     self.notes = decoded
+                    self.purgeExpiredRecentlyDeletedIfNeeded()
                 }
             } catch {
                 // If the file is corrupt/unreadable, fall back to an empty list.