Lazy bookshelf finalized

This commit is contained in:
advplyr 2021-12-01 19:07:03 -06:00
parent 5c92aef048
commit 1ef9a689bc
53 changed files with 914 additions and 795 deletions

View file

@ -9,9 +9,22 @@
</div>
</template>
<div v-if="!totalShelves && initialized" class="w-full py-16">
<div v-if="initialized && !totalShelves && !hasFilter && isRootUser && entityName === 'books'" class="w-full flex flex-col items-center justify-center py-12">
<p class="text-center text-2xl font-book mb-4 py-4">Audiobookshelf is empty!</p>
<div class="flex">
<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 Audiobooks</ui-btn>
</div>
</div>
<div v-else-if="!totalShelves && initialized" class="w-full py-16">
<p class="text-xl text-center">{{ emptyMessage }}</p>
</div>
<widgets-cover-size-widget class="fixed bottom-4 right-4 z-30" />
<!-- Experimental Bookshelf Texture -->
<div v-show="showExperimentalFeatures" class="fixed bottom-4 right-28 z-40">
<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>
</template>
@ -20,7 +33,8 @@ import bookshelfCardsHelpers from '@/mixins/bookshelfCardsHelpers'
export default {
props: {
page: String
page: String,
seriesId: String
},
mixins: [bookshelfCardsHelpers],
data() {
@ -36,7 +50,7 @@ export default {
pagesLoaded: {},
entityIndexesMounted: [],
entityComponentRefs: {},
bookWidth: 120,
currentBookWidth: 0,
pageLoadQueue: [],
isFetchingEntities: false,
scrollTimeout: null,
@ -47,27 +61,34 @@ export default {
isSelectAll: false,
currentSFQueryString: null,
pendingReset: false,
keywordFilter: null
keywordFilter: null,
currScrollTop: 0
}
},
watch: {
'$route.query.filter'() {
if (this.$route.query.filter && this.$route.query.filter !== this.filterBy) {
this.$store.dispatch('user/updateUserSettings', { filterBy: this.$route.query.filter })
} else if (!this.$route.query.filter && this.filterBy) {
this.$store.dispatch('user/updateUserSettings', { filterBy: 'all' })
}
}
},
computed: {
// 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))
// })
// },
isRootUser() {
return this.$store.getters['user/getIsRoot']
},
showExperimentalFeatures() {
return this.$store.state.showExperimentalFeatures
},
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'
if (!this.page) return 'books'
return this.page
},
orderBy() {
return this.$store.getters['user/getUserSetting']('orderBy')
@ -78,9 +99,15 @@ export default {
filterBy() {
return this.$store.getters['user/getUserSetting']('filterBy')
},
hasFilter() {
return this.filterBy && this.filterBy !== 'all'
},
currentLibraryId() {
return this.$store.state.libraries.currentLibraryId
},
bookWidth() {
return this.$store.getters['user/getUserSetting']('bookshelfCoverSize')
},
entityWidth() {
if (this.entityName === 'series') return this.bookWidth * 1.6
if (this.entityName === 'collections') return this.bookWidth * 2
@ -107,20 +134,30 @@ export default {
}
},
methods: {
showBookshelfTextureModal() {
this.$store.commit('globals/setShowBookshelfTextureModal', true)
},
editEntity(entity) {
if (this.entityName === 'books') {
if (this.entityName === 'books' || this.entityName === 'series-books') {
var bookIds = this.entities.map((e) => e.id)
this.$store.commit('setBookshelfBookIds', bookIds)
this.$store.commit('showEditModal', entity)
} else if (this.entityName === 'collections') {
this.$store.commit('globals/setEditCollection', entity)
}
},
clearSelectedBooks() {
clearSelectedEntities() {
this.updateBookSelectionMode(false)
this.isSelectionMode = false
this.isSelectAll = false
},
selectAllBooks() {
selectAllEntities() {
this.isSelectAll = true
if (this.entityName === 'books' || this.entityName === 'series-books') {
var allAvailableEntityIds = this.entities.map((ent) => ent.id).filter((ent) => !!ent)
this.$store.commit('setSelectedAudiobooks', allAvailableEntityIds)
}
for (const key in this.entityComponentRefs) {
if (this.entityIndexesMounted.includes(Number(key))) {
this.entityComponentRefs[key].selected = true
@ -128,7 +165,7 @@ export default {
}
},
selectEntity(entity) {
if (this.entityName === 'books') {
if (this.entityName === 'books' || this.entityName === 'series-books') {
this.$store.commit('toggleAudiobookSelected', entity.id)
var newIsSelectionMode = !!this.selectedAudiobooks.length
@ -155,8 +192,10 @@ export default {
}
var entityPath = this.entityName === 'books' ? `books/all` : this.entityName
if (this.entityName === 'series-books') entityPath = `series/${this.seriesId}`
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) => {
var fullQueryString = this.entityName === 'series-books' ? '' : `?${sfQueryString}limit=${this.booksPerFetch}&page=${page}`
var payload = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/${entityPath}${fullQueryString}`).catch((error) => {
console.error('failed to fetch books', error)
return null
})
@ -167,7 +206,7 @@ export default {
return
}
if (payload) {
console.log('Received payload', payload)
// console.log('Received payload', payload)
if (!this.initialized) {
this.initialized = true
this.totalEntities = payload.total
@ -202,6 +241,7 @@ export default {
}
},
handleScroll(scrollTop) {
this.currScrollTop = scrollTop
var firstShelfIndex = Math.floor(scrollTop / this.shelfHeight)
var lastShelfIndex = Math.ceil((scrollTop + this.bookshelfHeight) / this.shelfHeight)
lastShelfIndex = Math.min(this.totalShelves - 1, lastShelfIndex)
@ -233,11 +273,9 @@ export default {
},
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 = {}
@ -250,13 +288,25 @@ export default {
this.isSelectAll = false
this.initialized = false
this.initSizeData()
this.pagesLoaded[0] = true
await this.fetchEntites(0)
var lastBookIndex = Math.min(this.totalEntities, this.shelvesPerPage * this.entitiesPerShelf)
this.mountEntites(0, lastBookIndex)
},
remountEntities() {
for (const key in this.entityComponentRefs) {
if (this.entityComponentRefs[key]) {
this.entityComponentRefs[key].destroy()
}
}
this.entityComponentRefs = {}
this.entityIndexesMounted.forEach((i) => {
this.cardsHelpers.mountEntityCard(i)
})
},
buildSearchParams() {
if (this.page === 'search' || this.page === 'series' || this.page === 'collections') {
if (this.page === 'search' || this.page === 'series' || this.page === 'collections' || this.page === 'series-books') {
return ''
}
@ -273,11 +323,11 @@ export default {
checkUpdateSearchParams() {
var newSearchParams = this.buildSearchParams()
var currentQueryString = window.location.search
if (currentQueryString && currentQueryString.startsWith('?')) currentQueryString = currentQueryString.slice(1)
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)
@ -290,6 +340,20 @@ export default {
var wasUpdated = this.checkUpdateSearchParams()
if (wasUpdated) {
this.resetEntities()
} else if (settings.bookshelfCoverSize !== this.currentBookWidth) {
this.initSizeData()
var lastBookIndex = Math.min(this.totalEntities, this.shelvesPerPage * this.entitiesPerShelf)
this.entityIndexesMounted = []
for (let i = 0; i < lastBookIndex; i++) {
this.entityIndexesMounted.push(i)
}
var bookshelfEl = document.getElementById('bookshelf')
if (bookshelfEl) {
bookshelfEl.scrollTop = 0
}
this.$nextTick(this.remountEntities)
}
},
scroll(e) {
@ -300,8 +364,51 @@ export default {
this.handleScroll(scrollTop)
// }, 250)
},
async init(bookshelf) {
this.checkUpdateSearchParams()
audiobookAdded(audiobook) {
console.log('Audiobook added', audiobook)
// TODO: Check if audiobook would be on this shelf
this.resetEntities()
},
audiobookUpdated(audiobook) {
console.log('Audiobook updated', audiobook)
if (this.entityName === 'books' || this.entityName === 'series-books') {
var indexOf = this.entities.findIndex((ent) => ent && ent.id === audiobook.id)
if (indexOf >= 0) {
this.entities[indexOf] = audiobook
if (this.entityComponentRefs[indexOf]) {
this.entityComponentRefs[indexOf].setEntity(audiobook)
}
}
}
},
audiobookRemoved(audiobook) {
if (this.entityName === 'books' || this.entityName === 'series-books') {
var indexOf = this.entities.findIndex((ent) => ent && ent.id === audiobook.id)
if (indexOf >= 0) {
this.entities = this.entities.filter((ent) => ent.id !== audiobook.id)
this.totalEntities = this.entities.length
this.$eventBus.$emit('bookshelf-total-entities', this.totalEntities)
this.remountEntities()
}
}
},
audiobooksAdded(audiobooks) {
console.log('audiobooks added', audiobooks)
// TODO: Check if audiobook would be on this shelf
this.resetEntities()
},
audiobooksUpdated(audiobooks) {
audiobooks.forEach((ab) => {
this.audiobookUpdated(ab)
})
},
initSizeData(_bookshelf) {
var bookshelf = _bookshelf || document.getElementById('bookshelf')
if (!bookshelf) {
console.error('Failed to init size data')
return
}
var entitiesPerShelfBefore = this.entitiesPerShelf
var { clientHeight, clientWidth } = bookshelf
this.bookshelfHeight = clientHeight
@ -310,6 +417,16 @@ export default {
this.shelvesPerPage = Math.ceil(this.bookshelfHeight / this.shelfHeight) + 2
this.bookshelfMarginLeft = (this.bookshelfWidth - this.entitiesPerShelf * this.totalEntityCardWidth) / 2
this.currentBookWidth = this.bookWidth
if (this.totalEntities) {
this.totalShelves = Math.ceil(this.totalEntities / this.entitiesPerShelf)
}
return entitiesPerShelfBefore < this.entitiesPerShelf // Books per shelf has changed
},
async init(bookshelf) {
this.checkUpdateSearchParams()
this.initSizeData(bookshelf)
this.pagesLoaded[0] = true
await this.fetchEntites(0)
var lastBookIndex = Math.min(this.totalEntities, this.shelvesPerPage * this.entitiesPerShelf)
@ -321,22 +438,42 @@ export default {
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-clear-selection', this.clearSelectedEntities)
this.$eventBus.$on('bookshelf-select-all', this.selectAllEntities)
this.$eventBus.$on('bookshelf-keyword-filter', this.updateKeywordFilter)
this.$store.commit('user/addSettingsListener', { id: 'lazy-bookshelf', meth: this.settingsUpdated })
if (this.$root.socket) {
this.$root.socket.on('audiobook_updated', this.audiobookUpdated)
this.$root.socket.on('audiobook_added', this.audiobookAdded)
this.$root.socket.on('audiobook_removed', this.audiobookRemoved)
this.$root.socket.on('audiobooks_updated', this.audiobooksUpdated)
this.$root.socket.on('audiobooks_added', this.audiobooksAdded)
} else {
console.error('Bookshelf - Socket not initialized')
}
},
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-clear-selection', this.clearSelectedEntities)
this.$eventBus.$off('bookshelf-select-all', this.selectAllEntities)
this.$eventBus.$off('bookshelf-keyword-filter', this.updateKeywordFilter)
this.$store.commit('user/removeSettingsListener', 'lazy-bookshelf')
if (this.$root.socket) {
this.$root.socket.off('audiobook_updated', this.audiobookUpdated)
this.$root.socket.off('audiobook_added', this.audiobookAdded)
this.$root.socket.off('audiobook_removed', this.audiobookRemoved)
this.$root.socket.off('audiobooks_updated', this.audiobooksUpdated)
this.$root.socket.off('audiobooks_added', this.audiobooksAdded)
} else {
console.error('Bookshelf - Socket not initialized')
}
},
destroyEntityComponents() {
for (const key in this.entityComponentRefs) {
@ -344,6 +481,9 @@ export default {
this.entityComponentRefs[key].destroy()
}
}
},
scan() {
this.$root.socket.emit('scan', this.currentLibraryId)
}
},
mounted() {