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