Lazy bookshelf, api routes for categories and filter data

This commit is contained in:
advplyr 2021-11-30 20:02:40 -06:00
parent 4587916c8e
commit 5c92aef048
26 changed files with 1354 additions and 332 deletions

View file

@ -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>