add sidebar multi-selection with shift-click, cmd-click, and batch delete
This commit is contained in:
parent
d17c1fe919
commit
ecd01dfb37
|
|
@ -2,6 +2,8 @@ import SwiftUI
|
|||
|
||||
struct SidebarView: View {
|
||||
@ObservedObject var state: AppState
|
||||
@State private var selection: Set<UUID> = []
|
||||
@State private var lastClickedID: UUID?
|
||||
|
||||
private let dateFormatter: DateFormatter = {
|
||||
let f = DateFormatter()
|
||||
|
|
@ -38,46 +40,106 @@ struct SidebarView: View {
|
|||
Spacer()
|
||||
}
|
||||
} else {
|
||||
List(state.noteList) { note in
|
||||
ScrollView {
|
||||
LazyVStack(spacing: 0) {
|
||||
ForEach(Array(state.noteList.enumerated()), id: \.element.id) { index, note in
|
||||
NoteRow(
|
||||
note: note,
|
||||
isSelected: note.id == state.currentNoteID,
|
||||
isActive: note.id == state.currentNoteID,
|
||||
isSelected: selection.contains(note.id),
|
||||
dateFormatter: dateFormatter
|
||||
)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture { state.loadNote(note.id) }
|
||||
.onTapGesture(count: 2) {
|
||||
selection = [note.id]
|
||||
lastClickedID = note.id
|
||||
state.loadNote(note.id)
|
||||
}
|
||||
.onTapGesture(count: 1) {
|
||||
handleClick(note: note, index: index, modifiers: currentModifiers())
|
||||
}
|
||||
.contextMenu {
|
||||
if selection.count > 1 && selection.contains(note.id) {
|
||||
Button("Delete \(selection.count) Notes") {
|
||||
state.deleteNotes(selection)
|
||||
selection.removeAll()
|
||||
lastClickedID = nil
|
||||
}
|
||||
} else {
|
||||
Button("Delete") { state.deleteNote(note.id) }
|
||||
}
|
||||
.listRowBackground(
|
||||
note.id == state.currentNoteID
|
||||
? Color(ns: Theme.current.surface1)
|
||||
: Color.clear
|
||||
)
|
||||
}
|
||||
.listStyle(.plain)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onDeleteCommand {
|
||||
guard !selection.isEmpty else { return }
|
||||
state.deleteNotes(selection)
|
||||
selection.removeAll()
|
||||
lastClickedID = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
.background(Color(ns: Theme.current.mantle))
|
||||
.onAppear { state.refreshNoteList() }
|
||||
}
|
||||
|
||||
private func handleClick(note: NoteInfo, index: Int, modifiers: EventModifiers) {
|
||||
if modifiers.contains(.command) {
|
||||
if selection.contains(note.id) {
|
||||
selection.remove(note.id)
|
||||
} else {
|
||||
selection.insert(note.id)
|
||||
}
|
||||
lastClickedID = note.id
|
||||
} else if modifiers.contains(.shift), let lastID = lastClickedID {
|
||||
if let lastIndex = state.noteList.firstIndex(where: { $0.id == lastID }) {
|
||||
let range = min(lastIndex, index)...max(lastIndex, index)
|
||||
for i in range {
|
||||
selection.insert(state.noteList[i].id)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
selection = [note.id]
|
||||
lastClickedID = note.id
|
||||
state.loadNote(note.id)
|
||||
}
|
||||
}
|
||||
|
||||
private func currentModifiers() -> EventModifiers {
|
||||
let flags = NSEvent.modifierFlags
|
||||
var mods: EventModifiers = []
|
||||
if flags.contains(.command) { mods.insert(.command) }
|
||||
if flags.contains(.shift) { mods.insert(.shift) }
|
||||
return mods
|
||||
}
|
||||
}
|
||||
|
||||
struct NoteRow: View {
|
||||
let note: NoteInfo
|
||||
let isActive: Bool
|
||||
let isSelected: Bool
|
||||
let dateFormatter: DateFormatter
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(note.title)
|
||||
.font(.system(size: 13, weight: isSelected ? .semibold : .regular))
|
||||
.font(.system(size: 13, weight: isActive ? .semibold : .regular))
|
||||
.foregroundColor(Color(ns: Theme.current.text))
|
||||
.lineLimit(1)
|
||||
Text(dateFormatter.string(from: note.lastModified))
|
||||
.font(.system(size: 11))
|
||||
.foregroundColor(Color(ns: Theme.current.subtext0))
|
||||
}
|
||||
.padding(.vertical, 2)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.vertical, 4)
|
||||
.padding(.horizontal, 12)
|
||||
.background(
|
||||
isActive
|
||||
? Color(ns: Theme.current.surface1)
|
||||
: isSelected
|
||||
? Color(ns: Theme.current.surface0)
|
||||
: Color.clear
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue