From 4e93e20bbb6eef87da986a214a090789916e19f0 Mon Sep 17 00:00:00 2001 From: neon <18559658+neonsolstice@users.noreply.github.com> Date: Fri, 1 May 2026 23:59:37 -0600 Subject: [PATCH] feat(books): add table view, metadata export, and filter builder --- client/package.json | 20 +- client/src/components/AppLayout.vue | 9 +- client/src/components/SelectionActionBar.vue | 268 +++-- client/src/components/ViewHeader.vue | 162 +-- .../__tests__/SelectionActionBar.spec.ts | 104 +- .../components/ui/InputWithSuggestions.vue | 157 +++ .../ui/__tests__/InputWithSuggestions.spec.ts | 74 ++ .../ui/dropdown-menu/DropdownMenuItem.vue | 2 +- .../__tests__/useEffectiveViewMode.spec.ts | 55 + client/src/composables/useDisplaySettings.ts | 18 +- .../src/composables/useEffectiveViewMode.ts | 15 + .../src/composables/useViewDisplaySettings.ts | 2 +- .../src/features/author/views/AuthorsView.vue | 14 +- .../book/components/BookCoverCard.vue | 2 +- .../book/components/BookFilterBuilder.vue | 57 + .../features/book/components/BookListRow.vue | 2 +- .../book/components/BookQuickView.vue | 4 +- .../components/KeyboardShortcutOverlay.vue | 97 ++ .../book/components/MetadataExportDialog.vue | 356 +++++++ .../book/components/TableColumnPanel.vue | 266 +++++ .../book/components/VirtualBookGrid.vue | 2 +- .../book/components/VirtualBookTable.vue | 992 ++++++++++++++++++ .../__tests__/BookCoverCard.spec.ts | 20 +- .../components/__tests__/BookListRow.spec.ts | 20 +- .../__tests__/BookTableCells.spec.ts | 767 ++++++++++++++ .../__tests__/CollapsedSeriesCard.spec.ts | 9 + .../__tests__/KeyboardShortcutOverlay.spec.ts | 100 ++ .../__tests__/MetadataExportDialog.spec.ts | 211 ++++ .../VirtualBookTable.loadmore.spec.ts | 81 ++ .../detail/tabs/CoverEditorPanel.vue | 18 +- .../detail/tabs/CoverSearchDrawer.vue | 87 +- .../detail/tabs/EditMetadataTab.vue | 22 +- .../book/components/table/BookCoverDialog.vue | 154 +++ .../components/table/BookTableActionsCell.vue | 102 ++ .../table/BookTableCellDispatcher.vue | 165 +++ .../components/table/BookTableChipsCell.vue | 120 +++ .../table/BookTableCollapsedSeriesRow.vue | 55 + .../components/table/BookTableContextMenu.vue | 200 ++++ .../components/table/BookTableCoverCell.vue | 92 ++ .../components/table/BookTableDateCell.vue | 62 ++ .../components/table/BookTableFormatCell.vue | 51 + .../book/components/table/BookTableHeader.vue | 159 +++ .../table/BookTableHeaderContextMenu.vue | 190 ++++ .../table/BookTableLockableCell.vue | 44 + .../components/table/BookTableNumberCell.vue | 116 ++ .../table/BookTableProgressCell.vue | 30 + .../components/table/BookTableRatingCell.vue | 86 ++ .../table/BookTableReadStatusCell.vue | 88 ++ .../components/table/BookTableTextCell.vue | 255 +++++ .../__tests__/BookTableCellDispatcher.spec.ts | 100 ++ .../BookTableCollapsedSeriesRow.spec.ts | 156 +++ .../__tests__/tableColumnSchema.spec.ts | 88 ++ .../__tests__/useBookBulkActions.spec.ts | 237 ++++- .../__tests__/useBookDataSource.spec.ts | 235 +++++ .../__tests__/useBookMetadataExport.spec.ts | 221 ++++ .../__tests__/useBookQuery.spec.ts | 440 ++++++++ .../__tests__/useLibraryBooks.spec.ts | 546 ++++++++++ .../__tests__/useSavedViews.spec.ts | 99 ++ .../__tests__/useTableCellEditor.spec.ts | 343 ++++++ .../__tests__/useTableCellHelpers.spec.ts | 208 ++++ .../__tests__/useTableColumns.spec.ts | 366 +++++++ .../__tests__/useTableCoverDialog.spec.ts | 94 ++ .../__tests__/useTableDragReorder.spec.ts | 228 ++++ .../__tests__/useTableKeyboard.spec.ts | 281 +++++ .../__tests__/useTableLocks.spec.ts | 298 ++++++ .../__tests__/useTablePresets.spec.ts | 197 ++++ .../__tests__/useTableQuickFilters.spec.ts | 126 +++ .../__tests__/useTableResize.spec.ts | 122 +++ .../__tests__/useTableScrollBehavior.spec.ts | 135 +++ .../__tests__/useTableSorting.spec.ts | 188 ++++ .../composables/__tests__/useViewSort.spec.ts | 137 +++ .../book/composables/tableColumnSchema.ts | 355 +++++++ .../book/composables/useBookBulkActions.ts | 253 ++++- .../book/composables/useBookDataSource.ts | 111 ++ .../book/composables/useBookMetadataExport.ts | 169 +++ .../features/book/composables/useBookQuery.ts | 80 +- .../composables/useBookRefreshFeedback.ts | 147 +++ .../book/composables/useBookSelection.ts | 11 + .../book/composables/useBookTableShell.ts | 96 ++ .../book/composables/useBookViewContext.ts | 28 + .../book/composables/useBookViewSelection.ts | 28 + .../composables/useInfiniteScrollSentinel.ts | 52 + .../book/composables/useLibraryBooks.ts | 157 +++ .../composables/useMetadataFieldSearch.ts | 23 + .../book/composables/useRefreshMetadata.ts | 8 +- .../book/composables/useRefreshingBooks.ts | 23 +- .../book/composables/useSavedViews.ts | 180 ++++ .../book/composables/useScopedBookQuery.ts | 3 + .../book/composables/useTableCellEditor.ts | 162 +++ .../book/composables/useTableCellHelpers.ts | 50 + .../book/composables/useTableColumns.ts | 173 +++ .../book/composables/useTableContextMenu.ts | 20 + .../book/composables/useTableCoverDialog.ts | 23 + .../book/composables/useTableDragReorder.ts | 83 ++ .../book/composables/useTableKeyboard.ts | 222 ++++ .../book/composables/useTableLocks.ts | 95 ++ .../book/composables/useTablePresets.ts | 173 +++ .../book/composables/useTableQuickFilters.ts | 81 ++ .../book/composables/useTableResize.ts | 83 ++ .../composables/useTableScrollBehavior.ts | 31 + .../book/composables/useTableSorting.ts | 54 + .../book/composables/useTableViewControls.ts | 86 ++ .../features/book/composables/useViewSort.ts | 51 + .../src/features/book/lib/book-card-mapper.ts | 89 ++ client/src/features/book/lib/filter-labels.ts | 5 + .../book/lib/table-row-state-sync.spec.ts | 86 ++ .../features/book/lib/table-row-state-sync.ts | 32 + .../__tests__/useCollectionBooks.spec.ts | 154 +++ .../composables/useCollectionBooks.ts | 51 +- .../collection/composables/useCollections.ts | 21 +- .../series/views/SeriesDetailView.vue | 74 +- .../features/settings/AppearanceSettings.vue | 17 + .../__tests__/useSmartScope.spec.ts | 125 +++ .../smart-scope/composables/useSmartScope.ts | 49 +- .../smart-scope/composables/useSmartScopes.ts | 57 +- .../library/LanguageDistributionChart.vue | 16 +- client/src/views/CollectionView.vue | 380 ++++--- client/src/views/HomeView.vue | 498 ++++++--- client/src/views/SmartScopeView.vue | 393 ++++--- client/vite.config.ts | 4 + .../types/src/__tests__/table-layout.spec.ts | 146 +++ packages/types/src/audit.ts | 1 + packages/types/src/book.ts | 13 +- packages/types/src/font.ts | 36 +- packages/types/src/index.ts | 1 + packages/types/src/query.ts | 44 +- packages/types/src/table-layout.ts | 45 + packages/types/tsconfig.json | 2 +- pnpm-lock.yaml | 29 + server/src/db/migrations/0000_baseline.sql | 3 + .../src/db/migrations/meta/0000_snapshot.json | 65 +- server/src/db/migrations/meta/_journal.json | 2 +- server/src/db/schema/metadata.ts | 6 +- server/src/db/schema/reader.ts | 2 + .../book-metadata-lock.service.ts | 9 + .../book/book-query-builder.service.test.ts | 148 ++- .../book/book-query-builder.service.ts | 125 +-- .../book/book-sort-builder.service.test.ts | 182 ++++ .../modules/book/book-sort-builder.service.ts | 100 ++ .../src/modules/book/book.controller.test.ts | 186 +++- server/src/modules/book/book.controller.ts | 109 +- server/src/modules/book/book.module.test.ts | 3 +- server/src/modules/book/book.module.ts | 3 +- .../src/modules/book/book.repository.test.ts | 16 +- server/src/modules/book/book.repository.ts | 255 +++-- server/src/modules/book/book.service.test.ts | 620 ++++++++++- server/src/modules/book/book.service.ts | 661 +++++++++++- .../book/dto/book-dto-validation.test.ts | 96 +- .../src/modules/book/dto/bulk-book-ids.dto.ts | 9 +- .../book/dto/bulk-query-selection.dto.ts | 22 + .../modules/book/dto/bulk-selection.dto.ts | 24 + .../book/dto/bulk-set-metadata-lock.dto.ts | 10 +- .../modules/book/dto/bulk-set-metadata.dto.ts | 13 + .../modules/book/dto/bulk-set-rating.dto.ts | 10 +- .../modules/book/dto/bulk-set-status.dto.ts | 10 +- .../modules/book/dto/bulk-update-tags.dto.ts | 10 +- .../src/modules/book/dto/delete-books.dto.ts | 9 +- .../book/dto/metadata-export-options.dto.ts | 27 + .../modules/book/dto/metadata-export.dto.ts | 30 + .../book/pipes/book-query.pipe.test.ts | 38 +- .../src/modules/book/pipes/book-query.pipe.ts | 22 +- .../book/utils/assemble-book-cards.test.ts | 130 ++- .../modules/book/utils/assemble-book-cards.ts | 59 +- .../book/utils/group-rule.validator.ts | 84 +- .../collection/collection.controller.test.ts | 17 + .../collection/collection.controller.ts | 7 + .../collection/collection.repository.ts | 9 + .../collection/collection.service.test.ts | 125 +-- .../modules/collection/collection.service.ts | 96 +- .../src/modules/scanner/scanner.repository.ts | 6 +- .../smart-scope.controller.test.ts | 32 + .../smart-scope/smart-scope.controller.ts | 47 +- .../smart-scope/smart-scope.service.test.ts | 232 +++- .../smart-scope/smart-scope.service.ts | 91 +- 174 files changed, 18545 insertions(+), 1521 deletions(-) create mode 100644 client/src/components/ui/InputWithSuggestions.vue create mode 100644 client/src/components/ui/__tests__/InputWithSuggestions.spec.ts create mode 100644 client/src/composables/__tests__/useEffectiveViewMode.spec.ts create mode 100644 client/src/composables/useEffectiveViewMode.ts create mode 100644 client/src/features/book/components/KeyboardShortcutOverlay.vue create mode 100644 client/src/features/book/components/MetadataExportDialog.vue create mode 100644 client/src/features/book/components/TableColumnPanel.vue create mode 100644 client/src/features/book/components/VirtualBookTable.vue create mode 100644 client/src/features/book/components/__tests__/BookTableCells.spec.ts create mode 100644 client/src/features/book/components/__tests__/KeyboardShortcutOverlay.spec.ts create mode 100644 client/src/features/book/components/__tests__/MetadataExportDialog.spec.ts create mode 100644 client/src/features/book/components/__tests__/VirtualBookTable.loadmore.spec.ts create mode 100644 client/src/features/book/components/table/BookCoverDialog.vue create mode 100644 client/src/features/book/components/table/BookTableActionsCell.vue create mode 100644 client/src/features/book/components/table/BookTableCellDispatcher.vue create mode 100644 client/src/features/book/components/table/BookTableChipsCell.vue create mode 100644 client/src/features/book/components/table/BookTableCollapsedSeriesRow.vue create mode 100644 client/src/features/book/components/table/BookTableContextMenu.vue create mode 100644 client/src/features/book/components/table/BookTableCoverCell.vue create mode 100644 client/src/features/book/components/table/BookTableDateCell.vue create mode 100644 client/src/features/book/components/table/BookTableFormatCell.vue create mode 100644 client/src/features/book/components/table/BookTableHeader.vue create mode 100644 client/src/features/book/components/table/BookTableHeaderContextMenu.vue create mode 100644 client/src/features/book/components/table/BookTableLockableCell.vue create mode 100644 client/src/features/book/components/table/BookTableNumberCell.vue create mode 100644 client/src/features/book/components/table/BookTableProgressCell.vue create mode 100644 client/src/features/book/components/table/BookTableRatingCell.vue create mode 100644 client/src/features/book/components/table/BookTableReadStatusCell.vue create mode 100644 client/src/features/book/components/table/BookTableTextCell.vue create mode 100644 client/src/features/book/components/table/__tests__/BookTableCellDispatcher.spec.ts create mode 100644 client/src/features/book/components/table/__tests__/BookTableCollapsedSeriesRow.spec.ts create mode 100644 client/src/features/book/composables/__tests__/tableColumnSchema.spec.ts create mode 100644 client/src/features/book/composables/__tests__/useBookDataSource.spec.ts create mode 100644 client/src/features/book/composables/__tests__/useBookMetadataExport.spec.ts create mode 100644 client/src/features/book/composables/__tests__/useBookQuery.spec.ts create mode 100644 client/src/features/book/composables/__tests__/useLibraryBooks.spec.ts create mode 100644 client/src/features/book/composables/__tests__/useSavedViews.spec.ts create mode 100644 client/src/features/book/composables/__tests__/useTableCellEditor.spec.ts create mode 100644 client/src/features/book/composables/__tests__/useTableCellHelpers.spec.ts create mode 100644 client/src/features/book/composables/__tests__/useTableColumns.spec.ts create mode 100644 client/src/features/book/composables/__tests__/useTableCoverDialog.spec.ts create mode 100644 client/src/features/book/composables/__tests__/useTableDragReorder.spec.ts create mode 100644 client/src/features/book/composables/__tests__/useTableKeyboard.spec.ts create mode 100644 client/src/features/book/composables/__tests__/useTableLocks.spec.ts create mode 100644 client/src/features/book/composables/__tests__/useTablePresets.spec.ts create mode 100644 client/src/features/book/composables/__tests__/useTableQuickFilters.spec.ts create mode 100644 client/src/features/book/composables/__tests__/useTableResize.spec.ts create mode 100644 client/src/features/book/composables/__tests__/useTableScrollBehavior.spec.ts create mode 100644 client/src/features/book/composables/__tests__/useTableSorting.spec.ts create mode 100644 client/src/features/book/composables/__tests__/useViewSort.spec.ts create mode 100644 client/src/features/book/composables/tableColumnSchema.ts create mode 100644 client/src/features/book/composables/useBookDataSource.ts create mode 100644 client/src/features/book/composables/useBookMetadataExport.ts create mode 100644 client/src/features/book/composables/useBookRefreshFeedback.ts create mode 100644 client/src/features/book/composables/useBookTableShell.ts create mode 100644 client/src/features/book/composables/useBookViewContext.ts create mode 100644 client/src/features/book/composables/useBookViewSelection.ts create mode 100644 client/src/features/book/composables/useInfiniteScrollSentinel.ts create mode 100644 client/src/features/book/composables/useLibraryBooks.ts create mode 100644 client/src/features/book/composables/useMetadataFieldSearch.ts create mode 100644 client/src/features/book/composables/useSavedViews.ts create mode 100644 client/src/features/book/composables/useScopedBookQuery.ts create mode 100644 client/src/features/book/composables/useTableCellEditor.ts create mode 100644 client/src/features/book/composables/useTableCellHelpers.ts create mode 100644 client/src/features/book/composables/useTableColumns.ts create mode 100644 client/src/features/book/composables/useTableContextMenu.ts create mode 100644 client/src/features/book/composables/useTableCoverDialog.ts create mode 100644 client/src/features/book/composables/useTableDragReorder.ts create mode 100644 client/src/features/book/composables/useTableKeyboard.ts create mode 100644 client/src/features/book/composables/useTableLocks.ts create mode 100644 client/src/features/book/composables/useTablePresets.ts create mode 100644 client/src/features/book/composables/useTableQuickFilters.ts create mode 100644 client/src/features/book/composables/useTableResize.ts create mode 100644 client/src/features/book/composables/useTableScrollBehavior.ts create mode 100644 client/src/features/book/composables/useTableSorting.ts create mode 100644 client/src/features/book/composables/useTableViewControls.ts create mode 100644 client/src/features/book/composables/useViewSort.ts create mode 100644 client/src/features/book/lib/book-card-mapper.ts create mode 100644 client/src/features/book/lib/table-row-state-sync.spec.ts create mode 100644 client/src/features/book/lib/table-row-state-sync.ts create mode 100644 client/src/features/collection/composables/__tests__/useCollectionBooks.spec.ts create mode 100644 client/src/features/smart-scope/composables/__tests__/useSmartScope.spec.ts create mode 100644 packages/types/src/__tests__/table-layout.spec.ts create mode 100644 packages/types/src/table-layout.ts create mode 100644 server/src/modules/book/book-sort-builder.service.test.ts create mode 100644 server/src/modules/book/book-sort-builder.service.ts create mode 100644 server/src/modules/book/dto/bulk-query-selection.dto.ts create mode 100644 server/src/modules/book/dto/bulk-selection.dto.ts create mode 100644 server/src/modules/book/dto/bulk-set-metadata.dto.ts create mode 100644 server/src/modules/book/dto/metadata-export-options.dto.ts create mode 100644 server/src/modules/book/dto/metadata-export.dto.ts diff --git a/client/package.json b/client/package.json index f2811014c..3f54d1e17 100644 --- a/client/package.json +++ b/client/package.json @@ -23,20 +23,22 @@ "format:check": "prettier --check src/" }, "dependencies": { + "@bookorbit/types": "workspace:*", "@fontsource-variable/fraunces": "^5.2.9", "@fontsource-variable/inter": "^5.2.8", - "@bookorbit/types": "workspace:*", "@tailwindcss/vite": "^4.2.4", + "@tanstack/vue-table": "^8.21.3", + "@tanstack/vue-virtual": "^3.13.24", "@types/howler": "^2.2.12", - "@vueuse/core": "^14.2.1", + "@vueuse/core": "^14.3.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "dompurify": "^3.4.1", + "dompurify": "^3.4.2", "driver.js": "^1.4.0", "echarts": "^6.0.0", "howler": "^2.2.4", "lucide-vue-next": "^1.0.0", - "pdfjs-dist": "^5.6.205", + "pdfjs-dist": "^5.7.284", "pinia": "^3.0.4", "reka-ui": "^2.9.6", "socket.io-client": "^4.8.3", @@ -47,7 +49,7 @@ "vue-echarts": "8.1.0-beta.2", "vue-router": "^5.0.6", "vue-sonner": "^2.0.9", - "vue-virtual-scroller": "^3.0.0" + "vue-virtual-scroller": "^3.0.2" }, "devDependencies": { "@tsconfig/node24": "^24.0.4", @@ -58,16 +60,16 @@ "@vitest/coverage-v8": "^4.1.5", "@vitest/eslint-plugin": "^1.6.16", "@vue/eslint-config-typescript": "^14.7.0", - "@vue/test-utils": "^2.4.8", + "@vue/test-utils": "^2.4.10", "@vue/tsconfig": "^0.9.1", "eslint": "^10.2.1", "eslint-config-prettier": "^10.1.8", - "eslint-plugin-oxlint": "~1.61.0", + "eslint-plugin-oxlint": "~1.62.0", "eslint-plugin-vue": "~10.9.0", "jiti": "^2.6.1", - "jsdom": "^29.0.2", + "jsdom": "^29.1.1", "npm-run-all2": "^8.0.4", - "oxlint": "~1.61.0", + "oxlint": "~1.62.0", "prettier": "3.8.3", "tw-animate-css": "^1.4.0", "typescript": "~5.9.3", diff --git a/client/src/components/AppLayout.vue b/client/src/components/AppLayout.vue index f41277c46..dfbed603b 100644 --- a/client/src/components/AppLayout.vue +++ b/client/src/components/AppLayout.vue @@ -13,6 +13,7 @@ const themeStore = useThemeStore() const backgroundClass = computed(() => BACKGROUND_OPTIONS.find((b) => b.id === themeStore.background)?.cssClass ?? '') const BOOK_ROUTE_NAMES = new Set(['book-detail']) +const TABLE_VIEW_ROUTE_NAMES = new Set(['library', 'smartScope', 'collection', 'series-detail']) const viewKey = computed(() => { const name = String(route.name) if (BOOK_ROUTE_NAMES.has(name)) return name @@ -33,7 +34,13 @@ const viewKey = computed(() => {
-
+
diff --git a/client/src/components/SelectionActionBar.vue b/client/src/components/SelectionActionBar.vue index 1109b123f..c6dc0e1e6 100644 --- a/client/src/components/SelectionActionBar.vue +++ b/client/src/components/SelectionActionBar.vue @@ -3,6 +3,7 @@ import { computed, ref, useSlots, watch } from 'vue' import { BookOpen, Download, + FileSpreadsheet, FolderMinus, FolderPlus, ImageDown, @@ -18,12 +19,14 @@ import { Unlock, X, } from 'lucide-vue-next' +import InputWithSuggestions from '@/components/ui/InputWithSuggestions.vue' +import { usePublisherSearch, useSeriesNameSearch, useLanguageSearch } from '@/features/book/composables/useMetadataFieldSearch' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' import { usePermissions } from '@/features/auth/composables/usePermissions' import { STATUS_ICONS, STATUS_OPTIONS } from '@/features/book/composables/useBookStatus' import type { ReadStatus } from '@bookorbit/types' -import type { InFlightOp } from '@/features/book/composables/useBookBulkActions' +import type { BulkEditableField, BulkEditableValue, InFlightOp } from '@/features/book/composables/useBookBulkActions' export type ExportScope = 'primary' | 'all' | 'audio' @@ -53,31 +56,55 @@ const emit = defineEmits<{ 'remove-from-collection': [] edit: [] send: [] - export: [scope: ExportScope] + download: [scope: ExportScope] + 'export-metadata': [] 'refresh-metadata': [] 're-extract-cover': [] 'set-status': [status: ReadStatus] 'set-rating': [rating: number | null] 'edit-tags': [] + 'set-field': [field: BulkEditableField, value: BulkEditableValue] 'lock-metadata': [locked: boolean] delete: [] exit: [] }>() const { hasPermission, isDemoRestrictedAccount } = usePermissions() +const { search: searchPublisher } = usePublisherSearch() +const { search: searchSeriesName } = useSeriesNameSearch() +const { search: searchLanguage } = useLanguageSearch() const confirmingDelete = ref(false) const deleteInput = ref('') const exportMenuOpen = ref(false) const ratingMenuOpen = ref(false) -const moreMenuOpen = ref(false) +const fieldMenuOpen = ref(false) +const bulkField = ref('publisher') +const fieldValue = ref('') const slots = useSlots() const hasCustomContent = computed(() => Boolean(slots.content)) const canBulkActions = computed(() => !isDemoRestrictedAccount.value) +const canDownload = computed(() => hasPermission('library_download') && canBulkActions.value) +const canEditMetadata = computed(() => hasPermission('library_edit_metadata') && canBulkActions.value) +const canShowMoreMenu = computed(() => canDownload.value || canEditMetadata.value) +const numericFieldSelected = computed(() => bulkField.value === 'publishedYear') +const arrayFieldSelected = computed(() => ['authors', 'genres', 'tags', 'narrators'].includes(bulkField.value)) +const fieldValuePlaceholder = computed(() => (arrayFieldSelected.value ? 'Comma-separated (leave blank to clear)' : 'Leave blank to clear')) +const typeaheadSearchFn = computed<((q: string) => Promise) | null>(() => { + if (bulkField.value === 'seriesName') return searchSeriesName + if (bulkField.value === 'publisher') return searchPublisher + if (bulkField.value === 'language') return searchLanguage + return null +}) const canConfirmDelete = computed(() => props.count <= 50 || deleteInput.value === 'DELETE') +const canApplyFieldValue = computed(() => { + if (!numericFieldSelected.value) return true + const trimmed = fieldValue.value.trim() + return trimmed.length === 0 || !Number.isNaN(Number(trimmed)) +}) function onExport(scope: ExportScope) { - emit('export', scope) + emit('download', scope) exportMenuOpen.value = false } @@ -114,6 +141,56 @@ function unlockAll() { emit('lock-metadata', false) } +function openFieldEditor() { + if (props.count === 0) return + fieldMenuOpen.value = true +} + +function onEditTags() { + if (props.count === 0) return + emit('edit-tags') +} + +function onRefreshMetadata() { + if (props.count === 0) return + emit('refresh-metadata') +} + +function onReExtractCover() { + if (props.count === 0) return + emit('re-extract-cover') +} + +function resetFieldEditor() { + fieldMenuOpen.value = false + bulkField.value = 'publisher' + fieldValue.value = '' +} + +function applyFieldEdit() { + if (!canApplyFieldValue.value) return + const trimmed = fieldValue.value.trim() + let value: BulkEditableValue + if (numericFieldSelected.value) { + value = trimmed ? Number(trimmed) : null + } else if (arrayFieldSelected.value) { + value = trimmed + ? [ + ...new Set( + trimmed + .split(',') + .map((entry) => entry.trim()) + .filter((entry) => entry.length > 0), + ), + ] + : [] + } else { + value = trimmed || null + } + emit('set-field', bulkField.value, value) + resetFieldEditor() +} + watch( () => props.visible, (v) => { @@ -121,8 +198,8 @@ watch( confirmingDelete.value = false exportMenuOpen.value = false ratingMenuOpen.value = false - moreMenuOpen.value = false deleteInput.value = '' + resetFieldEditor() } }, ) @@ -155,7 +232,7 @@ watch( -