diff --git a/client/components/app/BookShelfCategorized.vue b/client/components/app/BookShelfCategorized.vue index e69183214..b00841a64 100644 --- a/client/components/app/BookShelfCategorized.vue +++ b/client/components/app/BookShelfCategorized.vue @@ -499,6 +499,70 @@ export default { } else { console.error('Error socket not initialized') } + }, + selectAllItems() { + console.log('BookShelfCategorized: selectAllItems called!') + console.log('BookShelfCategorized: shelves:', this.shelves) + + // Get all visible items from shelves + const allVisibleEntities = [] + this.shelves.forEach((shelf) => { + console.log('BookShelfCategorized: processing shelf:', shelf.label, 'type:', shelf.type, 'entities:', shelf.entities.length) + if (shelf.type === 'book' || shelf.type === 'podcast') { + allVisibleEntities.push(...shelf.entities) + } + }) + + // Check if all visible items are already selected + const selectedMediaItems = this.$store.state.globals.selectedMediaItems + const visibleEntityIds = allVisibleEntities.map((e) => e.id) + const selectedVisibleCount = selectedMediaItems.filter((item) => visibleEntityIds.includes(item.id)).length + const shouldDeselect = selectedVisibleCount === visibleEntityIds.length && selectedVisibleCount > 0 + + console.log('BookShelfCategorized: visible entities:', visibleEntityIds.length, 'selected visible:', selectedVisibleCount, 'shouldDeselect:', shouldDeselect) + + if (shouldDeselect) { + // Deselect all visible items + this.shelves.forEach((shelf) => { + console.log('BookShelfCategorized: processing shelf for deselection:', shelf.label, 'type:', shelf.type, 'entities:', shelf.entities.length) + if (shelf.type === 'book' || shelf.type === 'podcast') { + shelf.entities.forEach((entity) => { + console.log('BookShelfCategorized: deselecting entity:', entity.id, entity.mediaType) + // Create the media item object the same way the individual deselect does + const mediaItem = { + id: entity.id, + mediaType: entity.mediaType, + hasTracks: entity.mediaType === 'podcast' || entity.media.audioFile || entity.media.numTracks || (entity.media.tracks && entity.media.tracks.length) + } + + // Remove the item from selection + this.$store.commit('globals/setMediaItemSelected', { item: mediaItem, selected: false }) + }) + } + }) + } else { + // Select all visible items + this.shelves.forEach((shelf) => { + if (shelf.type === 'book' || shelf.type === 'podcast') { + shelf.entities.forEach((entity) => { + // Create the media item object the same way the individual select does + const mediaItem = { + id: entity.id, + mediaType: entity.mediaType, + hasTracks: entity.mediaType === 'podcast' || entity.media.audioFile || entity.media.numTracks || (entity.media.tracks && entity.media.tracks.length) + } + + // Use the existing store mutation to mark the item as selected + this.$store.commit('globals/setMediaItemSelected', { item: mediaItem, selected: true }) + }) + } + }) + } + + // Trigger a single UI update after all items are selected/deselected + this.$nextTick(() => { + this.$eventBus.$emit('item-selected') + }) } }, mounted() { diff --git a/client/components/app/BookShelfToolbar.vue b/client/components/app/BookShelfToolbar.vue index 95e7c378c..8dd7a8819 100644 --- a/client/components/app/BookShelfToolbar.vue +++ b/client/components/app/BookShelfToolbar.vue @@ -328,6 +328,13 @@ export default { }) } + if (!this.isBatchSelecting && (!this.page || this.page === 'search')) { + items.push({ + text: 'Select All', + action: 'select-all' + }) + } + this.addSubtitlesMenuItem(items) this.addCollapseSeriesMenuItem(items) @@ -426,12 +433,19 @@ export default { if (action === 'export-opml') { this.exportOPML() return + } else if (action === 'select-all') { + this.selectAllItems() + return } else if (this.handleSubtitlesAction(action)) { return } else if (this.handleCollapseSeriesAction(action)) { return } }, + selectAllItems() { + // Emit an event to the parent component to handle selecting all items + this.$emit('select-all-items') + }, exportOPML() { this.$downloadFile(`/api/libraries/${this.currentLibraryId}/opml?token=${this.$store.getters['user/getToken']}`, null, true) }, @@ -644,9 +658,8 @@ export default { } - + \ No newline at end of file diff --git a/client/components/app/LazyBookshelf.vue b/client/components/app/LazyBookshelf.vue index 9dee6a73e..c4612e7a5 100644 --- a/client/components/app/LazyBookshelf.vue +++ b/client/components/app/LazyBookshelf.vue @@ -880,6 +880,64 @@ export default { const shelfOffsetY = this.shelfPaddingHeight * this.sizeMultiplier const shelfOffsetX = (entityIndex - 1) * this.totalEntityCardWidth + this.bookshelfMarginLeft return `translate3d(${shelfOffsetX}px, ${shelfOffsetY}px, 0px)` + }, + selectAllItems() { + // Check if all visible items are already selected + const selectedMediaItems = this.$store.state.globals.selectedMediaItems + const visibleEntityIds = this.entities.filter((e) => e).map((e) => e.id) + const selectedVisibleCount = selectedMediaItems.filter((item) => visibleEntityIds.includes(item.id)).length + const shouldDeselect = selectedVisibleCount === visibleEntityIds.length && selectedVisibleCount > 0 + + if (shouldDeselect) { + // Deselect all visible items + this.entities.forEach((entity) => { + if (entity) { + // Create the media item object the same way the individual deselect does + const mediaItem = { + id: entity.id, + mediaType: entity.mediaType, + hasTracks: entity.mediaType === 'podcast' || entity.media.audioFile || entity.media.numTracks || (entity.media.tracks && entity.media.tracks.length) + } + + // Remove the item from selection + this.$store.commit('globals/setMediaItemSelected', { item: mediaItem, selected: false }) + } + }) + } else { + // Select all visible items + this.entities.forEach((entity) => { + if (entity) { + // Create the media item object the same way the individual select does + const mediaItem = { + id: entity.id, + mediaType: entity.mediaType, + hasTracks: entity.mediaType === 'podcast' || entity.media.audioFile || entity.media.numTracks || (entity.media.tracks && entity.media.tracks.length) + } + + // Use the existing store mutation to mark the item as selected + this.$store.commit('globals/setMediaItemSelected', { item: mediaItem, selected: true }) + } + }) + } + + // Force update all card components to reflect the selection state + this.$nextTick(() => { + this.updateBookSelectionMode(true) + + // Also update the selected property of each card component + const updatedSelectedItems = this.$store.state.globals.selectedMediaItems + + for (const key in this.entityComponentRefs) { + if (this.entityIndexesMounted.includes(Number(key))) { + const component = this.entityComponentRefs[key] + const entity = this.entities[key] + if (component && entity) { + const isSelected = updatedSelectedItems.some((item) => item.id === entity.id) + component.selected = isSelected + } + } + } + }) } }, async mounted() { diff --git a/client/pages/library/_library/bookshelf/_id.vue b/client/pages/library/_library/bookshelf/_id.vue index 72cfba4a9..8868bcf3e 100644 --- a/client/pages/library/_library/bookshelf/_id.vue +++ b/client/pages/library/_library/bookshelf/_id.vue @@ -1,7 +1,7 @@ - - + + @@ -44,6 +44,13 @@ export default { return this.$store.state.streamLibraryItem } }, - methods: {} + methods: { + selectAllItems() { + if (this.$refs.lazyBookshelf) { + this.$refs.lazyBookshelf.selectAllItems() + } else { + } + } + } } diff --git a/client/pages/library/_library/index.vue b/client/pages/library/_library/index.vue index f235028c7..f3c32a4a2 100644 --- a/client/pages/library/_library/index.vue +++ b/client/pages/library/_library/index.vue @@ -1,7 +1,7 @@ - - + + @@ -25,7 +25,14 @@ export default { return this.$store.state.streamLibraryItem } }, - methods: {}, + methods: { + selectAllItems() { + if (this.$refs.bookShelfCategorized) { + this.$refs.bookShelfCategorized.selectAllItems() + } else { + } + } + }, mounted() {}, beforeDestroy() {} } diff --git a/client/pages/library/_library/search.vue b/client/pages/library/_library/search.vue index 429603bff..cd6e1275a 100644 --- a/client/pages/library/_library/search.vue +++ b/client/pages/library/_library/search.vue @@ -1,6 +1,6 @@ - + {{ $getString('MessageNoSearchResultsFor', [query]) }} @@ -11,10 +11,13 @@
{{ $getString('MessageNoSearchResultsFor', [query]) }}