mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-12-30 15:49:38 +00:00
Lazy bookshelf, api routes for categories and filter data
This commit is contained in:
parent
4587916c8e
commit
5c92aef048
26 changed files with 1354 additions and 332 deletions
|
|
@ -40,7 +40,7 @@
|
|||
<div v-show="numAudiobooksSelected" class="absolute top-0 left-0 w-full h-full px-4 bg-primary flex items-center">
|
||||
<h1 class="text-2xl px-4">{{ numAudiobooksSelected }} Selected</h1>
|
||||
<ui-btn v-show="!isHome" small class="text-sm mx-2" @click="toggleSelectAll"
|
||||
>{{ isAllSelected ? 'Select None' : 'Select All' }}<span class="pl-2">({{ audiobooksShowing.length }})</span></ui-btn
|
||||
>{{ isAllSelected ? 'Select None' : 'Select All' }}<span class="pl-2">({{ totalBooks }})</span></ui-btn
|
||||
>
|
||||
|
||||
<div class="flex-grow" />
|
||||
|
|
@ -65,7 +65,9 @@
|
|||
export default {
|
||||
data() {
|
||||
return {
|
||||
processingBatchDelete: false
|
||||
processingBatchDelete: false,
|
||||
totalBooks: 0,
|
||||
isAllSelected: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -96,9 +98,9 @@ export default {
|
|||
selectedAudiobooks() {
|
||||
return this.$store.state.selectedAudiobooks
|
||||
},
|
||||
isAllSelected() {
|
||||
return this.audiobooksShowing.length === this.selectedAudiobooks.length
|
||||
},
|
||||
// isAllSelected() {
|
||||
// return this.audiobooksShowing.length === this.selectedAudiobooks.length
|
||||
// },
|
||||
userAudiobooks() {
|
||||
return this.$store.state.user.user.audiobooks || {}
|
||||
},
|
||||
|
|
@ -145,13 +147,17 @@ export default {
|
|||
cancelSelectionMode() {
|
||||
if (this.processingBatchDelete) return
|
||||
this.$store.commit('setSelectedAudiobooks', [])
|
||||
this.$eventBus.$emit('bookshelf-clear-selection')
|
||||
this.isAllSelected = false
|
||||
},
|
||||
toggleSelectAll() {
|
||||
if (this.isAllSelected) {
|
||||
this.cancelSelectionMode()
|
||||
} else {
|
||||
var audiobookIds = this.audiobooksShowing.map((a) => a.id)
|
||||
this.$store.commit('setSelectedAudiobooks', audiobookIds)
|
||||
this.$eventBus.$emit('bookshelf-select-all')
|
||||
this.isAllSelected = true
|
||||
// var audiobookIds = this.audiobooksShowing.map((a) => a.id)
|
||||
// this.$store.commit('setSelectedAudiobooks', audiobookIds)
|
||||
}
|
||||
},
|
||||
toggleBatchRead() {
|
||||
|
|
@ -205,9 +211,17 @@ export default {
|
|||
},
|
||||
batchAddToCollectionClick() {
|
||||
this.$store.commit('globals/setShowBatchUserCollectionsModal', true)
|
||||
},
|
||||
setBookshelfTotalBooks(totalBooks) {
|
||||
this.totalBooks = totalBooks
|
||||
}
|
||||
},
|
||||
mounted() {}
|
||||
mounted() {
|
||||
this.$eventBus.$on('bookshelf-total-books', this.setBookshelfTotalBooks)
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.$eventBus.$off('bookshelf-total-books', this.setBookshelfTotalBooks)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
<div class="rounded-full py-1 bg-primary hover:bg-bg cursor-pointer px-2 border border-black-100 text-center flex items-center box-shadow-md" @mousedown.prevent @mouseup.prevent @click="showBookshelfTextureModal"><p class="text-sm py-0.5">Texture</p></div>
|
||||
</div>
|
||||
|
||||
<div v-if="!audiobooks.length" class="w-full flex flex-col items-center justify-center py-12">
|
||||
<div v-if="loaded && !shelves.length" class="w-full flex flex-col items-center justify-center py-12">
|
||||
<p class="text-center text-2xl font-book mb-4 py-4">Your Audiobookshelf is empty!</p>
|
||||
<div class="flex">
|
||||
<ui-btn to="/config" color="primary" class="w-52 mr-2" @click="scan">Configure Scanner</ui-btn>
|
||||
|
|
@ -30,89 +30,36 @@
|
|||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
search: Boolean,
|
||||
results: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loaded: false,
|
||||
availableSizes: [60, 80, 100, 120, 140, 160, 180, 200, 220],
|
||||
selectedSizeIndex: 3,
|
||||
rowPaddingX: 40,
|
||||
keywordFilterTimeout: null,
|
||||
scannerParseSubtitle: false,
|
||||
wrapperClientWidth: 0,
|
||||
overflowingShelvesRight: {},
|
||||
overflowingShelvesLeft: {}
|
||||
shelves: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
showExperimentalFeatures() {
|
||||
return this.$store.state.showExperimentalFeatures
|
||||
},
|
||||
userAudiobooks() {
|
||||
return this.$store.state.user.user ? this.$store.state.user.user.audiobooks || {} : {}
|
||||
},
|
||||
audiobooks() {
|
||||
return this.$store.state.audiobooks.audiobooks
|
||||
currentLibraryId() {
|
||||
return this.$store.state.libraries.currentLibraryId
|
||||
},
|
||||
bookCoverWidth() {
|
||||
return this.availableSizes[this.selectedSizeIndex]
|
||||
},
|
||||
sizeMultiplier() {
|
||||
return this.bookCoverWidth / 120
|
||||
},
|
||||
paddingX() {
|
||||
return 16 * this.sizeMultiplier
|
||||
},
|
||||
bookWidth() {
|
||||
return this.bookCoverWidth + this.paddingX * 2
|
||||
},
|
||||
mostRecentPlayed() {
|
||||
var audiobooks = this.audiobooks.filter((ab) => this.userAudiobooks[ab.id] && this.userAudiobooks[ab.id].lastUpdate > 0 && this.userAudiobooks[ab.id].progress > 0 && !this.userAudiobooks[ab.id].isRead).map((ab) => ({ ...ab }))
|
||||
audiobooks.sort((a, b) => {
|
||||
return this.userAudiobooks[b.id].lastUpdate - this.userAudiobooks[a.id].lastUpdate
|
||||
})
|
||||
return audiobooks.slice(0, 10)
|
||||
},
|
||||
mostRecentAdded() {
|
||||
var audiobooks = this.audiobooks.map((ab) => ({ ...ab })).sort((a, b) => b.addedAt - a.addedAt)
|
||||
return audiobooks.slice(0, 10)
|
||||
},
|
||||
seriesGroups() {
|
||||
return this.$store.getters['audiobooks/getSeriesGroups']()
|
||||
},
|
||||
recentlyUpdatedSeries() {
|
||||
var mostRecentTime = 0
|
||||
var mostRecentSeries = null
|
||||
this.seriesGroups.forEach((series) => {
|
||||
if ((series.books.length && mostRecentSeries === null) || series.lastUpdate > mostRecentTime) {
|
||||
mostRecentTime = series.lastUpdate
|
||||
mostRecentSeries = series
|
||||
}
|
||||
})
|
||||
if (!mostRecentSeries) return null
|
||||
return mostRecentSeries.books
|
||||
},
|
||||
booksRecentlyRead() {
|
||||
var audiobooks = this.audiobooks.filter((ab) => this.userAudiobooks[ab.id] && this.userAudiobooks[ab.id].isRead).map((ab) => ({ ...ab }))
|
||||
audiobooks.sort((a, b) => {
|
||||
return this.userAudiobooks[b.id].finishedAt - this.userAudiobooks[a.id].finishedAt
|
||||
})
|
||||
return audiobooks.slice(0, 10)
|
||||
},
|
||||
shelves() {
|
||||
var shelves = []
|
||||
if (this.mostRecentPlayed.length) {
|
||||
shelves.push({ books: this.mostRecentPlayed, label: 'Continue Reading' })
|
||||
}
|
||||
|
||||
shelves.push({ books: this.mostRecentAdded, label: 'Recently Added' })
|
||||
|
||||
if (this.recentlyUpdatedSeries) {
|
||||
shelves.push({ books: this.recentlyUpdatedSeries, label: 'Newest Series' })
|
||||
}
|
||||
|
||||
if (this.booksRecentlyRead.length) {
|
||||
shelves.push({ books: this.booksRecentlyRead, label: 'Read Again' })
|
||||
}
|
||||
return shelves
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
|
@ -136,10 +83,73 @@ export default {
|
|||
var sizeIndex = this.availableSizes.findIndex((s) => s === bookshelfCoverSize)
|
||||
if (!isNaN(sizeIndex)) this.selectedSizeIndex = sizeIndex
|
||||
|
||||
await this.$store.dispatch('audiobooks/load')
|
||||
// await this.$store.dispatch('audiobooks/load')
|
||||
if (this.search) {
|
||||
this.setShelvesFromSearch()
|
||||
} else {
|
||||
var categories = await this.$axios
|
||||
.$get(`/api/libraries/${this.currentLibraryId}/categories`)
|
||||
.then((data) => {
|
||||
console.log('Category data', data)
|
||||
return data
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to fetch cats', error)
|
||||
})
|
||||
this.shelves = categories
|
||||
}
|
||||
|
||||
this.loaded = true
|
||||
},
|
||||
async setShelvesFromSearch() {
|
||||
var shelves = []
|
||||
if (this.results.audiobooks) {
|
||||
shelves.push({
|
||||
id: 'audiobooks',
|
||||
label: 'Books',
|
||||
type: 'books',
|
||||
entities: this.results.audiobooks.map((ab) => ab.audiobook)
|
||||
})
|
||||
}
|
||||
if (this.results.authors) {
|
||||
shelves.push({
|
||||
id: 'authors',
|
||||
label: 'Authors',
|
||||
type: 'authors',
|
||||
entities: this.results.authors.map((a) => a.author)
|
||||
})
|
||||
}
|
||||
if (this.results.series) {
|
||||
shelves.push({
|
||||
id: 'series',
|
||||
label: 'Series',
|
||||
type: 'series',
|
||||
entities: this.results.series.map((seriesObj) => {
|
||||
return {
|
||||
name: seriesObj.series,
|
||||
books: seriesObj.audiobooks,
|
||||
type: 'series'
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
if (this.results.tags) {
|
||||
shelves.push({
|
||||
id: 'tags',
|
||||
label: 'Tags',
|
||||
type: 'tags',
|
||||
entities: this.results.tags.map((tagObj) => {
|
||||
return {
|
||||
name: tagObj.tag,
|
||||
books: tagObj.audiobooks,
|
||||
type: 'tags'
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
this.shelves = shelves
|
||||
},
|
||||
resize() {},
|
||||
audiobooksUpdated() {},
|
||||
settingsUpdated(settings) {
|
||||
if (settings.bookshelfCoverSize !== this.bookCoverWidth && settings.bookshelfCoverSize !== undefined) {
|
||||
var index = this.availableSizes.indexOf(settings.bookshelfCoverSize)
|
||||
|
|
@ -154,15 +164,11 @@ export default {
|
|||
}
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener('resize', this.resize)
|
||||
this.$store.commit('audiobooks/addListener', { id: 'bookshelf', meth: this.audiobooksUpdated })
|
||||
this.$store.commit('user/addSettingsListener', { id: 'bookshelf', meth: this.settingsUpdated })
|
||||
|
||||
this.init()
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.resize)
|
||||
this.$store.commit('audiobooks/removeListener', 'bookshelf')
|
||||
this.$store.commit('user/removeSettingsListener', 'bookshelf')
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,11 +2,23 @@
|
|||
<div class="relative">
|
||||
<div ref="shelf" class="w-full max-w-full categorizedBookshelfRow relative overflow-x-scroll overflow-y-hidden z-10" :style="{ paddingLeft: paddingLeft * sizeMultiplier + 'rem' }" @scroll="scrolled">
|
||||
<div class="w-full h-full" :style="{ marginTop: sizeMultiplier + 'rem' }">
|
||||
<div v-if="shelf.books" class="flex items-center -mb-2">
|
||||
<template v-for="entity in shelf.books">
|
||||
<div v-if="shelf.type === 'books'" class="flex items-center -mb-2">
|
||||
<template v-for="entity in shelf.entities">
|
||||
<cards-book-card :key="entity.id" :width="bookCoverWidth" :user-progress="userAudiobooks[entity.id]" :audiobook="entity" @hook:updated="updatedBookCard" :padding-y="24" @edit="editBook" />
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="shelf.type === 'series'" class="flex items-center -mb-2">
|
||||
<template v-for="entity in shelf.entities">
|
||||
<cards-group-card :key="entity.name" :width="bookCoverWidth" :group="entity" @click="$emit('clickSeries', entity)" />
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="shelf.type === 'tags'" class="flex items-center -mb-2">
|
||||
<template v-for="entity in shelf.entities">
|
||||
<nuxt-link :key="entity.name" :to="`/library/${currentLibraryId}/bookshelf?filter=tags.${$encode(entity.name)}`">
|
||||
<cards-group-card is-search :width="bookCoverWidth" :group="entity" />
|
||||
</nuxt-link>
|
||||
</template>
|
||||
</div>
|
||||
<div v-else-if="shelf.series" class="flex items-center -mb-2">
|
||||
<template v-for="entity in shelf.series">
|
||||
<cards-group-card is-search :key="entity.name" :width="bookCoverWidth" :group="entity" @click="$emit('clickSeries', entity)" />
|
||||
|
|
@ -70,7 +82,7 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
editBook(audiobook) {
|
||||
var bookIds = this.shelf.books.map((e) => e.id)
|
||||
var bookIds = this.shelf.entities.map((e) => e.id)
|
||||
this.$store.commit('setBookshelfBookIds', bookIds)
|
||||
this.$store.commit('showEditModal', audiobook)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@
|
|||
</div>
|
||||
<div class="flex-grow hidden md:inline-block" />
|
||||
|
||||
<ui-text-input v-show="showSortFilters" v-model="_keywordFilter" placeholder="Keyword Filter" :padding-y="1.5" clearable class="text-xs w-40 hidden md:block" />
|
||||
<!-- <ui-text-input v-show="showSortFilters" v-model="keywordFilter" @input="keywordFilterInput" placeholder="Keyword Filter" :padding-y="1.5" clearable class="text-xs w-40 hidden md:block" /> -->
|
||||
<controls-filter-select v-show="showSortFilters" v-model="settings.filterBy" class="w-48 h-7.5 ml-4" @change="updateFilter" />
|
||||
<controls-order-select v-show="showSortFilters" v-model="settings.orderBy" :descending.sync="settings.orderDesc" class="w-48 h-7.5 ml-4" @change="updateOrder" />
|
||||
<div v-show="showSortFilters" class="h-7 ml-4 flex border border-white border-opacity-25 rounded-md">
|
||||
|
|
@ -69,7 +69,10 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
settings: {},
|
||||
hasInit: false
|
||||
hasInit: false,
|
||||
totalEntities: 0,
|
||||
keywordFilter: null,
|
||||
keywordTimeout: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -80,8 +83,11 @@ export default {
|
|||
return this.page === ''
|
||||
},
|
||||
numShowing() {
|
||||
return this.totalEntities
|
||||
|
||||
if (this.page === '') {
|
||||
return this.$store.getters['audiobooks/getFiltered']().length
|
||||
// return this.$store.getters['audiobooks/getFiltered']().length
|
||||
return this.totalEntities
|
||||
} else if (this.page === 'search') {
|
||||
var audiobookSearchResults = this.searchResults ? this.searchResults.audiobooks || [] : []
|
||||
return audiobookSearchResults.length
|
||||
|
|
@ -103,14 +109,14 @@ export default {
|
|||
if (this.page === 'collections') return 'Collections'
|
||||
return ''
|
||||
},
|
||||
_keywordFilter: {
|
||||
get() {
|
||||
return this.$store.state.audiobooks.keywordFilter
|
||||
},
|
||||
set(val) {
|
||||
this.$store.commit('audiobooks/setKeywordFilter', val)
|
||||
}
|
||||
},
|
||||
// _keywordFilter: {
|
||||
// get() {
|
||||
// return this.$store.state.audiobooks.keywordFilter
|
||||
// },
|
||||
// set(val) {
|
||||
// this.$store.commit('audiobooks/setKeywordFilter', val)
|
||||
// }
|
||||
// },
|
||||
paramId() {
|
||||
return this.$route.params ? this.$route.params.id || '' : ''
|
||||
},
|
||||
|
|
@ -151,14 +157,28 @@ export default {
|
|||
for (const key in settings) {
|
||||
this.settings[key] = settings[key]
|
||||
}
|
||||
},
|
||||
setBookshelfTotalEntities(totalEntities) {
|
||||
this.totalEntities = totalEntities
|
||||
},
|
||||
keywordFilterInput() {
|
||||
clearTimeout(this.keywordTimeout)
|
||||
this.keywordTimeout = setTimeout(() => {
|
||||
this.keywordUpdated(this.keywordFilter)
|
||||
}, 1000)
|
||||
},
|
||||
keywordUpdated() {
|
||||
this.$eventBus.$emit('bookshelf-keyword-filter', this.keywordFilter)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.init()
|
||||
this.$store.commit('user/addSettingsListener', { id: 'bookshelftoolbar', meth: this.settingsUpdated })
|
||||
this.$eventBus.$on('bookshelf-total-entities', this.setBookshelfTotalEntities)
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.$store.commit('user/removeSettingsListener', 'bookshelftoolbar')
|
||||
this.$eventBus.$off('bookshelf-total-entities', this.setBookshelfTotalEntities)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -2,47 +2,77 @@
|
|||
<div id="bookshelf" class="w-full overflow-y-auto">
|
||||
<template v-for="shelf in totalShelves">
|
||||
<div :key="shelf" class="w-full px-8 bookshelfRow relative" :id="`shelf-${shelf - 1}`" :style="{ height: shelfHeight + 'px' }">
|
||||
<div class="absolute top-0 left-0 bottom-0 p-4 z-10">
|
||||
<!-- <div class="absolute top-0 left-0 bottom-0 p-4 z-10">
|
||||
<p class="text-white text-2xl">{{ shelf }}</p>
|
||||
</div>
|
||||
</div> -->
|
||||
<div class="bookshelfDivider w-full absolute bottom-0 left-0 right-0 z-10" :class="`h-${shelfDividerHeightIndex}`" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-if="!totalShelves && initialized" class="w-full py-16">
|
||||
<p class="text-xl text-center">{{ emptyMessage }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from 'vue'
|
||||
import LazyBookCard from '../cards/LazyBookCard'
|
||||
import bookshelfCardsHelpers from '@/mixins/bookshelfCardsHelpers'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
page: String
|
||||
},
|
||||
mixins: [bookshelfCardsHelpers],
|
||||
data() {
|
||||
return {
|
||||
initialized: false,
|
||||
bookshelfHeight: 0,
|
||||
bookshelfWidth: 0,
|
||||
shelvesPerPage: 0,
|
||||
booksPerShelf: 8,
|
||||
entitiesPerShelf: 8,
|
||||
currentPage: 0,
|
||||
totalBooks: 0,
|
||||
books: [],
|
||||
totalEntities: 0,
|
||||
entities: [],
|
||||
pagesLoaded: {},
|
||||
bookIndexesMounted: [],
|
||||
bookComponentRefs: {},
|
||||
entityIndexesMounted: [],
|
||||
entityComponentRefs: {},
|
||||
bookWidth: 120,
|
||||
pageLoadQueue: [],
|
||||
isFetchingBooks: false,
|
||||
isFetchingEntities: false,
|
||||
scrollTimeout: null,
|
||||
booksPerFetch: 100,
|
||||
booksPerFetch: 250,
|
||||
totalShelves: 0,
|
||||
bookshelfMarginLeft: 0
|
||||
bookshelfMarginLeft: 0,
|
||||
isSelectionMode: false,
|
||||
isSelectAll: false,
|
||||
currentSFQueryString: null,
|
||||
pendingReset: false,
|
||||
keywordFilter: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
sortBy() {
|
||||
// booksFiltered() {
|
||||
// const keywordFilterKeys = ['title', 'subtitle', 'author', 'series', 'narrator']
|
||||
// const keyworkFilter = state.keywordFilter.toLowerCase()
|
||||
// return this.books.filter((ab) => {
|
||||
// if (!ab.book) return false
|
||||
// return !!keywordFilterKeys.find((key) => ab.book[key] && ab.book[key].toLowerCase().includes(keyworkFilter))
|
||||
// })
|
||||
// },
|
||||
emptyMessage() {
|
||||
if (this.page === 'series') return `You have no series`
|
||||
if (this.page === 'collections') return "You haven't made any collections yet"
|
||||
return 'No results'
|
||||
},
|
||||
entityName() {
|
||||
if (this.page === 'series') return 'series'
|
||||
if (this.page === 'collections') return 'collections'
|
||||
return 'books'
|
||||
},
|
||||
orderBy() {
|
||||
return this.$store.getters['user/getUserSetting']('orderBy')
|
||||
},
|
||||
sortDesc() {
|
||||
orderDesc() {
|
||||
return this.$store.getters['user/getUserSetting']('orderDesc')
|
||||
},
|
||||
filterBy() {
|
||||
|
|
@ -51,6 +81,11 @@ export default {
|
|||
currentLibraryId() {
|
||||
return this.$store.state.libraries.currentLibraryId
|
||||
},
|
||||
entityWidth() {
|
||||
if (this.entityName === 'series') return this.bookWidth * 1.6
|
||||
if (this.entityName === 'collections') return this.bookWidth * 2
|
||||
return this.bookWidth
|
||||
},
|
||||
bookHeight() {
|
||||
return this.bookWidth * 1.6
|
||||
},
|
||||
|
|
@ -60,119 +95,133 @@ export default {
|
|||
shelfHeight() {
|
||||
return this.bookHeight + 40
|
||||
},
|
||||
totalBookCardWidth() {
|
||||
totalEntityCardWidth() {
|
||||
// Includes margin
|
||||
return this.bookWidth + 24
|
||||
return this.entityWidth + 24
|
||||
},
|
||||
booksPerPage() {
|
||||
return this.shelvesPerPage * this.booksPerShelf
|
||||
return this.shelvesPerPage * this.entitiesPerShelf
|
||||
},
|
||||
selectedAudiobooks() {
|
||||
return this.$store.state.selectedAudiobooks || []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async fetchBooks(page = 0) {
|
||||
editEntity(entity) {
|
||||
if (this.entityName === 'books') {
|
||||
var bookIds = this.entities.map((e) => e.id)
|
||||
this.$store.commit('setBookshelfBookIds', bookIds)
|
||||
this.$store.commit('showEditModal', entity)
|
||||
}
|
||||
},
|
||||
clearSelectedBooks() {
|
||||
this.updateBookSelectionMode(false)
|
||||
this.isSelectionMode = false
|
||||
this.isSelectAll = false
|
||||
},
|
||||
selectAllBooks() {
|
||||
this.isSelectAll = true
|
||||
for (const key in this.entityComponentRefs) {
|
||||
if (this.entityIndexesMounted.includes(Number(key))) {
|
||||
this.entityComponentRefs[key].selected = true
|
||||
}
|
||||
}
|
||||
},
|
||||
selectEntity(entity) {
|
||||
if (this.entityName === 'books') {
|
||||
this.$store.commit('toggleAudiobookSelected', entity.id)
|
||||
|
||||
var newIsSelectionMode = !!this.selectedAudiobooks.length
|
||||
if (this.isSelectionMode !== newIsSelectionMode) {
|
||||
this.isSelectionMode = newIsSelectionMode
|
||||
this.updateBookSelectionMode(newIsSelectionMode)
|
||||
}
|
||||
}
|
||||
},
|
||||
updateBookSelectionMode(isSelectionMode) {
|
||||
for (const key in this.entityComponentRefs) {
|
||||
if (this.entityIndexesMounted.includes(Number(key))) {
|
||||
this.entityComponentRefs[key].setSelectionMode(isSelectionMode)
|
||||
}
|
||||
}
|
||||
},
|
||||
async fetchEntites(page = 0) {
|
||||
var startIndex = page * this.booksPerFetch
|
||||
|
||||
this.isFetchingBooks = true
|
||||
var payload = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/books/all?sort=${this.sortBy}&desc=${this.sortDesc}&filter=${this.filterBy}&limit=${this.booksPerFetch}&page=${page}`).catch((error) => {
|
||||
this.isFetchingEntities = true
|
||||
|
||||
if (!this.initialized) {
|
||||
this.currentSFQueryString = this.buildSearchParams()
|
||||
}
|
||||
|
||||
var entityPath = this.entityName === 'books' ? `books/all` : this.entityName
|
||||
var sfQueryString = this.currentSFQueryString ? this.currentSFQueryString + '&' : ''
|
||||
var payload = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/${entityPath}?${sfQueryString}limit=${this.booksPerFetch}&page=${page}`).catch((error) => {
|
||||
console.error('failed to fetch books', error)
|
||||
return null
|
||||
})
|
||||
this.isFetchingEntities = false
|
||||
if (this.pendingReset) {
|
||||
this.pendingReset = false
|
||||
this.resetEntities()
|
||||
return
|
||||
}
|
||||
if (payload) {
|
||||
console.log('Received payload with start index', startIndex, 'pages loaded', this.pagesLoaded)
|
||||
console.log('Received payload', payload)
|
||||
if (!this.initialized) {
|
||||
this.initialized = true
|
||||
this.totalBooks = payload.total
|
||||
this.totalShelves = Math.ceil(this.totalBooks / this.booksPerShelf)
|
||||
this.books = new Array(this.totalBooks)
|
||||
this.totalEntities = payload.total
|
||||
this.totalShelves = Math.ceil(this.totalEntities / this.entitiesPerShelf)
|
||||
this.entities = new Array(this.totalEntities)
|
||||
this.$eventBus.$emit('bookshelf-total-entities', this.totalEntities)
|
||||
}
|
||||
|
||||
for (let i = 0; i < payload.results.length; i++) {
|
||||
var bookIndex = i + startIndex
|
||||
this.books[bookIndex] = payload.results[i]
|
||||
var index = i + startIndex
|
||||
this.entities[index] = payload.results[i]
|
||||
|
||||
if (this.bookComponentRefs[bookIndex]) {
|
||||
this.bookComponentRefs[bookIndex].setBook(this.books[bookIndex])
|
||||
if (this.entityComponentRefs[index]) {
|
||||
this.entityComponentRefs[index].setEntity(this.entities[index])
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
loadPage(page) {
|
||||
this.pagesLoaded[page] = true
|
||||
this.fetchBooks(page)
|
||||
},
|
||||
async mountBookCard(index) {
|
||||
var shelf = Math.floor(index / this.booksPerShelf)
|
||||
var shelfEl = document.getElementById(`shelf-${shelf}`)
|
||||
if (!shelfEl) {
|
||||
console.error('invalid shelf', shelf)
|
||||
return
|
||||
}
|
||||
this.bookIndexesMounted.push(index)
|
||||
if (this.bookComponentRefs[index] && !this.bookIndexesMounted.includes(index)) {
|
||||
shelfEl.appendChild(this.bookComponentRefs[index].$el)
|
||||
return
|
||||
}
|
||||
|
||||
var shelfOffsetY = 16
|
||||
var row = index % this.booksPerShelf
|
||||
var shelfOffsetX = row * this.totalBookCardWidth + this.bookshelfMarginLeft
|
||||
|
||||
var ComponentClass = Vue.extend(LazyBookCard)
|
||||
|
||||
var _this = this
|
||||
var instance = new ComponentClass({
|
||||
propsData: {
|
||||
index: index,
|
||||
bookWidth: this.bookWidth
|
||||
},
|
||||
created() {
|
||||
// this.$on('action', (func) => {
|
||||
// if (_this[func]) _this[func]()
|
||||
// })
|
||||
}
|
||||
})
|
||||
this.bookComponentRefs[index] = instance
|
||||
|
||||
instance.$mount()
|
||||
instance.$el.style.transform = `translate3d(${shelfOffsetX}px, ${shelfOffsetY}px, 0px)`
|
||||
shelfEl.appendChild(instance.$el)
|
||||
|
||||
if (this.books[index]) {
|
||||
instance.setBook(this.books[index])
|
||||
}
|
||||
this.fetchEntites(page)
|
||||
},
|
||||
showHideBookPlaceholder(index, show) {
|
||||
var el = document.getElementById(`book-${index}-placeholder`)
|
||||
if (el) el.style.display = show ? 'flex' : 'none'
|
||||
},
|
||||
unmountBookCard(index) {
|
||||
if (this.bookComponentRefs[index]) {
|
||||
this.bookComponentRefs[index].detach()
|
||||
}
|
||||
},
|
||||
mountBooks(fromIndex, toIndex) {
|
||||
mountEntites(fromIndex, toIndex) {
|
||||
for (let i = fromIndex; i < toIndex; i++) {
|
||||
this.mountBookCard(i)
|
||||
if (!this.entityIndexesMounted.includes(i)) {
|
||||
this.cardsHelpers.mountEntityCard(i)
|
||||
}
|
||||
}
|
||||
},
|
||||
handleScroll(scrollTop) {
|
||||
var firstShelfIndex = Math.floor(scrollTop / this.shelfHeight)
|
||||
var lastShelfIndex = Math.ceil((scrollTop + this.bookshelfHeight) / this.shelfHeight)
|
||||
lastShelfIndex = Math.min(this.totalShelves - 1, lastShelfIndex)
|
||||
|
||||
var topShelfPage = Math.floor(firstShelfIndex / this.shelvesPerPage)
|
||||
var bottomShelfPage = Math.floor(lastShelfIndex / this.shelvesPerPage)
|
||||
if (!this.pagesLoaded[topShelfPage]) {
|
||||
this.loadPage(topShelfPage)
|
||||
var firstBookIndex = firstShelfIndex * this.entitiesPerShelf
|
||||
var lastBookIndex = lastShelfIndex * this.entitiesPerShelf + this.entitiesPerShelf
|
||||
lastBookIndex = Math.min(this.totalEntities, lastBookIndex)
|
||||
|
||||
var firstBookPage = Math.floor(firstBookIndex / this.booksPerFetch)
|
||||
var lastBookPage = Math.floor(lastBookIndex / this.booksPerFetch)
|
||||
if (!this.pagesLoaded[firstBookPage]) {
|
||||
console.log('Must load next batch', firstBookPage, 'book index', firstBookIndex)
|
||||
this.loadPage(firstBookPage)
|
||||
}
|
||||
if (!this.pagesLoaded[bottomShelfPage]) {
|
||||
this.loadPage(bottomShelfPage)
|
||||
if (!this.pagesLoaded[lastBookPage]) {
|
||||
console.log('Must load last next batch', lastBookPage, 'book index', lastBookIndex)
|
||||
this.loadPage(lastBookPage)
|
||||
}
|
||||
console.log('Shelves in view', firstShelfIndex, 'to', lastShelfIndex)
|
||||
|
||||
var firstBookIndex = firstShelfIndex * this.booksPerShelf
|
||||
var lastBookIndex = lastShelfIndex * this.booksPerShelf + this.booksPerShelf
|
||||
|
||||
this.bookIndexesMounted = this.bookIndexesMounted.filter((_index) => {
|
||||
this.entityIndexesMounted = this.entityIndexesMounted.filter((_index) => {
|
||||
if (_index < firstBookIndex || _index >= lastBookIndex) {
|
||||
var el = document.getElementById(`book-card-${_index}`)
|
||||
if (el) el.remove()
|
||||
|
|
@ -180,7 +229,68 @@ export default {
|
|||
}
|
||||
return true
|
||||
})
|
||||
this.mountBooks(firstBookIndex, lastBookIndex)
|
||||
this.mountEntites(firstBookIndex, lastBookIndex)
|
||||
},
|
||||
async resetEntities() {
|
||||
if (this.isFetchingEntities) {
|
||||
console.warn('RESET BOOKS BUT ALREADY FETCHING, WAIT FOR FETCH')
|
||||
this.pendingReset = true
|
||||
return
|
||||
}
|
||||
|
||||
this.destroyEntityComponents()
|
||||
this.entityIndexesMounted = []
|
||||
this.entityComponentRefs = {}
|
||||
this.pagesLoaded = {}
|
||||
this.entities = []
|
||||
this.totalShelves = 0
|
||||
this.totalEntities = 0
|
||||
this.currentPage = 0
|
||||
this.isSelectionMode = false
|
||||
this.isSelectAll = false
|
||||
this.initialized = false
|
||||
|
||||
this.pagesLoaded[0] = true
|
||||
await this.fetchEntites(0)
|
||||
var lastBookIndex = Math.min(this.totalEntities, this.shelvesPerPage * this.entitiesPerShelf)
|
||||
this.mountEntites(0, lastBookIndex)
|
||||
},
|
||||
buildSearchParams() {
|
||||
if (this.page === 'search' || this.page === 'series' || this.page === 'collections') {
|
||||
return ''
|
||||
}
|
||||
|
||||
let searchParams = new URLSearchParams()
|
||||
if (this.filterBy && this.filterBy !== 'all') {
|
||||
searchParams.set('filter', this.filterBy)
|
||||
}
|
||||
if (this.orderBy) {
|
||||
searchParams.set('sort', this.orderBy)
|
||||
searchParams.set('desc', this.orderDesc ? 1 : 0)
|
||||
}
|
||||
return searchParams.toString()
|
||||
},
|
||||
checkUpdateSearchParams() {
|
||||
var newSearchParams = this.buildSearchParams()
|
||||
var currentQueryString = window.location.search
|
||||
|
||||
if (newSearchParams === '') {
|
||||
return false
|
||||
}
|
||||
|
||||
if (newSearchParams !== this.currentSFQueryString || newSearchParams !== currentQueryString) {
|
||||
let newurl = window.location.protocol + '//' + window.location.host + window.location.pathname + '?' + newSearchParams
|
||||
window.history.replaceState({ path: newurl }, '', newurl)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
settingsUpdated(settings) {
|
||||
var wasUpdated = this.checkUpdateSearchParams()
|
||||
if (wasUpdated) {
|
||||
this.resetEntities()
|
||||
}
|
||||
},
|
||||
scroll(e) {
|
||||
if (!e || !e.target) return
|
||||
|
|
@ -191,32 +301,57 @@ export default {
|
|||
// }, 250)
|
||||
},
|
||||
async init(bookshelf) {
|
||||
this.checkUpdateSearchParams()
|
||||
|
||||
var { clientHeight, clientWidth } = bookshelf
|
||||
this.bookshelfHeight = clientHeight
|
||||
this.bookshelfWidth = clientWidth
|
||||
this.booksPerShelf = Math.floor((this.bookshelfWidth - 64) / this.totalBookCardWidth)
|
||||
this.entitiesPerShelf = Math.floor((this.bookshelfWidth - 64) / this.totalEntityCardWidth)
|
||||
this.shelvesPerPage = Math.ceil(this.bookshelfHeight / this.shelfHeight) + 2
|
||||
this.bookshelfMarginLeft = (this.bookshelfWidth - this.booksPerShelf * this.totalBookCardWidth) / 2
|
||||
console.log('Shelves per page', this.shelvesPerPage, 'books per page =', this.booksPerShelf * this.shelvesPerPage)
|
||||
this.bookshelfMarginLeft = (this.bookshelfWidth - this.entitiesPerShelf * this.totalEntityCardWidth) / 2
|
||||
|
||||
this.pagesLoaded[0] = true
|
||||
await this.fetchBooks(0)
|
||||
var lastBookIndex = this.shelvesPerPage * this.booksPerShelf
|
||||
this.mountBooks(0, lastBookIndex)
|
||||
await this.fetchEntites(0)
|
||||
var lastBookIndex = Math.min(this.totalEntities, this.shelvesPerPage * this.entitiesPerShelf)
|
||||
this.mountEntites(0, lastBookIndex)
|
||||
},
|
||||
initListeners() {
|
||||
var bookshelf = document.getElementById('bookshelf')
|
||||
if (bookshelf) {
|
||||
this.init(bookshelf)
|
||||
bookshelf.addEventListener('scroll', this.scroll)
|
||||
}
|
||||
this.$eventBus.$on('bookshelf-clear-selection', this.clearSelectedBooks)
|
||||
this.$eventBus.$on('bookshelf-select-all', this.selectAllBooks)
|
||||
this.$eventBus.$on('bookshelf-keyword-filter', this.updateKeywordFilter)
|
||||
|
||||
this.$store.commit('user/addSettingsListener', { id: 'lazy-bookshelf', meth: this.settingsUpdated })
|
||||
},
|
||||
removeListeners() {
|
||||
var bookshelf = document.getElementById('bookshelf')
|
||||
if (bookshelf) {
|
||||
bookshelf.removeEventListener('scroll', this.scroll)
|
||||
}
|
||||
this.$eventBus.$off('bookshelf-clear-selection', this.clearSelectedBooks)
|
||||
this.$eventBus.$off('bookshelf-select-all', this.selectAllBooks)
|
||||
this.$eventBus.$off('bookshelf-keyword-filter', this.updateKeywordFilter)
|
||||
|
||||
this.$store.commit('user/removeSettingsListener', 'lazy-bookshelf')
|
||||
},
|
||||
destroyEntityComponents() {
|
||||
for (const key in this.entityComponentRefs) {
|
||||
if (this.entityComponentRefs[key] && this.entityComponentRefs[key].destroy) {
|
||||
this.entityComponentRefs[key].destroy()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
var bookshelf = document.getElementById('bookshelf')
|
||||
if (bookshelf) {
|
||||
this.init(bookshelf)
|
||||
bookshelf.addEventListener('scroll', this.scroll)
|
||||
}
|
||||
this.initListeners()
|
||||
},
|
||||
beforeDestroy() {
|
||||
var bookshelf = document.getElementById('bookshelf')
|
||||
if (bookshelf) {
|
||||
bookshelf.removeEventListener('scroll', this.scroll)
|
||||
}
|
||||
this.destroyEntityComponents()
|
||||
this.removeListeners()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue