add sidebar multi-selection with shift-click, cmd-click, and batch delete

This commit is contained in:
jess 2026-04-06 00:58:11 -07:00
parent d17c1fe919
commit ecd01dfb37
1 changed files with 80 additions and 18 deletions

View File

@ -2,6 +2,8 @@ import SwiftUI
struct SidebarView: View { struct SidebarView: View {
@ObservedObject var state: AppState @ObservedObject var state: AppState
@State private var selection: Set<UUID> = []
@State private var lastClickedID: UUID?
private let dateFormatter: DateFormatter = { private let dateFormatter: DateFormatter = {
let f = DateFormatter() let f = DateFormatter()
@ -38,46 +40,106 @@ struct SidebarView: View {
Spacer() Spacer()
} }
} else { } else {
List(state.noteList) { note in ScrollView {
LazyVStack(spacing: 0) {
ForEach(Array(state.noteList.enumerated()), id: \.element.id) { index, note in
NoteRow( NoteRow(
note: note, note: note,
isSelected: note.id == state.currentNoteID, isActive: note.id == state.currentNoteID,
isSelected: selection.contains(note.id),
dateFormatter: dateFormatter dateFormatter: dateFormatter
) )
.contentShape(Rectangle()) .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 { .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) } 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)) .background(Color(ns: Theme.current.mantle))
.onAppear { state.refreshNoteList() } .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 { struct NoteRow: View {
let note: NoteInfo let note: NoteInfo
let isActive: Bool
let isSelected: Bool let isSelected: Bool
let dateFormatter: DateFormatter let dateFormatter: DateFormatter
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {
Text(note.title) 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)) .foregroundColor(Color(ns: Theme.current.text))
.lineLimit(1) .lineLimit(1)
Text(dateFormatter.string(from: note.lastModified)) Text(dateFormatter.string(from: note.lastModified))
.font(.system(size: 11)) .font(.system(size: 11))
.foregroundColor(Color(ns: Theme.current.subtext0)) .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
)
} }
} }