mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 00:11:37 -06:00
feat: add multi-book edit action to selection bar
This commit is contained in:
@@ -1,10 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, useSlots, watch } from 'vue'
|
||||
import { Download, FolderMinus, FolderPlus, ImageDown, Mail, RefreshCw, Trash2, X } from 'lucide-vue-next'
|
||||
import { Download, FolderMinus, FolderPlus, ImageDown, Mail, Pencil, RefreshCw, Trash2, X } from 'lucide-vue-next'
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
|
||||
const ICON_SIZE = 17
|
||||
|
||||
const BTN_ICON = 'text-foreground/80 h-9 w-9 flex items-center justify-center rounded-full transition-colors'
|
||||
const BTN_DISABLED = 'text-muted-foreground/60 cursor-not-allowed'
|
||||
const BTN_PRIMARY = 'text-foreground hover:bg-primary hover:text-primary-foreground'
|
||||
const BTN_MUTED = 'text-foreground hover:bg-muted'
|
||||
const BTN_DESTRUCTIVE = 'text-destructive hover:bg-destructive hover:text-destructive-foreground'
|
||||
const BTN_TEXT_PRIMARY =
|
||||
'flex items-center gap-1.5 h-8 px-3 rounded-full text-sm font-medium text-foreground hover:bg-primary hover:text-primary-foreground transition-colors'
|
||||
const BTN_TEXT_CANCEL = 'h-8 px-3 rounded-full text-sm font-medium text-muted-foreground hover:text-foreground hover:bg-muted transition-colors'
|
||||
const BTN_TEXT_DESTRUCTIVE =
|
||||
'h-8 px-3 rounded-full text-sm font-medium text-destructive hover:bg-destructive hover:text-destructive-foreground transition-colors'
|
||||
const DIVIDER = 'w-px h-5 bg-border mx-1 shrink-0'
|
||||
|
||||
const props = defineProps<{
|
||||
count: number
|
||||
visible: boolean
|
||||
@@ -14,6 +26,7 @@ const props = defineProps<{
|
||||
const emit = defineEmits<{
|
||||
'add-to-collection': []
|
||||
'remove-from-collection': []
|
||||
edit: []
|
||||
send: []
|
||||
export: [allFormats: boolean]
|
||||
'refresh-metadata': []
|
||||
@@ -66,19 +79,13 @@ watch(
|
||||
<slot name="content" :count="count" />
|
||||
</template>
|
||||
<template v-else-if="!confirmingDelete && !exportMenuOpen">
|
||||
<!-- Count -->
|
||||
<span class="px-2.5 py-0.5 text-sm font-semibold tabular-nums whitespace-nowrap rounded-full bg-primary/10 text-primary">{{ count }}</span>
|
||||
|
||||
<div class="w-px h-5 bg-border mx-1 shrink-0" />
|
||||
<div :class="DIVIDER" />
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<button
|
||||
:disabled="count === 0"
|
||||
class="h-9 w-9 flex items-center justify-center rounded-full transition-colors"
|
||||
:class="count > 0 ? 'text-foreground hover:bg-primary hover:text-primary-foreground' : 'text-muted-foreground/60 cursor-not-allowed'"
|
||||
@click="emit('send')"
|
||||
>
|
||||
<button :disabled="count === 0" :class="[BTN_ICON, count > 0 ? BTN_PRIMARY : BTN_DISABLED]" @click="emit('send')">
|
||||
<Mail :size="ICON_SIZE" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
@@ -87,12 +94,7 @@ watch(
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<button
|
||||
:disabled="count === 0"
|
||||
class="h-9 w-9 flex items-center justify-center rounded-full transition-colors"
|
||||
:class="count > 0 ? 'text-foreground hover:bg-primary hover:text-primary-foreground' : 'text-muted-foreground/60 cursor-not-allowed'"
|
||||
@click="exportMenuOpen = true"
|
||||
>
|
||||
<button :disabled="count === 0" :class="[BTN_ICON, count > 0 ? BTN_PRIMARY : BTN_DISABLED]" @click="exportMenuOpen = true">
|
||||
<Download :size="ICON_SIZE" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
@@ -101,12 +103,7 @@ watch(
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<button
|
||||
:disabled="count === 0"
|
||||
class="h-9 w-9 flex items-center justify-center rounded-full transition-colors"
|
||||
:class="count > 0 ? 'text-foreground hover:bg-primary hover:text-primary-foreground' : 'text-muted-foreground/60 cursor-not-allowed'"
|
||||
@click="emit('add-to-collection')"
|
||||
>
|
||||
<button :disabled="count === 0" :class="[BTN_ICON, count > 0 ? BTN_PRIMARY : BTN_DISABLED]" @click="emit('add-to-collection')">
|
||||
<FolderPlus :size="ICON_SIZE" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
@@ -115,32 +112,27 @@ watch(
|
||||
|
||||
<Tooltip v-if="inCollection">
|
||||
<TooltipTrigger as-child>
|
||||
<button
|
||||
:disabled="count === 0"
|
||||
class="h-9 w-9 flex items-center justify-center rounded-full transition-colors"
|
||||
:class="
|
||||
count > 0
|
||||
? 'text-destructive hover:bg-destructive hover:text-destructive-foreground'
|
||||
: 'text-muted-foreground/60 cursor-not-allowed'
|
||||
"
|
||||
@click="emit('remove-from-collection')"
|
||||
>
|
||||
<button :disabled="count === 0" :class="[BTN_ICON, count > 0 ? BTN_DESTRUCTIVE : BTN_DISABLED]" @click="emit('remove-from-collection')">
|
||||
<FolderMinus :size="ICON_SIZE" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">Remove from collection</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<div class="w-px h-5 bg-border mx-1 shrink-0" />
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<button :disabled="count === 0" :class="[BTN_ICON, count > 0 ? BTN_PRIMARY : BTN_DISABLED]" @click="emit('edit')">
|
||||
<Pencil :size="ICON_SIZE" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">Edit metadata</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<div :class="DIVIDER" />
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<button
|
||||
:disabled="count === 0"
|
||||
class="h-9 w-9 flex items-center justify-center rounded-full transition-colors"
|
||||
:class="count > 0 ? 'text-foreground hover:bg-muted' : 'text-muted-foreground/60 cursor-not-allowed'"
|
||||
@click="emit('refresh-metadata')"
|
||||
>
|
||||
<button :disabled="count === 0" :class="[BTN_ICON, count > 0 ? BTN_MUTED : BTN_DISABLED]" @click="emit('refresh-metadata')">
|
||||
<RefreshCw :size="ICON_SIZE" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
@@ -149,46 +141,29 @@ watch(
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<button
|
||||
:disabled="count === 0"
|
||||
class="h-9 w-9 flex items-center justify-center rounded-full transition-colors"
|
||||
:class="count > 0 ? 'text-foreground hover:bg-muted' : 'text-muted-foreground/60 cursor-not-allowed'"
|
||||
@click="emit('re-extract-cover')"
|
||||
>
|
||||
<button :disabled="count === 0" :class="[BTN_ICON, count > 0 ? BTN_MUTED : BTN_DISABLED]" @click="emit('re-extract-cover')">
|
||||
<ImageDown :size="ICON_SIZE" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">Re-extract cover from file</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<div class="w-px h-5 bg-border mx-1 shrink-0" />
|
||||
<div :class="DIVIDER" />
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<button
|
||||
:disabled="count === 0"
|
||||
class="h-9 w-9 flex items-center justify-center rounded-full transition-colors"
|
||||
:class="
|
||||
count > 0
|
||||
? 'text-destructive hover:bg-destructive hover:text-destructive-foreground'
|
||||
: 'text-muted-foreground/60 cursor-not-allowed'
|
||||
"
|
||||
@click="confirmingDelete = true"
|
||||
>
|
||||
<button :disabled="count === 0" :class="[BTN_ICON, count > 0 ? BTN_DESTRUCTIVE : BTN_DISABLED]" @click="confirmingDelete = true">
|
||||
<Trash2 :size="ICON_SIZE" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">Delete selected</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<div class="w-px h-5 bg-border mx-1 shrink-0" />
|
||||
<div :class="DIVIDER" />
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<button
|
||||
class="h-9 w-9 flex items-center justify-center rounded-full text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
|
||||
@click="emit('exit')"
|
||||
>
|
||||
<button :class="[BTN_ICON, 'text-muted-foreground hover:text-foreground hover:bg-muted']" @click="emit('exit')">
|
||||
<X :size="ICON_SIZE" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
@@ -199,50 +174,25 @@ watch(
|
||||
<template v-else-if="exportMenuOpen">
|
||||
<span class="px-3 text-sm font-semibold text-foreground whitespace-nowrap">Export as ZIP:</span>
|
||||
|
||||
<div class="w-px h-5 bg-border mx-1 shrink-0" />
|
||||
<div :class="DIVIDER" />
|
||||
|
||||
<button
|
||||
class="flex items-center gap-1.5 h-8 px-3 rounded-full text-sm font-medium text-foreground hover:bg-primary hover:text-primary-foreground transition-colors"
|
||||
@click="onExport(false)"
|
||||
>
|
||||
Primary only
|
||||
</button>
|
||||
<button :class="BTN_TEXT_PRIMARY" @click="onExport(false)">Primary only</button>
|
||||
|
||||
<button
|
||||
class="flex items-center gap-1.5 h-8 px-3 rounded-full text-sm font-medium text-foreground hover:bg-primary hover:text-primary-foreground transition-colors"
|
||||
@click="onExport(true)"
|
||||
>
|
||||
All formats
|
||||
</button>
|
||||
<button :class="BTN_TEXT_PRIMARY" @click="onExport(true)">All formats</button>
|
||||
|
||||
<div class="w-px h-5 bg-border mx-1 shrink-0" />
|
||||
<div :class="DIVIDER" />
|
||||
|
||||
<button
|
||||
class="h-8 px-3 rounded-full text-sm font-medium text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
|
||||
@click="exportMenuOpen = false"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button :class="BTN_TEXT_CANCEL" @click="exportMenuOpen = false">Cancel</button>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<span class="px-3 text-sm font-semibold text-destructive whitespace-nowrap">Delete {{ count }} book{{ count === 1 ? '' : 's' }}?</span>
|
||||
|
||||
<div class="w-px h-5 bg-border mx-1 shrink-0" />
|
||||
<div :class="DIVIDER" />
|
||||
|
||||
<button
|
||||
class="h-8 px-3 rounded-full text-sm font-medium text-destructive hover:bg-destructive hover:text-destructive-foreground transition-colors"
|
||||
@click="onConfirmDelete"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
<button :class="BTN_TEXT_DESTRUCTIVE" @click="onConfirmDelete">Delete</button>
|
||||
|
||||
<button
|
||||
class="h-8 px-3 rounded-full text-sm font-medium text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
|
||||
@click="confirmingDelete = false"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button :class="BTN_TEXT_CANCEL" @click="confirmingDelete = false">Cancel</button>
|
||||
</template>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
|
||||
@@ -106,6 +106,14 @@ async function handleRemoveFromCollection() {
|
||||
}
|
||||
}
|
||||
|
||||
function handleEditSelected() {
|
||||
const ids = [...selectedIds.value]
|
||||
if (ids.length === 0) return
|
||||
setBookContext(ids, ids.length)
|
||||
router.push({ name: 'book-edit', params: { bookId: ids[0] } })
|
||||
exitSelectionMode()
|
||||
}
|
||||
|
||||
function handleBookAction(book: BookCard, action: 'quick-view' | 'edit-metadata' | 'add-to-collection' | 'delete') {
|
||||
if (action === 'quick-view') {
|
||||
quickViewBookId.value = book.id
|
||||
@@ -167,6 +175,7 @@ watch(
|
||||
@export="handleExport"
|
||||
@add-to-collection="addToCollectionOpen = true"
|
||||
@remove-from-collection="handleRemoveFromCollection"
|
||||
@edit="handleEditSelected"
|
||||
@refresh-metadata="handleBulkRefreshMetadata"
|
||||
@re-extract-cover="handleBulkReExtractCover"
|
||||
@delete="handleDeleteSelected"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted, ref, shallowRef, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ArrowUpDown, Bookmark, BookmarkCheck, BookOpen, Filter, Telescope, X } from 'lucide-vue-next'
|
||||
import VirtualBookGrid from '@/features/book/components/VirtualBookGrid.vue'
|
||||
import BookListRow from '@/features/book/components/BookListRow.vue'
|
||||
@@ -30,6 +30,7 @@ import { SORT_FIELD_LABELS } from '@/features/book/lib/filter-labels'
|
||||
import type { GroupRule, SortSpec } from '@projectx/types'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const { coverSize, gridGap, viewMode } = useDisplaySettings()
|
||||
const { libraries } = useLibraries()
|
||||
|
||||
@@ -263,6 +264,14 @@ type BookActionType = 'quick-view' | 'edit-metadata' | 'add-to-collection' | 'de
|
||||
const quickViewBookId = ref<number | null>(null)
|
||||
const quickViewOpen = ref(false)
|
||||
|
||||
function handleEditSelected() {
|
||||
const ids = [...selectedIds.value]
|
||||
if (ids.length === 0) return
|
||||
setBookContext(ids, ids.length)
|
||||
router.push({ name: 'book-edit', params: { bookId: ids[0] } })
|
||||
exitSelectionMode()
|
||||
}
|
||||
|
||||
function handleBookAction(book: BookCard, action: BookActionType) {
|
||||
if (action === 'quick-view') {
|
||||
quickViewBookId.value = book.id
|
||||
@@ -481,6 +490,7 @@ function handleBookAction(book: BookCard, action: BookActionType) {
|
||||
@send="sendBookOpen = true"
|
||||
@export="handleExport"
|
||||
@add-to-collection="addToCollectionOpen = true"
|
||||
@edit="handleEditSelected"
|
||||
@refresh-metadata="handleBulkRefreshMetadata"
|
||||
@re-extract-cover="handleBulkReExtractCover"
|
||||
@delete="handleDeleteSelected"
|
||||
|
||||
@@ -99,6 +99,14 @@ type BookActionType = 'quick-view' | 'edit-metadata' | 'add-to-collection' | 'de
|
||||
const quickViewBookId = ref<number | null>(null)
|
||||
const quickViewOpen = ref(false)
|
||||
|
||||
function handleEditSelected() {
|
||||
const ids = [...selectedIds.value]
|
||||
if (ids.length === 0) return
|
||||
setBookContext(ids, ids.length)
|
||||
router.push({ name: 'book-edit', params: { bookId: ids[0] } })
|
||||
exitSelectionMode()
|
||||
}
|
||||
|
||||
function handleBookAction(book: BookCard, action: BookActionType) {
|
||||
if (action === 'quick-view') {
|
||||
quickViewBookId.value = book.id
|
||||
@@ -200,6 +208,7 @@ watch(
|
||||
@send="sendBookOpen = true"
|
||||
@export="handleExport"
|
||||
@add-to-collection="addToCollectionOpen = true"
|
||||
@edit="handleEditSelected"
|
||||
@refresh-metadata="handleBulkRefreshMetadata"
|
||||
@re-extract-cover="handleBulkReExtractCover"
|
||||
@delete="handleDeleteSelected"
|
||||
|
||||
@@ -8,7 +8,7 @@ const maxWidth = computed(() => (route.meta.maxWidth as string | undefined) ?? '
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col my-2 min-h-[calc(100%-1.25rem)] overflow-hidden rounded-xl border border-border/70 bg-card/40">
|
||||
<div class="flex flex-col mt-2 min-h-[calc(100%-0.5rem)] overflow-hidden rounded-xl border border-border/70 bg-card/40">
|
||||
<SettingsHeader />
|
||||
<main class="flex-1 overflow-y-auto overflow-x-hidden">
|
||||
<div class="px-6 py-6" :class="maxWidth">
|
||||
|
||||
@@ -349,7 +349,7 @@ onUnmounted(() => {
|
||||
|
||||
<template>
|
||||
<main class="flex-1" @dragover="onDragOver" @dragenter="onDragEnter" @dragleave="onDragLeave" @drop="onDrop">
|
||||
<div class="flex flex-col gap-4 p-4 sm:p-6 max-w-8xl w-full">
|
||||
<div class="flex flex-col gap-4 py-4 sm:py-6 max-w-8xl w-full">
|
||||
<div class="flex items-center gap-2.5">
|
||||
<div class="flex items-center justify-center size-9 rounded-lg bg-primary/10">
|
||||
<PackageOpen class="size-4.5 text-primary" />
|
||||
|
||||
Reference in New Issue
Block a user