From 854ab783c80829a8ca76714f80fd2e88bc186fe1 Mon Sep 17 00:00:00 2001 From: neon <18559658+neonsolstice@users.noreply.github.com> Date: Mon, 16 Mar 2026 08:33:03 -0600 Subject: [PATCH] feat: multi-format read and download support - add split Read button on book detail page (primary format direct, dropdown for others) - add useBookDownload composable with auth-aware blob download and ZIP export - add BookDownloadButton component for detail page and files tab - refactor useBookBulkActions to delegate export to useBookDownload - add Open/Download submenus to book card dropdown - group Edit/Refresh/Regenerate under Metadata submenu in book card --- .../book/components/BookCoverCard.vue | 100 ++++++++++++---- .../book/components/BookDownloadButton.vue | 109 ++++++++++++++++++ .../__tests__/BookCoverCard.spec.ts | 3 + .../components/detail/tabs/DetailsTab.vue | 74 +++++++++--- .../book/components/detail/tabs/FilesTab.vue | 7 +- .../book/composables/useBookBulkActions.ts | 22 +--- .../book/composables/useBookDownload.ts | 65 +++++++++++ 7 files changed, 319 insertions(+), 61 deletions(-) create mode 100644 client/src/features/book/components/BookDownloadButton.vue create mode 100644 client/src/features/book/composables/useBookDownload.ts diff --git a/client/src/features/book/components/BookCoverCard.vue b/client/src/features/book/components/BookCoverCard.vue index 999c39413..ac9fb89db 100644 --- a/client/src/features/book/components/BookCoverCard.vue +++ b/client/src/features/book/components/BookCoverCard.vue @@ -8,6 +8,7 @@ import { useRouter } from 'vue-router' import { BookOpen, Check, + Download, ExternalLink, FolderPlus, Image, @@ -21,12 +22,22 @@ import { Trash2, TriangleAlert, } from 'lucide-vue-next' -import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu' import { useCoverVersions } from '../composables/useCoverVersions' import { useRefreshMetadata } from '../composables/useRefreshMetadata' import { useRefreshingBooks } from '../composables/useRefreshingBooks' import { usePermissions } from '@/features/auth/composables/usePermissions' import { useDisplaySettings } from '@/composables/useDisplaySettings' +import { useBookDownload } from '@/features/book/composables/useBookDownload' import SendBookDialog from '@/features/email/components/SendBookDialog.vue' const router = useRouter() @@ -54,7 +65,6 @@ const seriesLine = computed(() => { const readableFiles = computed(() => props.book.files.filter((f) => f.format && f.format in FORMAT_TO_GROUP)) const primaryFile = computed(() => readableFiles.value.find((f) => f.role === 'primary') ?? readableFiles.value[0] ?? null) -const extraFiles = computed(() => readableFiles.value.filter((f) => f !== primaryFile.value)) const { coverUrl, bumpVersion } = useCoverVersions() const coverSrc = computed(() => coverUrl(props.book.id)) @@ -129,6 +139,16 @@ function openAuthorBrowse() { if (!authorQuery.value) return void router.push({ name: 'authors', query: { q: authorQuery.value } }) } + +const { downloadFile, exportBooks } = useBookDownload() + +function handleDownloadFile(file: BookFileRef) { + void downloadFile(file.id) +} + +function handleExportAll() { + void exportBooks([props.book.id], true) +}