added select all option for library items

This commit is contained in:
zipben 2025-06-05 01:08:08 +00:00
parent 140544ba77
commit 9baca4f16c
7 changed files with 169 additions and 10 deletions

View file

@ -499,6 +499,70 @@ export default {
} else { } else {
console.error('Error socket not initialized') 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() { mounted() {

View file

@ -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.addSubtitlesMenuItem(items)
this.addCollapseSeriesMenuItem(items) this.addCollapseSeriesMenuItem(items)
@ -426,12 +433,19 @@ export default {
if (action === 'export-opml') { if (action === 'export-opml') {
this.exportOPML() this.exportOPML()
return return
} else if (action === 'select-all') {
this.selectAllItems()
return
} else if (this.handleSubtitlesAction(action)) { } else if (this.handleSubtitlesAction(action)) {
return return
} else if (this.handleCollapseSeriesAction(action)) { } else if (this.handleCollapseSeriesAction(action)) {
return return
} }
}, },
selectAllItems() {
// Emit an event to the parent component to handle selecting all items
this.$emit('select-all-items')
},
exportOPML() { exportOPML() {
this.$downloadFile(`/api/libraries/${this.currentLibraryId}/opml?token=${this.$store.getters['user/getToken']}`, null, true) this.$downloadFile(`/api/libraries/${this.currentLibraryId}/opml?token=${this.$store.getters['user/getToken']}`, null, true)
}, },
@ -644,7 +658,6 @@ export default {
} }
</script> </script>
<style> <style>
#toolbar { #toolbar {
box-shadow: 0px 8px 6px #111111aa; box-shadow: 0px 8px 6px #111111aa;

View file

@ -880,6 +880,64 @@ export default {
const shelfOffsetY = this.shelfPaddingHeight * this.sizeMultiplier const shelfOffsetY = this.shelfPaddingHeight * this.sizeMultiplier
const shelfOffsetX = (entityIndex - 1) * this.totalEntityCardWidth + this.bookshelfMarginLeft const shelfOffsetX = (entityIndex - 1) * this.totalEntityCardWidth + this.bookshelfMarginLeft
return `translate3d(${shelfOffsetX}px, ${shelfOffsetY}px, 0px)` 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() { async mounted() {

View file

@ -1,7 +1,7 @@
<template> <template>
<div class="page" :class="streamLibraryItem ? 'streaming' : ''"> <div class="page" :class="streamLibraryItem ? 'streaming' : ''">
<app-book-shelf-toolbar :page="id || ''" /> <app-book-shelf-toolbar :page="id || ''" @select-all-items="selectAllItems" />
<app-lazy-bookshelf :page="id || ''" /> <app-lazy-bookshelf ref="lazyBookshelf" :page="id || ''" />
</div> </div>
</template> </template>
@ -44,6 +44,13 @@ export default {
return this.$store.state.streamLibraryItem return this.$store.state.streamLibraryItem
} }
}, },
methods: {} methods: {
selectAllItems() {
if (this.$refs.lazyBookshelf) {
this.$refs.lazyBookshelf.selectAllItems()
} else {
}
}
}
} }
</script> </script>

View file

@ -1,7 +1,7 @@
<template> <template>
<div class="page" :class="streamLibraryItem ? 'streaming' : ''"> <div class="page" :class="streamLibraryItem ? 'streaming' : ''">
<app-book-shelf-toolbar is-home /> <app-book-shelf-toolbar is-home @select-all-items="selectAllItems" />
<app-book-shelf-categorized /> <app-book-shelf-categorized ref="bookShelfCategorized" />
</div> </div>
</template> </template>
@ -25,7 +25,14 @@ export default {
return this.$store.state.streamLibraryItem return this.$store.state.streamLibraryItem
} }
}, },
methods: {}, methods: {
selectAllItems() {
if (this.$refs.bookShelfCategorized) {
this.$refs.bookShelfCategorized.selectAllItems()
} else {
}
}
},
mounted() {}, mounted() {},
beforeDestroy() {} beforeDestroy() {}
} }

View file

@ -1,6 +1,6 @@
<template> <template>
<div class="page" :class="streamLibraryItem ? 'streaming' : ''"> <div class="page" :class="streamLibraryItem ? 'streaming' : ''">
<app-book-shelf-toolbar is-home page="search" :search-query="query" /> <app-book-shelf-toolbar is-home page="search" :search-query="query" @select-all-items="selectAllItems" />
<app-book-shelf-categorized v-if="hasResults" ref="bookshelf" search :results="results" /> <app-book-shelf-categorized v-if="hasResults" ref="bookshelf" search :results="results" />
<div v-else class="w-full py-16"> <div v-else class="w-full py-16">
<p class="text-xl text-center">{{ $getString('MessageNoSearchResultsFor', [query]) }}</p> <p class="text-xl text-center">{{ $getString('MessageNoSearchResultsFor', [query]) }}</p>
@ -11,10 +11,13 @@
<script> <script>
export default { export default {
async asyncData({ store, params, redirect, query, app }) { async asyncData({ store, params, redirect, query, app }) {
if (!query.q) {
return redirect(`/library/${params.library}`)
}
const libraryId = params.library const libraryId = params.library
const library = await store.dispatch('libraries/fetch', libraryId) const library = await store.dispatch('libraries/fetch', libraryId)
if (!library) { if (!library) {
return redirect('/oops?message=Library not found') return redirect(`/oops?message=Library "${libraryId}" not found`)
} }
let results = await app.$axios.$get(`/api/libraries/${libraryId}/search?q=${encodeURIComponent(query.q)}`).catch((error) => { let results = await app.$axios.$get(`/api/libraries/${libraryId}/search?q=${encodeURIComponent(query.q)}`).catch((error) => {
console.error('Failed to search library', error) console.error('Failed to search library', error)
@ -74,6 +77,12 @@ export default {
this.$refs.bookshelf.setShelvesFromSearch() this.$refs.bookshelf.setShelvesFromSearch()
} }
}) })
},
selectAllItems() {
if (this.$refs.bookshelf) {
this.$refs.bookshelf.selectAllItems()
} else {
}
} }
}, },
mounted() {}, mounted() {},

View file

@ -1,4 +1,5 @@
{ {
"LabelSelectAll": "Select All",
"LabelComments": "Comments", "LabelComments": "Comments",
"PlaceholderAddComment": "Add a comment... (Ctrl+Enter to post)", "PlaceholderAddComment": "Add a comment... (Ctrl+Enter to post)",
"ButtonPost": "Post", "ButtonPost": "Post",