From 59511b0856776d4384ed75232833062000144728 Mon Sep 17 00:00:00 2001 From: neon <18559658+neonsolstice@users.noreply.github.com> Date: Fri, 29 May 2026 04:57:12 +0000 Subject: [PATCH] feat(client): add shift-click selection to book dock Allow users to select a range of files in the book dock by holding the shift key while clicking checkboxes, matching standard multi-select behavior. Closes #136 --- .../book-dock/components/BookDockFileList.vue | 11 ++++--- .../book-dock/composables/useBookDockFiles.ts | 31 ++++++++++++++++--- client/src/views/BookDockView.vue | 6 +++- 3 files changed, 39 insertions(+), 9 deletions(-) diff --git a/client/src/features/book-dock/components/BookDockFileList.vue b/client/src/features/book-dock/components/BookDockFileList.vue index 527929d8f..f7a5c780d 100644 --- a/client/src/features/book-dock/components/BookDockFileList.vue +++ b/client/src/features/book-dock/components/BookDockFileList.vue @@ -37,13 +37,17 @@ const props = withDefaults( }, ) -defineEmits<{ - select: [number] +const emit = defineEmits<{ + select: [number, boolean] selectAll: [] open: [BookDockFile] applyFetched: [number] }>() +function onCheckboxClick(event: MouseEvent, id: number) { + emit('select', id, event.shiftKey) +} + function backendCoverUrl(file: BookDockFile): string { return `/api/v1/book-dock/files/${file.id}/cover?v=${new Date(file.updatedAt).getTime()}` } @@ -234,8 +238,7 @@ function targetSummary(file: BookDockFile): string { type="checkbox" :checked="isSelected(file.id)" class="size-3.5 rounded border-input accent-primary shrink-0 self-center" - @click.stop - @change.stop="$emit('select', file.id)" + @click.stop="onCheckboxClick($event, file.id)" /> diff --git a/client/src/features/book-dock/composables/useBookDockFiles.ts b/client/src/features/book-dock/composables/useBookDockFiles.ts index 13ce7fb1c..19b0d0d5d 100644 --- a/client/src/features/book-dock/composables/useBookDockFiles.ts +++ b/client/src/features/book-dock/composables/useBookDockFiles.ts @@ -23,6 +23,7 @@ export function useBookDockFiles() { const selectedIds = ref>(new Set()) const selectAll = ref(false) const excludedIds = ref>(new Set()) + const lastToggledId = ref(null) let fetchReqSeq = 0 const pageCount = computed(() => Math.ceil(total.value / filters.limit) || 1) @@ -85,16 +86,37 @@ export function useBookDockFiles() { fetchFiles() } - function toggleSelect(id: number) { + function setSelected(id: number, selected: boolean) { if (selectAll.value) { - if (excludedIds.value.has(id)) excludedIds.value.delete(id) + if (selected) excludedIds.value.delete(id) else excludedIds.value.add(id) } else { - if (selectedIds.value.has(id)) selectedIds.value.delete(id) - else selectedIds.value.add(id) + if (selected) selectedIds.value.add(id) + else selectedIds.value.delete(id) } } + function toggleSelect(id: number, options: { range?: boolean } = {}) { + const willBeSelected = !isSelected(id) + + if (options.range && lastToggledId.value !== null && lastToggledId.value !== id) { + const anchorIdx = items.value.findIndex((f) => f.id === lastToggledId.value) + const targetIdx = items.value.findIndex((f) => f.id === id) + if (anchorIdx !== -1 && targetIdx !== -1) { + const [start, end] = anchorIdx < targetIdx ? [anchorIdx, targetIdx] : [targetIdx, anchorIdx] + for (let i = start; i <= end; i++) { + const item = items.value[i] + if (item) setSelected(item.id, willBeSelected) + } + lastToggledId.value = id + return + } + } + + setSelected(id, willBeSelected) + lastToggledId.value = id + } + function toggleSelectAll() { if (selectAll.value) { selectAll.value = false @@ -111,6 +133,7 @@ export function useBookDockFiles() { selectAll.value = false selectedIds.value.clear() excludedIds.value.clear() + lastToggledId.value = null } function isSelected(id: number): boolean { diff --git a/client/src/views/BookDockView.vue b/client/src/views/BookDockView.vue index 0ebf4a89e..0827da42e 100644 --- a/client/src/views/BookDockView.vue +++ b/client/src/views/BookDockView.vue @@ -84,6 +84,10 @@ function openSheet(file: BookDockFile) { selectedFile.value = file } +function handleSelect(id: number, shiftKey: boolean) { + toggleSelect(id, { range: shiftKey }) +} + function closeSheet() { selectedFile.value = null } @@ -469,7 +473,7 @@ onUnmounted(() => { :select-all="selectAll" :name-preview-by-file-id="namePreviewByFileId" :empty-message="emptyMessage" - @select="toggleSelect" + @select="handleSelect" @select-all="toggleSelectAll" @open="openSheet" @apply-fetched="handleInlineApplyFetched"