audiobookshelf/client/components/app/BookShelfCategorized.vue

438 lines
16 KiB
Vue
Raw Normal View History

2021-09-28 13:44:40 +02:00
<template>
<div id="bookshelf" ref="wrapper" class="w-full max-w-full h-full overflow-y-scroll relative">
2021-09-28 13:44:40 +02:00
<!-- Cover size widget -->
<widgets-cover-size-widget class="fixed bottom-4 right-4 z-50" />
2021-09-28 13:44:40 +02:00
<div v-if="loaded && !shelves.length && !search" class="w-full flex flex-col items-center justify-center py-12">
<p class="text-center text-2xl mb-4 py-4">{{ libraryName }} Library is empty!</p>
<div v-if="userIsAdminOrUp" class="flex">
2021-12-02 02:07:03 +01:00
<ui-btn to="/config" color="primary" class="w-52 mr-2">Configure Scanner</ui-btn>
<ui-btn color="success" class="w-52" @click="scan">Scan Library</ui-btn>
2021-09-28 13:44:40 +02:00
</div>
</div>
<div v-else-if="loaded && !shelves.length && search" class="w-full h-40 flex items-center justify-center">
<p class="text-center text-xl py-4">No results for query</p>
</div>
<!-- Alternate plain view -->
<div v-else-if="isAlternativeBookshelfView" class="w-full mb-24">
<template v-for="(shelf, index) in shelves">
<widgets-item-slider v-if="shelf.type === 'book' || shelf.type === 'podcast'" :shelf-id="shelf.id" :key="index + '.'" :items="shelf.entities" :continue-listening-shelf="shelf.id === 'continue-listening' || shelf.id === 'continue-reading'" :height="232 * sizeMultiplier" class="bookshelf-row pl-8 my-6" @selectEntity="(payload) => selectEntity(payload, index)">
2022-11-09 00:10:08 +01:00
<p class="font-semibold text-gray-100" :style="{ fontSize: sizeMultiplier + 'rem' }">{{ $strings[shelf.labelStringKey] }}</p>
</widgets-item-slider>
<widgets-episode-slider v-else-if="shelf.type === 'episode'" :key="index + '.'" :items="shelf.entities" :continue-listening-shelf="shelf.id === 'continue-listening'" :height="232 * sizeMultiplier" class="bookshelf-row pl-8 my-6" @selectEntity="(payload) => selectEntity(payload, index)">
2022-11-09 00:10:08 +01:00
<p class="font-semibold text-gray-100" :style="{ fontSize: sizeMultiplier + 'rem' }">{{ $strings[shelf.labelStringKey] }}</p>
</widgets-episode-slider>
<widgets-series-slider v-else-if="shelf.type === 'series'" :key="index + '.'" :items="shelf.entities" :height="232 * sizeMultiplier" class="bookshelf-row pl-8 my-6">
2022-11-09 00:10:08 +01:00
<p class="font-semibold text-gray-100" :style="{ fontSize: sizeMultiplier + 'rem' }">{{ $strings[shelf.labelStringKey] }}</p>
</widgets-series-slider>
<widgets-authors-slider v-else-if="shelf.type === 'authors'" :key="index + '.'" :items="shelf.entities" :height="192 * sizeMultiplier" class="bookshelf-row pl-8 my-6">
2022-11-09 00:10:08 +01:00
<p class="font-semibold text-gray-100" :style="{ fontSize: sizeMultiplier + 'rem' }">{{ $strings[shelf.labelStringKey] }}</p>
</widgets-authors-slider>
2023-04-25 01:25:30 +02:00
<widgets-narrators-slider v-else-if="shelf.type === 'narrators'" :key="index + '.'" :items="shelf.entities" :height="100 * sizeMultiplier" class="bookshelf-row pl-8 my-6">
<p class="font-semibold text-gray-100" :style="{ fontSize: sizeMultiplier + 'rem' }">{{ $strings[shelf.labelStringKey] }}</p>
</widgets-narrators-slider>
</template>
</div>
<!-- Regular bookshelf view -->
<div v-else class="w-full">
2021-09-28 13:44:40 +02:00
<template v-for="(shelf, index) in shelves">
<app-book-shelf-row :key="index" :index="index" :shelf="shelf" :size-multiplier="sizeMultiplier" :book-cover-width="bookCoverWidth" :book-cover-aspect-ratio="coverAspectRatio" :continue-listening-shelf="shelf.id === 'continue-listening' || shelf.id === 'continue-reading'" @selectEntity="(payload) => selectEntity(payload, index)" />
2021-09-28 13:44:40 +02:00
</template>
</div>
</div>
</template>
<script>
export default {
props: {
search: Boolean,
results: {
type: Object,
default: () => {}
}
},
2021-09-28 13:44:40 +02:00
data() {
return {
loaded: false,
2021-09-28 13:44:40 +02:00
keywordFilterTimeout: null,
scannerParseSubtitle: false,
wrapperClientWidth: 0,
shelves: [],
lastItemIndexSelected: -1
2021-09-28 13:44:40 +02:00
}
},
computed: {
userIsAdminOrUp() {
return this.$store.getters['user/getIsAdminOrUp']
2021-12-02 02:07:03 +01:00
},
showExperimentalFeatures() {
return this.$store.state.showExperimentalFeatures
},
currentLibraryId() {
return this.$store.state.libraries.currentLibraryId
2021-09-28 13:44:40 +02:00
},
libraryName() {
return this.$store.getters['libraries/getCurrentLibraryName']
},
isAlternativeBookshelfView() {
return this.$store.getters['getHomeBookshelfView'] === this.$constants.BookshelfView.DETAIL
},
2021-09-28 13:44:40 +02:00
bookCoverWidth() {
var coverSize = this.$store.getters['user/getUserSetting']('bookshelfCoverSize')
if (this.isCoverSquareAspectRatio) return coverSize * 1.6
return coverSize
},
coverAspectRatio() {
return this.$store.getters['libraries/getBookCoverAspectRatio']
},
isCoverSquareAspectRatio() {
return this.coverAspectRatio == 1
2021-09-28 13:44:40 +02:00
},
sizeMultiplier() {
var baseSize = this.isCoverSquareAspectRatio ? 192 : 120
return this.bookCoverWidth / baseSize
},
selectedMediaItems() {
return this.$store.state.globals.selectedMediaItems || []
2021-09-28 13:44:40 +02:00
}
},
methods: {
selectEntity({ entity, shiftKey }, shelfIndex) {
const shelf = this.shelves[shelfIndex]
const entityShelfIndex = shelf.entities.findIndex((ent) => ent.id === entity.id)
const indexOf = shelf.shelfStartIndex + entityShelfIndex
const lastLastItemIndexSelected = this.lastItemIndexSelected
if (!this.selectedMediaItems.some((i) => i.id === entity.id)) {
this.lastItemIndexSelected = indexOf
} else {
this.lastItemIndexSelected = -1
}
if (shiftKey && lastLastItemIndexSelected >= 0) {
let loopStart = indexOf
let loopEnd = lastLastItemIndexSelected
if (indexOf > lastLastItemIndexSelected) {
loopStart = lastLastItemIndexSelected
loopEnd = indexOf
}
const flattenedEntitiesArray = []
this.shelves.map((s) => flattenedEntitiesArray.push(...s.entities))
let isSelecting = false
// If any items in this range is not selected then select all otherwise unselect all
for (let i = loopStart; i <= loopEnd; i++) {
const thisEntity = flattenedEntitiesArray[i]
if (thisEntity) {
if (!this.selectedMediaItems.some((i) => i.id === thisEntity.id)) {
isSelecting = true
break
}
}
}
if (isSelecting) this.lastItemIndexSelected = indexOf
for (let i = loopStart; i <= loopEnd; i++) {
const thisEntity = flattenedEntitiesArray[i]
if (thisEntity) {
const mediaItem = {
id: thisEntity.id,
mediaType: thisEntity.mediaType,
2023-01-04 01:00:01 +01:00
hasTracks: thisEntity.mediaType === 'podcast' || thisEntity.media.audioFile || thisEntity.media.numTracks || (thisEntity.media.tracks && thisEntity.media.tracks.length)
}
this.$store.commit('globals/setMediaItemSelected', { item: mediaItem, selected: isSelecting })
} else {
console.error('Invalid entity index', i)
}
}
} else {
const mediaItem = {
id: entity.id,
mediaType: entity.mediaType,
2023-01-04 01:00:01 +01:00
hasTracks: entity.mediaType === 'podcast' || entity.media.audioFile || entity.media.numTracks || (entity.media.tracks && entity.media.tracks.length)
}
this.$store.commit('globals/toggleMediaItemSelected', mediaItem)
}
this.$nextTick(() => {
this.$eventBus.$emit('item-selected', entity)
})
},
2021-09-28 13:44:40 +02:00
async init() {
this.wrapperClientWidth = this.$refs.wrapper ? this.$refs.wrapper.clientWidth : 0
if (this.search) {
this.setShelvesFromSearch()
} else {
2021-12-02 02:07:03 +01:00
await this.fetchCategories()
}
this.loaded = true
},
2021-12-02 02:07:03 +01:00
async fetchCategories() {
const categories = await this.$axios
.$get(`/api/libraries/${this.currentLibraryId}/personalized?include=rssfeed`)
2021-12-02 02:07:03 +01:00
.then((data) => {
return data
})
.catch((error) => {
console.error('Failed to fetch categories', error)
return []
})
let totalEntityCount = 0
for (const shelf of categories) {
shelf.shelfStartIndex = totalEntityCount
totalEntityCount += shelf.entities.length
}
2021-12-02 02:07:03 +01:00
this.shelves = categories
},
async setShelvesFromSearch() {
2023-04-25 01:25:30 +02:00
const shelves = []
if (this.results.books?.length) {
shelves.push({
id: 'books',
label: 'Books',
2022-11-09 00:10:08 +01:00
labelStringKey: 'LabelBooks',
type: 'book',
entities: this.results.books.map((res) => res.libraryItem)
})
}
2021-12-02 02:07:03 +01:00
2023-04-25 01:25:30 +02:00
if (this.results.podcasts?.length) {
2022-04-12 23:54:52 +02:00
shelves.push({
id: 'podcasts',
label: 'Podcasts',
2022-11-09 00:10:08 +01:00
labelStringKey: 'LabelPodcasts',
2022-04-12 23:54:52 +02:00
type: 'podcast',
entities: this.results.podcasts.map((res) => res.libraryItem)
})
}
2023-04-25 01:25:30 +02:00
if (this.results.series?.length) {
shelves.push({
id: 'series',
label: 'Series',
2022-11-09 00:10:08 +01:00
labelStringKey: 'LabelSeries',
type: 'series',
entities: this.results.series.map((seriesObj) => {
return {
...seriesObj.series,
books: seriesObj.books,
type: 'series'
}
})
})
}
2023-04-25 01:25:30 +02:00
if (this.results.tags?.length) {
shelves.push({
id: 'tags',
label: 'Tags',
2022-11-09 00:10:08 +01:00
labelStringKey: 'LabelTags',
type: 'tags',
entities: this.results.tags.map((tagObj) => {
return {
name: tagObj.name,
books: tagObj.books || [],
type: 'tags'
}
})
})
}
2023-04-25 01:25:30 +02:00
if (this.results.authors?.length) {
shelves.push({
id: 'authors',
label: 'Authors',
2022-11-09 00:10:08 +01:00
labelStringKey: 'LabelAuthors',
type: 'authors',
entities: this.results.authors.map((a) => {
return {
...a,
type: 'author'
}
})
})
}
2023-04-25 01:25:30 +02:00
if (this.results.narrators?.length) {
shelves.push({
id: 'narrators',
label: 'Narrators',
labelStringKey: 'LabelNarrators',
type: 'narrators',
entities: this.results.narrators.map((n) => {
return {
...n,
type: 'narrator'
}
})
})
}
this.shelves = shelves
2021-09-28 13:44:40 +02:00
},
2021-12-02 02:07:03 +01:00
scan() {
2022-05-18 23:33:24 +02:00
this.$store
.dispatch('libraries/requestLibraryScan', { libraryId: this.$store.state.libraries.currentLibraryId })
.then(() => {
this.$toast.success('Library scan started')
})
.catch((error) => {
console.error('Failed to start scan', error)
this.$toast.error('Failed to start scan')
})
2021-12-02 02:07:03 +01:00
},
userUpdated(user) {
if (user.seriesHideFromContinueListening && user.seriesHideFromContinueListening.length) {
this.removeAllSeriesFromContinueSeries(user.seriesHideFromContinueListening)
}
if (user.mediaProgress.length) {
const mediaProgressToHide = user.mediaProgress.filter((mp) => mp.hideFromContinueListening)
this.removeItemsFromContinueListeningReading(mediaProgressToHide, 'continue-listening')
this.removeItemsFromContinueListeningReading(mediaProgressToHide, 'continue-reading')
}
},
libraryItemAdded(libraryItem) {
console.log('libraryItem added', libraryItem)
// TODO: Check if libraryItem would be on this shelf
2021-12-02 02:07:03 +01:00
if (!this.search) {
this.fetchCategories()
}
},
libraryItemUpdated(libraryItem) {
console.log('libraryItem updated', libraryItem)
2021-12-02 02:07:03 +01:00
this.shelves.forEach((shelf) => {
if (shelf.type == 'book' || shelf.type == 'podcast') {
2021-12-02 02:07:03 +01:00
shelf.entities = shelf.entities.map((ent) => {
if (ent.id === libraryItem.id) {
return libraryItem
2021-12-02 02:07:03 +01:00
}
return ent
})
} else if (shelf.type === 'series') {
shelf.entities.forEach((ent) => {
ent.books = ent.books.map((book) => {
if (book.id === libraryItem.id) return libraryItem
2021-12-02 02:07:03 +01:00
return book
})
})
2021-09-28 13:44:40 +02:00
}
2021-12-02 02:07:03 +01:00
})
},
removeBookFromShelf(libraryItem) {
2021-12-02 02:07:03 +01:00
this.shelves.forEach((shelf) => {
if (shelf.type == 'book' || shelf.type == 'podcast') {
2021-12-02 02:07:03 +01:00
shelf.entities = shelf.entities.filter((ent) => {
return ent.id !== libraryItem.id
2021-12-02 02:07:03 +01:00
})
} else if (shelf.type === 'series') {
shelf.entities.forEach((ent) => {
ent.books = ent.books.filter((book) => {
return book.id !== libraryItem.id
2021-12-02 02:07:03 +01:00
})
})
}
})
},
libraryItemRemoved(libraryItem) {
this.removeBookFromShelf(libraryItem)
2021-12-02 02:07:03 +01:00
},
libraryItemsAdded(libraryItems) {
console.log('libraryItems added', libraryItems)
2021-12-02 02:07:03 +01:00
// TODO: Check if audiobook would be on this shelf
if (!this.search) {
this.fetchCategories()
2021-09-28 13:44:40 +02:00
}
},
libraryItemsUpdated(items) {
items.forEach((li) => {
this.libraryItemUpdated(li)
2021-12-02 02:07:03 +01:00
})
},
removeAllSeriesFromContinueSeries(seriesIds) {
this.shelves.forEach((shelf) => {
if (shelf.type == 'book' && shelf.id == 'continue-series') {
// Filter out series books from continue series shelf
shelf.entities = shelf.entities.filter((ent) => {
if (ent.media.metadata.series && seriesIds.includes(ent.media.metadata.series.id)) return false
return true
})
}
})
},
removeItemsFromContinueListeningReading(mediaProgressItems, categoryId) {
const continueListeningShelf = this.shelves.find((s) => s.id === categoryId)
if (continueListeningShelf) {
if (continueListeningShelf.type === 'book') {
continueListeningShelf.entities = continueListeningShelf.entities.filter((ent) => {
if (mediaProgressItems.some((mp) => mp.libraryItemId === ent.id)) return false
return true
})
} else if (continueListeningShelf.type === 'episode') {
continueListeningShelf.entities = continueListeningShelf.entities.filter((ent) => {
if (!ent.recentEpisode) return true // Should always have this here
if (mediaProgressItems.some((mp) => mp.libraryItemId === ent.id && mp.episodeId === ent.recentEpisode.id)) return false
return true
})
}
}
},
authorUpdated(author) {
this.shelves.forEach((shelf) => {
if (shelf.type == 'authors') {
shelf.entities = shelf.entities.map((ent) => {
if (ent.id === author.id) {
return {
...ent,
...author
}
}
return ent
})
}
})
},
authorRemoved(author) {
this.shelves.forEach((shelf) => {
if (shelf.type == 'authors') {
shelf.entities = shelf.entities.filter((ent) => ent.id != author.id)
}
})
},
2021-12-02 02:07:03 +01:00
initListeners() {
if (this.$root.socket) {
this.$root.socket.on('user_updated', this.userUpdated)
this.$root.socket.on('author_updated', this.authorUpdated)
this.$root.socket.on('author_removed', this.authorRemoved)
this.$root.socket.on('item_updated', this.libraryItemUpdated)
this.$root.socket.on('item_added', this.libraryItemAdded)
this.$root.socket.on('item_removed', this.libraryItemRemoved)
this.$root.socket.on('items_updated', this.libraryItemsUpdated)
this.$root.socket.on('items_added', this.libraryItemsAdded)
2021-12-02 02:07:03 +01:00
} else {
console.error('Error socket not initialized')
}
},
removeListeners() {
if (this.$root.socket) {
this.$root.socket.off('user_updated', this.userUpdated)
this.$root.socket.off('author_updated', this.authorUpdated)
this.$root.socket.off('author_removed', this.authorRemoved)
this.$root.socket.off('item_updated', this.libraryItemUpdated)
this.$root.socket.off('item_added', this.libraryItemAdded)
this.$root.socket.off('item_removed', this.libraryItemRemoved)
this.$root.socket.off('items_updated', this.libraryItemsUpdated)
this.$root.socket.off('items_added', this.libraryItemsAdded)
2021-12-02 02:07:03 +01:00
} else {
console.error('Error socket not initialized')
}
2021-09-28 13:44:40 +02:00
}
},
mounted() {
2021-12-02 02:07:03 +01:00
this.initListeners()
2021-09-28 13:44:40 +02:00
this.init()
},
beforeDestroy() {
2021-12-02 02:07:03 +01:00
this.removeListeners()
2021-09-28 13:44:40 +02:00
}
}
</script>