mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-06-20 01:39:36 +02:00
Lazy bookshelf finalized
This commit is contained in:
parent
5c92aef048
commit
1ef9a689bc
@ -39,19 +39,18 @@
|
|||||||
|
|
||||||
<div v-show="numAudiobooksSelected" class="absolute top-0 left-0 w-full h-full px-4 bg-primary flex items-center">
|
<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>
|
<h1 class="text-2xl px-4">{{ numAudiobooksSelected }} Selected</h1>
|
||||||
<ui-btn v-show="!isHome" small class="text-sm mx-2" @click="toggleSelectAll"
|
<!-- <ui-btn v-show="!isHome" small class="text-sm mx-2" @click="toggleSelectAll"
|
||||||
>{{ isAllSelected ? 'Select None' : 'Select All' }}<span class="pl-2">({{ totalBooks }})</span></ui-btn
|
>{{ isAllSelected ? 'Select None' : 'Select All' }}<span class="pl-2">({{ entitiesLoaded }})</span></ui-btn
|
||||||
>
|
> -->
|
||||||
|
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
|
|
||||||
<ui-tooltip :text="`Mark as ${selectedIsRead ? 'Not Read' : 'Read'}`" direction="bottom">
|
<ui-tooltip :text="`Mark as ${selectedIsRead ? 'Not Read' : 'Read'}`" direction="bottom">
|
||||||
<ui-read-icon-btn :disabled="processingBatch" :is-read="selectedIsRead" @click="toggleBatchRead" class="mx-1.5" />
|
<ui-read-icon-btn :disabled="processingBatch" :is-read="selectedIsRead" @click="toggleBatchRead" class="mx-1.5" />
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
<ui-tooltip text="Add to Collection" direction="bottom">
|
<ui-tooltip v-if="userCanUpdate" text="Add to Collection" direction="bottom">
|
||||||
<ui-icon-btn :disabled="processingBatch" icon="collections_bookmark" @click="batchAddToCollectionClick" class="mx-1.5" />
|
<ui-icon-btn :disabled="processingBatch" icon="collections_bookmark" @click="batchAddToCollectionClick" class="mx-1.5" />
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
<template v-if="userCanUpdate">
|
<template v-if="userCanUpdate && numAudiobooksSelected < 50">
|
||||||
<ui-icon-btn v-show="!processingBatchDelete" icon="edit" bg-color="warning" class="mx-1.5" @click="batchEditClick" />
|
<ui-icon-btn v-show="!processingBatchDelete" icon="edit" bg-color="warning" class="mx-1.5" @click="batchEditClick" />
|
||||||
</template>
|
</template>
|
||||||
<ui-icon-btn v-show="userCanDelete" :disabled="processingBatchDelete" icon="delete" bg-color="error" class="mx-1.5" @click="batchDeleteClick" />
|
<ui-icon-btn v-show="userCanDelete" :disabled="processingBatchDelete" icon="delete" bg-color="error" class="mx-1.5" @click="batchDeleteClick" />
|
||||||
@ -66,7 +65,7 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
processingBatchDelete: false,
|
processingBatchDelete: false,
|
||||||
totalBooks: 0,
|
totalEntities: 0,
|
||||||
isAllSelected: false
|
isAllSelected: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -98,16 +97,9 @@ export default {
|
|||||||
selectedAudiobooks() {
|
selectedAudiobooks() {
|
||||||
return this.$store.state.selectedAudiobooks
|
return this.$store.state.selectedAudiobooks
|
||||||
},
|
},
|
||||||
// isAllSelected() {
|
|
||||||
// return this.audiobooksShowing.length === this.selectedAudiobooks.length
|
|
||||||
// },
|
|
||||||
userAudiobooks() {
|
userAudiobooks() {
|
||||||
return this.$store.state.user.user.audiobooks || {}
|
return this.$store.state.user.user.audiobooks || {}
|
||||||
},
|
},
|
||||||
audiobooksShowing() {
|
|
||||||
// return this.$store.getters['audiobooks/getFiltered']()
|
|
||||||
return this.$store.getters['audiobooks/getEntitiesShowing']()
|
|
||||||
},
|
|
||||||
selectedSeries() {
|
selectedSeries() {
|
||||||
return this.$store.state.audiobooks.selectedSeries
|
return this.$store.state.audiobooks.selectedSeries
|
||||||
},
|
},
|
||||||
@ -150,16 +142,14 @@ export default {
|
|||||||
this.$eventBus.$emit('bookshelf-clear-selection')
|
this.$eventBus.$emit('bookshelf-clear-selection')
|
||||||
this.isAllSelected = false
|
this.isAllSelected = false
|
||||||
},
|
},
|
||||||
toggleSelectAll() {
|
// toggleSelectAll() {
|
||||||
if (this.isAllSelected) {
|
// if (this.isAllSelected) {
|
||||||
this.cancelSelectionMode()
|
// this.cancelSelectionMode()
|
||||||
} else {
|
// } else {
|
||||||
this.$eventBus.$emit('bookshelf-select-all')
|
// this.$eventBus.$emit('bookshelf-select-all')
|
||||||
this.isAllSelected = true
|
// this.isAllSelected = true
|
||||||
// var audiobookIds = this.audiobooksShowing.map((a) => a.id)
|
// }
|
||||||
// this.$store.commit('setSelectedAudiobooks', audiobookIds)
|
// },
|
||||||
}
|
|
||||||
},
|
|
||||||
toggleBatchRead() {
|
toggleBatchRead() {
|
||||||
this.$store.commit('setProcessingBatch', true)
|
this.$store.commit('setProcessingBatch', true)
|
||||||
var newIsRead = !this.selectedIsRead
|
var newIsRead = !this.selectedIsRead
|
||||||
@ -175,6 +165,7 @@ export default {
|
|||||||
this.$toast.success('Batch update success!')
|
this.$toast.success('Batch update success!')
|
||||||
this.$store.commit('setProcessingBatch', false)
|
this.$store.commit('setProcessingBatch', false)
|
||||||
this.$store.commit('setSelectedAudiobooks', [])
|
this.$store.commit('setSelectedAudiobooks', [])
|
||||||
|
this.$eventBus.$emit('bookshelf-clear-selection')
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
this.$toast.error('Batch update failed')
|
this.$toast.error('Batch update failed')
|
||||||
@ -197,6 +188,7 @@ export default {
|
|||||||
this.processingBatchDelete = false
|
this.processingBatchDelete = false
|
||||||
this.$store.commit('setProcessingBatch', false)
|
this.$store.commit('setProcessingBatch', false)
|
||||||
this.$store.commit('setSelectedAudiobooks', [])
|
this.$store.commit('setSelectedAudiobooks', [])
|
||||||
|
this.$eventBus.$emit('bookshelf-clear-selection')
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
this.$toast.error('Batch delete failed')
|
this.$toast.error('Batch delete failed')
|
||||||
@ -212,15 +204,15 @@ export default {
|
|||||||
batchAddToCollectionClick() {
|
batchAddToCollectionClick() {
|
||||||
this.$store.commit('globals/setShowBatchUserCollectionsModal', true)
|
this.$store.commit('globals/setShowBatchUserCollectionsModal', true)
|
||||||
},
|
},
|
||||||
setBookshelfTotalBooks(totalBooks) {
|
setBookshelfTotalEntities(totalEntities) {
|
||||||
this.totalBooks = totalBooks
|
this.totalEntities = totalEntities
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.$eventBus.$on('bookshelf-total-books', this.setBookshelfTotalBooks)
|
this.$eventBus.$on('bookshelf-total-entities', this.setBookshelfTotalEntities)
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
this.$eventBus.$off('bookshelf-total-books', this.setBookshelfTotalBooks)
|
this.$eventBus.$off('bookshelf-total-entities', this.setBookshelfTotalEntities)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -72,12 +72,6 @@ export default {
|
|||||||
audiobookId() {
|
audiobookId() {
|
||||||
return this.book.id
|
return this.book.id
|
||||||
},
|
},
|
||||||
isSelectionMode() {
|
|
||||||
return !!this.selectedAudiobooks.length
|
|
||||||
},
|
|
||||||
selectedAudiobooks() {
|
|
||||||
return this.$store.state.selectedAudiobooks
|
|
||||||
},
|
|
||||||
selected: {
|
selected: {
|
||||||
get() {
|
get() {
|
||||||
return this.$store.getters['getIsAudiobookSelected'](this.audiobookId)
|
return this.$store.getters['getIsAudiobookSelected'](this.audiobookId)
|
||||||
@ -114,8 +108,8 @@ export default {
|
|||||||
isMissing() {
|
isMissing() {
|
||||||
return this.book.isMissing
|
return this.book.isMissing
|
||||||
},
|
},
|
||||||
isIncomplete() {
|
isInvalid() {
|
||||||
return this.book.isIncomplete
|
return this.book.isInvalid
|
||||||
},
|
},
|
||||||
numEbooks() {
|
numEbooks() {
|
||||||
return this.book.numEbooks
|
return this.book.numEbooks
|
||||||
@ -130,7 +124,7 @@ export default {
|
|||||||
return this.showExperimentalFeatures && this.numEbooks
|
return this.showExperimentalFeatures && this.numEbooks
|
||||||
},
|
},
|
||||||
showPlayButton() {
|
showPlayButton() {
|
||||||
return !this.isMissing && !this.isIncomplete && this.numTracks && !this.isStreaming
|
return !this.isMissing && !this.isInvalid && this.numTracks && !this.isStreaming
|
||||||
},
|
},
|
||||||
userIsRead() {
|
userIsRead() {
|
||||||
return this.userAudiobook ? !!this.userAudiobook.isRead : false
|
return this.userAudiobook ? !!this.userAudiobook.isRead : false
|
||||||
|
@ -101,7 +101,6 @@ export default {
|
|||||||
},
|
},
|
||||||
searchResults() {
|
searchResults() {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
// this.$store.commit('audiobooks/setSearchResults', this.searchResults)
|
|
||||||
this.setBookshelfEntities()
|
this.setBookshelfEntities()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
@ -215,21 +214,7 @@ export default {
|
|||||||
return shelves
|
return shelves
|
||||||
},
|
},
|
||||||
entities() {
|
entities() {
|
||||||
if (this.page === '') {
|
return []
|
||||||
return this.$store.getters['audiobooks/getFilteredAndSorted']()
|
|
||||||
} else if (this.page === 'search') {
|
|
||||||
var audiobookSearchResults = this.searchResults ? this.searchResults.audiobooks || [] : []
|
|
||||||
return audiobookSearchResults.map((absr) => absr.audiobook)
|
|
||||||
} else if (this.page === 'collections') {
|
|
||||||
return this.$store.state.user.collections || []
|
|
||||||
} else {
|
|
||||||
var seriesGroups = this.$store.getters['audiobooks/getSeriesGroups']()
|
|
||||||
if (this.selectedSeries) {
|
|
||||||
var group = seriesGroups.find((group) => group.name === this.selectedSeries)
|
|
||||||
return group.books
|
|
||||||
}
|
|
||||||
return seriesGroups
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@ -308,10 +293,10 @@ export default {
|
|||||||
var sizeIndex = this.availableSizes.findIndex((s) => s === bookshelfCoverSize)
|
var sizeIndex = this.availableSizes.findIndex((s) => s === bookshelfCoverSize)
|
||||||
if (!isNaN(sizeIndex)) this.selectedSizeIndex = sizeIndex
|
if (!isNaN(sizeIndex)) this.selectedSizeIndex = sizeIndex
|
||||||
|
|
||||||
var isLoading = await this.$store.dispatch('audiobooks/load')
|
// var isLoading = await this.$store.dispatch('audiobooks/load')
|
||||||
if (!isLoading) {
|
// if (!isLoading) {
|
||||||
this.setBookshelfEntities()
|
// this.setBookshelfEntities()
|
||||||
}
|
// }
|
||||||
},
|
},
|
||||||
resize() {
|
resize() {
|
||||||
this.$nextTick(this.setBookshelfEntities)
|
this.$nextTick(this.setBookshelfEntities)
|
||||||
|
@ -1,22 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="bookshelf" ref="wrapper" class="w-full h-full overflow-y-scroll relative">
|
<div id="bookshelf" ref="wrapper" class="w-full h-full overflow-y-scroll relative">
|
||||||
<!-- Cover size widget -->
|
<!-- Cover size widget -->
|
||||||
<div class="fixed bottom-4 right-4 z-40">
|
<widgets-cover-size-widget class="fixed bottom-4 right-4 z-30" />
|
||||||
<div class="rounded-full py-1 bg-primary px-2 border border-black-100 text-center flex items-center box-shadow-md" @mousedown.prevent @mouseup.prevent>
|
|
||||||
<span class="material-icons" :class="selectedSizeIndex === 0 ? 'text-gray-400' : 'hover:text-yellow-300 cursor-pointer'" style="font-size: 0.9rem" @mousedown.prevent @click="decreaseSize">remove</span>
|
|
||||||
<p class="px-2 font-mono">{{ bookCoverWidth }}</p>
|
|
||||||
<span class="material-icons" :class="selectedSizeIndex === availableSizes.length - 1 ? 'text-gray-400' : 'hover:text-yellow-300 cursor-pointer'" style="font-size: 0.9rem" @mousedown.prevent @click="increaseSize">add</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- Experimental Bookshelf Texture -->
|
<!-- Experimental Bookshelf Texture -->
|
||||||
<div v-show="showExperimentalFeatures" class="fixed bottom-4 right-28 z-40">
|
<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 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>
|
||||||
|
|
||||||
<div v-if="loaded && !shelves.length" class="w-full flex flex-col items-center justify-center py-12">
|
<div v-if="loaded && !shelves.length && isRootUser" 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>
|
<p class="text-center text-2xl font-book mb-4 py-4">Audiobookshelf is empty!</p>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<ui-btn to="/config" color="primary" class="w-52 mr-2" @click="scan">Configure Scanner</ui-btn>
|
<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>
|
<ui-btn color="success" class="w-52" @click="scan">Scan Audiobooks</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -40,8 +34,6 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
loaded: false,
|
loaded: false,
|
||||||
availableSizes: [60, 80, 100, 120, 140, 160, 180, 200, 220],
|
|
||||||
selectedSizeIndex: 3,
|
|
||||||
keywordFilterTimeout: null,
|
keywordFilterTimeout: null,
|
||||||
scannerParseSubtitle: false,
|
scannerParseSubtitle: false,
|
||||||
wrapperClientWidth: 0,
|
wrapperClientWidth: 0,
|
||||||
@ -49,6 +41,9 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
isRootUser() {
|
||||||
|
return this.$store.getters['user/getIsRoot']
|
||||||
|
},
|
||||||
showExperimentalFeatures() {
|
showExperimentalFeatures() {
|
||||||
return this.$store.state.showExperimentalFeatures
|
return this.$store.state.showExperimentalFeatures
|
||||||
},
|
},
|
||||||
@ -56,7 +51,7 @@ export default {
|
|||||||
return this.$store.state.libraries.currentLibraryId
|
return this.$store.state.libraries.currentLibraryId
|
||||||
},
|
},
|
||||||
bookCoverWidth() {
|
bookCoverWidth() {
|
||||||
return this.availableSizes[this.selectedSizeIndex]
|
return this.$store.getters['user/getUserSetting']('bookshelfCoverSize')
|
||||||
},
|
},
|
||||||
sizeMultiplier() {
|
sizeMultiplier() {
|
||||||
return this.bookCoverWidth / 120
|
return this.bookCoverWidth / 120
|
||||||
@ -66,41 +61,28 @@ export default {
|
|||||||
showBookshelfTextureModal() {
|
showBookshelfTextureModal() {
|
||||||
this.$store.commit('globals/setShowBookshelfTextureModal', true)
|
this.$store.commit('globals/setShowBookshelfTextureModal', true)
|
||||||
},
|
},
|
||||||
increaseSize() {
|
|
||||||
this.selectedSizeIndex = Math.min(this.availableSizes.length - 1, this.selectedSizeIndex + 1)
|
|
||||||
this.resize()
|
|
||||||
this.$store.dispatch('user/updateUserSettings', { bookshelfCoverSize: this.bookCoverWidth })
|
|
||||||
},
|
|
||||||
decreaseSize() {
|
|
||||||
this.selectedSizeIndex = Math.max(0, this.selectedSizeIndex - 1)
|
|
||||||
this.resize()
|
|
||||||
this.$store.dispatch('user/updateUserSettings', { bookshelfCoverSize: this.bookCoverWidth })
|
|
||||||
},
|
|
||||||
async init() {
|
async init() {
|
||||||
this.wrapperClientWidth = this.$refs.wrapper ? this.$refs.wrapper.clientWidth : 0
|
this.wrapperClientWidth = this.$refs.wrapper ? this.$refs.wrapper.clientWidth : 0
|
||||||
|
|
||||||
var bookshelfCoverSize = this.$store.getters['user/getUserSetting']('bookshelfCoverSize')
|
|
||||||
var sizeIndex = this.availableSizes.findIndex((s) => s === bookshelfCoverSize)
|
|
||||||
if (!isNaN(sizeIndex)) this.selectedSizeIndex = sizeIndex
|
|
||||||
|
|
||||||
// await this.$store.dispatch('audiobooks/load')
|
|
||||||
if (this.search) {
|
if (this.search) {
|
||||||
this.setShelvesFromSearch()
|
this.setShelvesFromSearch()
|
||||||
} else {
|
} else {
|
||||||
var categories = await this.$axios
|
await this.fetchCategories()
|
||||||
.$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
|
this.loaded = true
|
||||||
},
|
},
|
||||||
|
async fetchCategories() {
|
||||||
|
var categories = await this.$axios
|
||||||
|
.$get(`/api/libraries/${this.currentLibraryId}/categories`)
|
||||||
|
.then((data) => {
|
||||||
|
return data
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to fetch categories', error)
|
||||||
|
return []
|
||||||
|
})
|
||||||
|
this.shelves = categories
|
||||||
|
},
|
||||||
async setShelvesFromSearch() {
|
async setShelvesFromSearch() {
|
||||||
var shelves = []
|
var shelves = []
|
||||||
if (this.results.audiobooks) {
|
if (this.results.audiobooks) {
|
||||||
@ -111,14 +93,17 @@ export default {
|
|||||||
entities: this.results.audiobooks.map((ab) => ab.audiobook)
|
entities: this.results.audiobooks.map((ab) => ab.audiobook)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (this.results.authors) {
|
|
||||||
shelves.push({
|
// TODO: Author shelves
|
||||||
id: 'authors',
|
// if (this.results.authors) {
|
||||||
label: 'Authors',
|
// shelves.push({
|
||||||
type: 'authors',
|
// id: 'authors',
|
||||||
entities: this.results.authors.map((a) => a.author)
|
// label: 'Authors',
|
||||||
})
|
// type: 'authors',
|
||||||
}
|
// entities: this.results.authors.map((a) => a.author)
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
if (this.results.series) {
|
if (this.results.series) {
|
||||||
shelves.push({
|
shelves.push({
|
||||||
id: 'series',
|
id: 'series',
|
||||||
@ -149,27 +134,100 @@ export default {
|
|||||||
}
|
}
|
||||||
this.shelves = shelves
|
this.shelves = shelves
|
||||||
},
|
},
|
||||||
resize() {},
|
settingsUpdated(settings) {},
|
||||||
settingsUpdated(settings) {
|
|
||||||
if (settings.bookshelfCoverSize !== this.bookCoverWidth && settings.bookshelfCoverSize !== undefined) {
|
|
||||||
var index = this.availableSizes.indexOf(settings.bookshelfCoverSize)
|
|
||||||
if (index >= 0) {
|
|
||||||
this.selectedSizeIndex = index
|
|
||||||
this.resize()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
scan() {
|
scan() {
|
||||||
this.$root.socket.emit('scan', this.$store.state.libraries.currentLibraryId)
|
this.$root.socket.emit('scan', this.$store.state.libraries.currentLibraryId)
|
||||||
|
},
|
||||||
|
audiobookAdded(audiobook) {
|
||||||
|
console.log('Audiobook added', audiobook)
|
||||||
|
// TODO: Check if audiobook would be on this shelf
|
||||||
|
if (!this.search) {
|
||||||
|
this.fetchCategories()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
audiobookUpdated(audiobook) {
|
||||||
|
console.log('Audiobook updated', audiobook)
|
||||||
|
this.shelves.forEach((shelf) => {
|
||||||
|
if (shelf.type === 'books') {
|
||||||
|
shelf.entities = shelf.entities.map((ent) => {
|
||||||
|
if (ent.id === audiobook.id) {
|
||||||
|
return audiobook
|
||||||
|
}
|
||||||
|
return ent
|
||||||
|
})
|
||||||
|
} else if (shelf.type === 'series') {
|
||||||
|
shelf.entities.forEach((ent) => {
|
||||||
|
ent.books = ent.books.map((book) => {
|
||||||
|
if (book.id === audiobook.id) return audiobook
|
||||||
|
return book
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
removeBookFromShelf(audiobook) {
|
||||||
|
this.shelves.forEach((shelf) => {
|
||||||
|
if (shelf.type === 'books') {
|
||||||
|
shelf.entities = shelf.entities.filter((ent) => {
|
||||||
|
return ent.id !== audiobook.id
|
||||||
|
})
|
||||||
|
} else if (shelf.type === 'series') {
|
||||||
|
shelf.entities.forEach((ent) => {
|
||||||
|
ent.books = ent.books.filter((book) => {
|
||||||
|
return book.id !== audiobook.id
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
audiobookRemoved(audiobook) {
|
||||||
|
this.removeBookFromShelf(audiobook)
|
||||||
|
},
|
||||||
|
audiobooksAdded(audiobooks) {
|
||||||
|
console.log('audiobooks added', audiobooks)
|
||||||
|
// TODO: Check if audiobook would be on this shelf
|
||||||
|
if (!this.search) {
|
||||||
|
this.fetchCategories()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
audiobooksUpdated(audiobooks) {
|
||||||
|
audiobooks.forEach((ab) => {
|
||||||
|
this.audiobookUpdated(ab)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
initListeners() {
|
||||||
|
this.$store.commit('user/addSettingsListener', { id: '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('Error socket not initialized')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
removeListeners() {
|
||||||
|
this.$store.commit('user/removeSettingsListener', '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('Error socket not initialized')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.$store.commit('user/addSettingsListener', { id: 'bookshelf', meth: this.settingsUpdated })
|
this.initListeners()
|
||||||
|
|
||||||
this.init()
|
this.init()
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
this.$store.commit('user/removeSettingsListener', 'bookshelf')
|
this.removeListeners()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -9,25 +9,25 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-if="shelf.type === 'series'" class="flex items-center -mb-2">
|
<div v-if="shelf.type === 'series'" class="flex items-center -mb-2">
|
||||||
<template v-for="entity in shelf.entities">
|
<template v-for="entity in shelf.entities">
|
||||||
<cards-group-card :key="entity.name" :width="bookCoverWidth" :group="entity" @click="$emit('clickSeries', entity)" />
|
<cards-group-card :key="entity.name" is-categorized :width="bookCoverWidth" :group="entity" @click="$emit('clickSeries', entity)" />
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="shelf.type === 'tags'" class="flex items-center -mb-2">
|
<div v-if="shelf.type === 'tags'" class="flex items-center -mb-2">
|
||||||
<template v-for="entity in shelf.entities">
|
<template v-for="entity in shelf.entities">
|
||||||
<nuxt-link :key="entity.name" :to="`/library/${currentLibraryId}/bookshelf?filter=tags.${$encode(entity.name)}`">
|
<nuxt-link :key="entity.name" :to="`/library/${currentLibraryId}/bookshelf?filter=tags.${$encode(entity.name)}`">
|
||||||
<cards-group-card is-search :width="bookCoverWidth" :group="entity" />
|
<cards-group-card is-categorized :width="bookCoverWidth" :group="entity" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="shelf.series" class="flex items-center -mb-2">
|
<div v-else-if="shelf.series" class="flex items-center -mb-2">
|
||||||
<template v-for="entity in shelf.series">
|
<template v-for="entity in shelf.series">
|
||||||
<cards-group-card is-search :key="entity.name" :width="bookCoverWidth" :group="entity" @click="$emit('clickSeries', entity)" />
|
<cards-group-card is-categorized :key="entity.name" :width="bookCoverWidth" :group="entity" @click="$emit('clickSeries', entity)" />
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="shelf.tags" class="flex items-center -mb-2">
|
<div v-else-if="shelf.tags" class="flex items-center -mb-2">
|
||||||
<template v-for="entity in shelf.tags">
|
<template v-for="entity in shelf.tags">
|
||||||
<nuxt-link :key="entity.name" :to="`/library/${currentLibraryId}/bookshelf?filter=tags.${$encode(entity.name)}`">
|
<nuxt-link :key="entity.name" :to="`/library/${currentLibraryId}/bookshelf?filter=tags.${$encode(entity.name)}`">
|
||||||
<cards-group-card is-search :width="bookCoverWidth" :group="entity" />
|
<cards-group-card is-categorized :width="bookCoverWidth" :group="entity" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
@ -31,16 +31,16 @@
|
|||||||
<!-- <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" /> -->
|
<!-- <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-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" />
|
<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">
|
<!-- <div v-show="showSortFilters" class="h-7 ml-4 flex border border-white border-opacity-25 rounded-md">
|
||||||
<div class="h-full px-2 text-white flex items-center rounded-l-md hover:bg-primary hover:bg-opacity-75 cursor-pointer" :class="isGridMode ? 'bg-primary' : 'text-opacity-70'" @click="$emit('update:viewMode', 'grid')">
|
<div class="h-full px-2 text-white flex items-center rounded-l-md hover:bg-primary hover:bg-opacity-75 cursor-pointer" :class="isGridMode ? 'bg-primary' : 'text-opacity-70'" @click="$emit('update:viewMode', 'grid')">
|
||||||
<span class="material-icons" style="font-size: 1.4rem">view_module</span>
|
<span class="material-icons" style="font-size: 1.4rem">view_module</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="h-full px-2 text-white flex items-center rounded-r-md hover:bg-primary hover:bg-opacity-75 cursor-pointer" :class="!isGridMode ? 'bg-primary' : 'text-opacity-70'" @click="$emit('update:viewMode', 'list')">
|
<div class="h-full px-2 text-white flex items-center rounded-r-md hover:bg-primary hover:bg-opacity-75 cursor-pointer" :class="!isGridMode ? 'bg-primary' : 'text-opacity-70'" @click="$emit('update:viewMode', 'list')">
|
||||||
<span class="material-icons" style="font-size: 1.4rem">view_list</span>
|
<span class="material-icons" style="font-size: 1.4rem">view_list</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> -->
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="!isHome">
|
<template v-else-if="page === 'search'">
|
||||||
<div @click="searchBackArrow" class="rounded-full h-10 w-10 flex items-center justify-center hover:bg-white hover:bg-opacity-10 cursor-pointer">
|
<div @click="searchBackArrow" class="rounded-full h-10 w-10 flex items-center justify-center hover:bg-white hover:bg-opacity-10 cursor-pointer">
|
||||||
<span class="material-icons text-3xl text-white">west</span>
|
<span class="material-icons text-3xl text-white">west</span>
|
||||||
</div>
|
</div>
|
||||||
@ -59,10 +59,6 @@ export default {
|
|||||||
page: String,
|
page: String,
|
||||||
isHome: Boolean,
|
isHome: Boolean,
|
||||||
selectedSeries: String,
|
selectedSeries: String,
|
||||||
searchResults: {
|
|
||||||
type: Object,
|
|
||||||
default: () => {}
|
|
||||||
},
|
|
||||||
searchQuery: String,
|
searchQuery: String,
|
||||||
viewMode: String
|
viewMode: String
|
||||||
},
|
},
|
||||||
@ -84,27 +80,9 @@ export default {
|
|||||||
},
|
},
|
||||||
numShowing() {
|
numShowing() {
|
||||||
return this.totalEntities
|
return this.totalEntities
|
||||||
|
|
||||||
if (this.page === '') {
|
|
||||||
// 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
|
|
||||||
} else if (this.page === 'collections') {
|
|
||||||
return (this.$store.state.user.collections || []).length
|
|
||||||
} else {
|
|
||||||
var groups = this.$store.getters['audiobooks/getSeriesGroups']()
|
|
||||||
if (this.selectedSeries) {
|
|
||||||
var group = groups.find((g) => g.name === this.selectedSeries)
|
|
||||||
if (group) return group.books.length
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return groups.length
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
entityName() {
|
entityName() {
|
||||||
if (!this.page) return 'Audiobooks'
|
if (!this.page) return 'Books'
|
||||||
if (this.page === 'series') return 'Series'
|
if (this.page === 'series') return 'Series'
|
||||||
if (this.page === 'collections') return 'Collections'
|
if (this.page === 'collections') return 'Collections'
|
||||||
return ''
|
return ''
|
||||||
@ -139,7 +117,6 @@ export default {
|
|||||||
},
|
},
|
||||||
seriesBackArrow() {
|
seriesBackArrow() {
|
||||||
this.$router.replace(`/library/${this.currentLibraryId}/bookshelf/series`)
|
this.$router.replace(`/library/${this.currentLibraryId}/bookshelf/series`)
|
||||||
this.$emit('update:selectedSeries', null)
|
|
||||||
},
|
},
|
||||||
updateOrder() {
|
updateOrder() {
|
||||||
this.saveSettings()
|
this.saveSettings()
|
||||||
|
@ -9,9 +9,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</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>
|
<p class="text-xl text-center">{{ emptyMessage }}</p>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -20,7 +33,8 @@ import bookshelfCardsHelpers from '@/mixins/bookshelfCardsHelpers'
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
page: String
|
page: String,
|
||||||
|
seriesId: String
|
||||||
},
|
},
|
||||||
mixins: [bookshelfCardsHelpers],
|
mixins: [bookshelfCardsHelpers],
|
||||||
data() {
|
data() {
|
||||||
@ -36,7 +50,7 @@ export default {
|
|||||||
pagesLoaded: {},
|
pagesLoaded: {},
|
||||||
entityIndexesMounted: [],
|
entityIndexesMounted: [],
|
||||||
entityComponentRefs: {},
|
entityComponentRefs: {},
|
||||||
bookWidth: 120,
|
currentBookWidth: 0,
|
||||||
pageLoadQueue: [],
|
pageLoadQueue: [],
|
||||||
isFetchingEntities: false,
|
isFetchingEntities: false,
|
||||||
scrollTimeout: null,
|
scrollTimeout: null,
|
||||||
@ -47,27 +61,34 @@ export default {
|
|||||||
isSelectAll: false,
|
isSelectAll: false,
|
||||||
currentSFQueryString: null,
|
currentSFQueryString: null,
|
||||||
pendingReset: false,
|
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: {
|
computed: {
|
||||||
// booksFiltered() {
|
isRootUser() {
|
||||||
// const keywordFilterKeys = ['title', 'subtitle', 'author', 'series', 'narrator']
|
return this.$store.getters['user/getIsRoot']
|
||||||
// const keyworkFilter = state.keywordFilter.toLowerCase()
|
},
|
||||||
// return this.books.filter((ab) => {
|
showExperimentalFeatures() {
|
||||||
// if (!ab.book) return false
|
return this.$store.state.showExperimentalFeatures
|
||||||
// return !!keywordFilterKeys.find((key) => ab.book[key] && ab.book[key].toLowerCase().includes(keyworkFilter))
|
},
|
||||||
// })
|
|
||||||
// },
|
|
||||||
emptyMessage() {
|
emptyMessage() {
|
||||||
if (this.page === 'series') return `You have no series`
|
if (this.page === 'series') return `You have no series`
|
||||||
if (this.page === 'collections') return "You haven't made any collections yet"
|
if (this.page === 'collections') return "You haven't made any collections yet"
|
||||||
return 'No results'
|
return 'No results'
|
||||||
},
|
},
|
||||||
entityName() {
|
entityName() {
|
||||||
if (this.page === 'series') return 'series'
|
if (!this.page) return 'books'
|
||||||
if (this.page === 'collections') return 'collections'
|
return this.page
|
||||||
return 'books'
|
|
||||||
},
|
},
|
||||||
orderBy() {
|
orderBy() {
|
||||||
return this.$store.getters['user/getUserSetting']('orderBy')
|
return this.$store.getters['user/getUserSetting']('orderBy')
|
||||||
@ -78,9 +99,15 @@ export default {
|
|||||||
filterBy() {
|
filterBy() {
|
||||||
return this.$store.getters['user/getUserSetting']('filterBy')
|
return this.$store.getters['user/getUserSetting']('filterBy')
|
||||||
},
|
},
|
||||||
|
hasFilter() {
|
||||||
|
return this.filterBy && this.filterBy !== 'all'
|
||||||
|
},
|
||||||
currentLibraryId() {
|
currentLibraryId() {
|
||||||
return this.$store.state.libraries.currentLibraryId
|
return this.$store.state.libraries.currentLibraryId
|
||||||
},
|
},
|
||||||
|
bookWidth() {
|
||||||
|
return this.$store.getters['user/getUserSetting']('bookshelfCoverSize')
|
||||||
|
},
|
||||||
entityWidth() {
|
entityWidth() {
|
||||||
if (this.entityName === 'series') return this.bookWidth * 1.6
|
if (this.entityName === 'series') return this.bookWidth * 1.6
|
||||||
if (this.entityName === 'collections') return this.bookWidth * 2
|
if (this.entityName === 'collections') return this.bookWidth * 2
|
||||||
@ -107,20 +134,30 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
showBookshelfTextureModal() {
|
||||||
|
this.$store.commit('globals/setShowBookshelfTextureModal', true)
|
||||||
|
},
|
||||||
editEntity(entity) {
|
editEntity(entity) {
|
||||||
if (this.entityName === 'books') {
|
if (this.entityName === 'books' || this.entityName === 'series-books') {
|
||||||
var bookIds = this.entities.map((e) => e.id)
|
var bookIds = this.entities.map((e) => e.id)
|
||||||
this.$store.commit('setBookshelfBookIds', bookIds)
|
this.$store.commit('setBookshelfBookIds', bookIds)
|
||||||
this.$store.commit('showEditModal', entity)
|
this.$store.commit('showEditModal', entity)
|
||||||
|
} else if (this.entityName === 'collections') {
|
||||||
|
this.$store.commit('globals/setEditCollection', entity)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
clearSelectedBooks() {
|
clearSelectedEntities() {
|
||||||
this.updateBookSelectionMode(false)
|
this.updateBookSelectionMode(false)
|
||||||
this.isSelectionMode = false
|
this.isSelectionMode = false
|
||||||
this.isSelectAll = false
|
this.isSelectAll = false
|
||||||
},
|
},
|
||||||
selectAllBooks() {
|
selectAllEntities() {
|
||||||
this.isSelectAll = true
|
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) {
|
for (const key in this.entityComponentRefs) {
|
||||||
if (this.entityIndexesMounted.includes(Number(key))) {
|
if (this.entityIndexesMounted.includes(Number(key))) {
|
||||||
this.entityComponentRefs[key].selected = true
|
this.entityComponentRefs[key].selected = true
|
||||||
@ -128,7 +165,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
selectEntity(entity) {
|
selectEntity(entity) {
|
||||||
if (this.entityName === 'books') {
|
if (this.entityName === 'books' || this.entityName === 'series-books') {
|
||||||
this.$store.commit('toggleAudiobookSelected', entity.id)
|
this.$store.commit('toggleAudiobookSelected', entity.id)
|
||||||
|
|
||||||
var newIsSelectionMode = !!this.selectedAudiobooks.length
|
var newIsSelectionMode = !!this.selectedAudiobooks.length
|
||||||
@ -155,8 +192,10 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var entityPath = this.entityName === 'books' ? `books/all` : this.entityName
|
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 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)
|
console.error('failed to fetch books', error)
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
@ -167,7 +206,7 @@ export default {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (payload) {
|
if (payload) {
|
||||||
console.log('Received payload', payload)
|
// console.log('Received payload', payload)
|
||||||
if (!this.initialized) {
|
if (!this.initialized) {
|
||||||
this.initialized = true
|
this.initialized = true
|
||||||
this.totalEntities = payload.total
|
this.totalEntities = payload.total
|
||||||
@ -202,6 +241,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
handleScroll(scrollTop) {
|
handleScroll(scrollTop) {
|
||||||
|
this.currScrollTop = scrollTop
|
||||||
var firstShelfIndex = Math.floor(scrollTop / this.shelfHeight)
|
var firstShelfIndex = Math.floor(scrollTop / this.shelfHeight)
|
||||||
var lastShelfIndex = Math.ceil((scrollTop + this.bookshelfHeight) / this.shelfHeight)
|
var lastShelfIndex = Math.ceil((scrollTop + this.bookshelfHeight) / this.shelfHeight)
|
||||||
lastShelfIndex = Math.min(this.totalShelves - 1, lastShelfIndex)
|
lastShelfIndex = Math.min(this.totalShelves - 1, lastShelfIndex)
|
||||||
@ -233,11 +273,9 @@ export default {
|
|||||||
},
|
},
|
||||||
async resetEntities() {
|
async resetEntities() {
|
||||||
if (this.isFetchingEntities) {
|
if (this.isFetchingEntities) {
|
||||||
console.warn('RESET BOOKS BUT ALREADY FETCHING, WAIT FOR FETCH')
|
|
||||||
this.pendingReset = true
|
this.pendingReset = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.destroyEntityComponents()
|
this.destroyEntityComponents()
|
||||||
this.entityIndexesMounted = []
|
this.entityIndexesMounted = []
|
||||||
this.entityComponentRefs = {}
|
this.entityComponentRefs = {}
|
||||||
@ -250,13 +288,25 @@ export default {
|
|||||||
this.isSelectAll = false
|
this.isSelectAll = false
|
||||||
this.initialized = false
|
this.initialized = false
|
||||||
|
|
||||||
|
this.initSizeData()
|
||||||
this.pagesLoaded[0] = true
|
this.pagesLoaded[0] = true
|
||||||
await this.fetchEntites(0)
|
await this.fetchEntites(0)
|
||||||
var lastBookIndex = Math.min(this.totalEntities, this.shelvesPerPage * this.entitiesPerShelf)
|
var lastBookIndex = Math.min(this.totalEntities, this.shelvesPerPage * this.entitiesPerShelf)
|
||||||
this.mountEntites(0, lastBookIndex)
|
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() {
|
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 ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -273,11 +323,11 @@ export default {
|
|||||||
checkUpdateSearchParams() {
|
checkUpdateSearchParams() {
|
||||||
var newSearchParams = this.buildSearchParams()
|
var newSearchParams = this.buildSearchParams()
|
||||||
var currentQueryString = window.location.search
|
var currentQueryString = window.location.search
|
||||||
|
if (currentQueryString && currentQueryString.startsWith('?')) currentQueryString = currentQueryString.slice(1)
|
||||||
|
|
||||||
if (newSearchParams === '') {
|
if (newSearchParams === '') {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newSearchParams !== this.currentSFQueryString || newSearchParams !== currentQueryString) {
|
if (newSearchParams !== this.currentSFQueryString || newSearchParams !== currentQueryString) {
|
||||||
let newurl = window.location.protocol + '//' + window.location.host + window.location.pathname + '?' + newSearchParams
|
let newurl = window.location.protocol + '//' + window.location.host + window.location.pathname + '?' + newSearchParams
|
||||||
window.history.replaceState({ path: newurl }, '', newurl)
|
window.history.replaceState({ path: newurl }, '', newurl)
|
||||||
@ -290,6 +340,20 @@ export default {
|
|||||||
var wasUpdated = this.checkUpdateSearchParams()
|
var wasUpdated = this.checkUpdateSearchParams()
|
||||||
if (wasUpdated) {
|
if (wasUpdated) {
|
||||||
this.resetEntities()
|
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) {
|
scroll(e) {
|
||||||
@ -300,8 +364,51 @@ export default {
|
|||||||
this.handleScroll(scrollTop)
|
this.handleScroll(scrollTop)
|
||||||
// }, 250)
|
// }, 250)
|
||||||
},
|
},
|
||||||
async init(bookshelf) {
|
audiobookAdded(audiobook) {
|
||||||
this.checkUpdateSearchParams()
|
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
|
var { clientHeight, clientWidth } = bookshelf
|
||||||
this.bookshelfHeight = clientHeight
|
this.bookshelfHeight = clientHeight
|
||||||
@ -310,6 +417,16 @@ export default {
|
|||||||
this.shelvesPerPage = Math.ceil(this.bookshelfHeight / this.shelfHeight) + 2
|
this.shelvesPerPage = Math.ceil(this.bookshelfHeight / this.shelfHeight) + 2
|
||||||
this.bookshelfMarginLeft = (this.bookshelfWidth - this.entitiesPerShelf * this.totalEntityCardWidth) / 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
|
this.pagesLoaded[0] = true
|
||||||
await this.fetchEntites(0)
|
await this.fetchEntites(0)
|
||||||
var lastBookIndex = Math.min(this.totalEntities, this.shelvesPerPage * this.entitiesPerShelf)
|
var lastBookIndex = Math.min(this.totalEntities, this.shelvesPerPage * this.entitiesPerShelf)
|
||||||
@ -321,22 +438,42 @@ export default {
|
|||||||
this.init(bookshelf)
|
this.init(bookshelf)
|
||||||
bookshelf.addEventListener('scroll', this.scroll)
|
bookshelf.addEventListener('scroll', this.scroll)
|
||||||
}
|
}
|
||||||
this.$eventBus.$on('bookshelf-clear-selection', this.clearSelectedBooks)
|
this.$eventBus.$on('bookshelf-clear-selection', this.clearSelectedEntities)
|
||||||
this.$eventBus.$on('bookshelf-select-all', this.selectAllBooks)
|
this.$eventBus.$on('bookshelf-select-all', this.selectAllEntities)
|
||||||
this.$eventBus.$on('bookshelf-keyword-filter', this.updateKeywordFilter)
|
this.$eventBus.$on('bookshelf-keyword-filter', this.updateKeywordFilter)
|
||||||
|
|
||||||
this.$store.commit('user/addSettingsListener', { id: 'lazy-bookshelf', meth: this.settingsUpdated })
|
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() {
|
removeListeners() {
|
||||||
var bookshelf = document.getElementById('bookshelf')
|
var bookshelf = document.getElementById('bookshelf')
|
||||||
if (bookshelf) {
|
if (bookshelf) {
|
||||||
bookshelf.removeEventListener('scroll', this.scroll)
|
bookshelf.removeEventListener('scroll', this.scroll)
|
||||||
}
|
}
|
||||||
this.$eventBus.$off('bookshelf-clear-selection', this.clearSelectedBooks)
|
this.$eventBus.$off('bookshelf-clear-selection', this.clearSelectedEntities)
|
||||||
this.$eventBus.$off('bookshelf-select-all', this.selectAllBooks)
|
this.$eventBus.$off('bookshelf-select-all', this.selectAllEntities)
|
||||||
this.$eventBus.$off('bookshelf-keyword-filter', this.updateKeywordFilter)
|
this.$eventBus.$off('bookshelf-keyword-filter', this.updateKeywordFilter)
|
||||||
|
|
||||||
this.$store.commit('user/removeSettingsListener', 'lazy-bookshelf')
|
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() {
|
destroyEntityComponents() {
|
||||||
for (const key in this.entityComponentRefs) {
|
for (const key in this.entityComponentRefs) {
|
||||||
@ -344,6 +481,9 @@ export default {
|
|||||||
this.entityComponentRefs[key].destroy()
|
this.entityComponentRefs[key].destroy()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
scan() {
|
||||||
|
this.$root.socket.emit('scan', this.currentLibraryId)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
@ -21,14 +21,14 @@
|
|||||||
<div v-show="showLibrary" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
<div v-show="showLibrary" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf/series`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="paramId === 'series' ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf/series`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isSeriesPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
<p class="font-book pt-1.5" style="font-size: 0.9rem">Series</p>
|
<p class="font-book pt-1.5" style="font-size: 0.9rem">Series</p>
|
||||||
|
|
||||||
<div v-show="paramId === 'series'" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
<div v-show="isSeriesPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf/collections`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="paramId === 'collections' ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf/collections`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="paramId === 'collections' ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||||
@ -103,6 +103,9 @@ export default {
|
|||||||
homePage() {
|
homePage() {
|
||||||
return this.$route.name === 'library-library'
|
return this.$route.name === 'library-library'
|
||||||
},
|
},
|
||||||
|
isSeriesPage() {
|
||||||
|
return this.$route.name === 'library-library-series-id'
|
||||||
|
},
|
||||||
libraryBookshelfPage() {
|
libraryBookshelfPage() {
|
||||||
return this.$route.name === 'library-library-bookshelf-id'
|
return this.$route.name === 'library-library-bookshelf-id'
|
||||||
},
|
},
|
||||||
@ -114,7 +117,7 @@ export default {
|
|||||||
return this.libraryBookshelfPage && this.$route.query.filter === 'issues'
|
return this.libraryBookshelfPage && this.$route.query.filter === 'issues'
|
||||||
},
|
},
|
||||||
numIssues() {
|
numIssues() {
|
||||||
return this.$store.getters['audiobooks/getAudiobooksWithIssues'].length
|
return this.$store.state.libraries.issues || 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {},
|
methods: {},
|
||||||
|
@ -190,7 +190,7 @@ export default {
|
|||||||
return this.userProgress ? !!this.userProgress.isRead : false
|
return this.userProgress ? !!this.userProgress.isRead : false
|
||||||
},
|
},
|
||||||
showError() {
|
showError() {
|
||||||
return this.hasMissingParts || this.hasInvalidParts || this.isMissing || this.isIncomplete
|
return this.hasMissingParts || this.hasInvalidParts || this.isMissing || this.isInvalid
|
||||||
},
|
},
|
||||||
isStreaming() {
|
isStreaming() {
|
||||||
return this.$store.getters['getAudiobookIdStreaming'] === this.audiobookId
|
return this.$store.getters['getAudiobookIdStreaming'] === this.audiobookId
|
||||||
@ -199,7 +199,7 @@ export default {
|
|||||||
return !this.isSelectionMode && this.showExperimentalFeatures && !this.showPlayButton && this.hasEbook
|
return !this.isSelectionMode && this.showExperimentalFeatures && !this.showPlayButton && this.hasEbook
|
||||||
},
|
},
|
||||||
showPlayButton() {
|
showPlayButton() {
|
||||||
return !this.isSelectionMode && !this.isMissing && !this.isIncomplete && this.hasTracks && !this.isStreaming
|
return !this.isSelectionMode && !this.isMissing && !this.isInvalid && this.hasTracks && !this.isStreaming
|
||||||
},
|
},
|
||||||
showSmallEBookIcon() {
|
showSmallEBookIcon() {
|
||||||
return !this.isSelectionMode && this.showExperimentalFeatures && this.hasEbook
|
return !this.isSelectionMode && this.showExperimentalFeatures && this.hasEbook
|
||||||
@ -207,8 +207,8 @@ export default {
|
|||||||
isMissing() {
|
isMissing() {
|
||||||
return this.audiobook.isMissing
|
return this.audiobook.isMissing
|
||||||
},
|
},
|
||||||
isIncomplete() {
|
isInvalid() {
|
||||||
return this.audiobook.isIncomplete
|
return this.audiobook.isInvalid
|
||||||
},
|
},
|
||||||
hasMissingParts() {
|
hasMissingParts() {
|
||||||
return this.audiobook.hasMissingParts
|
return this.audiobook.hasMissingParts
|
||||||
@ -218,7 +218,7 @@ export default {
|
|||||||
},
|
},
|
||||||
errorText() {
|
errorText() {
|
||||||
if (this.isMissing) return 'Audiobook directory is missing!'
|
if (this.isMissing) return 'Audiobook directory is missing!'
|
||||||
else if (this.isIncomplete) return 'Audiobook has no audio tracks & ebook'
|
else if (this.isInvalid) return 'Audiobook has no audio tracks & ebook'
|
||||||
var txt = ''
|
var txt = ''
|
||||||
if (this.hasMissingParts) {
|
if (this.hasMissingParts) {
|
||||||
txt = `${this.hasMissingParts} missing parts.`
|
txt = `${this.hasMissingParts} missing parts.`
|
||||||
|
@ -3,9 +3,9 @@
|
|||||||
<div class="rounded-sm h-full relative" :style="{ padding: `${paddingY}px ${paddingX}px` }" @mouseover="mouseoverCard" @mouseleave="mouseleaveCard" @click="clickCard">
|
<div class="rounded-sm h-full relative" :style="{ padding: `${paddingY}px ${paddingX}px` }" @mouseover="mouseoverCard" @mouseleave="mouseleaveCard" @click="clickCard">
|
||||||
<nuxt-link :to="groupTo" class="cursor-pointer">
|
<nuxt-link :to="groupTo" class="cursor-pointer">
|
||||||
<div class="w-full h-full relative" :class="isHovering ? 'bg-black-400' : 'bg-primary'" :style="{ height: coverHeight + 'px', width: coverWidth + 'px' }">
|
<div class="w-full h-full relative" :class="isHovering ? 'bg-black-400' : 'bg-primary'" :style="{ height: coverHeight + 'px', width: coverWidth + 'px' }">
|
||||||
<covers-group-cover ref="groupcover" :name="groupName" :is-search="isSearch" :group-to="groupTo" :type="groupType" :book-items="bookItems" :width="coverWidth" :height="coverHeight" />
|
<covers-group-cover ref="groupcover" :name="groupName" :is-categorized="isCategorized" :group-to="groupTo" :type="groupType" :book-items="bookItems" :width="coverWidth" :height="coverHeight" />
|
||||||
|
|
||||||
<div v-if="hasValidCovers && (!showExperimentalFeatures || isSearch)" class="bg-black bg-opacity-60 absolute top-0 left-0 w-full h-full flex items-center justify-center text-center transition-opacity z-30" :class="isHovering ? '' : 'opacity-0'" :style="{ padding: `${sizeMultiplier}rem` }">
|
<div v-if="hasValidCovers && (!showExperimentalFeatures || isCategorized)" class="bg-black bg-opacity-60 absolute top-0 left-0 w-full h-full flex items-center justify-center text-center transition-opacity z-30" :class="isHovering ? '' : 'opacity-0'" :style="{ padding: `${sizeMultiplier}rem` }">
|
||||||
<p class="font-book" :style="{ fontSize: sizeMultiplier + 'rem' }">{{ groupName }}</p>
|
<p class="font-book" :style="{ fontSize: sizeMultiplier + 'rem' }">{{ groupName }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -19,7 +19,7 @@
|
|||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="categoryPlacard absolute z-30 left-0 right-0 mx-auto bottom-0 h-6 rounded-md font-book text-center" :style="{ width: Math.min(160, coverWidth) + 'px' }">
|
<div v-if="!isCategorized" class="categoryPlacard absolute z-30 left-0 right-0 mx-auto bottom-0 h-6 rounded-md font-book text-center" :style="{ width: Math.min(160, coverWidth) + 'px' }">
|
||||||
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${1 * sizeMultiplier}rem` }">
|
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${1 * sizeMultiplier}rem` }">
|
||||||
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ groupName }}</p>
|
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ groupName }}</p>
|
||||||
</div>
|
</div>
|
||||||
@ -42,7 +42,7 @@ export default {
|
|||||||
type: Number,
|
type: Number,
|
||||||
default: 24
|
default: 24
|
||||||
},
|
},
|
||||||
isSearch: Boolean
|
isCategorized: Boolean
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@ -74,7 +74,7 @@ export default {
|
|||||||
},
|
},
|
||||||
groupTo() {
|
groupTo() {
|
||||||
if (this.groupType === 'series') {
|
if (this.groupType === 'series') {
|
||||||
return `/library/${this.currentLibraryId}/bookshelf/series?series=${this.groupEncode}`
|
return `/library/${this.currentLibraryId}/series/${this.groupEncode}`
|
||||||
} else if (this.groupType === 'collection') {
|
} else if (this.groupType === 'collection') {
|
||||||
return `/collection/${this._group.id}`
|
return `/collection/${this._group.id}`
|
||||||
} else {
|
} else {
|
||||||
|
@ -1,11 +1,22 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="card" :id="`book-card-${index}`" :style="{ width: width + 'px', height: bookHeight + 'px' }" class="absolute top-0 left-0 rounded-sm z-10 cursor-pointer box-shadow-book" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
|
<div ref="card" :id="`book-card-${index}`" :style="{ width: width + 'px', height: bookHeight + 'px' }" class="absolute top-0 left-0 rounded-sm z-10 bg-primary cursor-pointer box-shadow-book" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
|
||||||
<div class="w-full h-full bg-primary relative rounded overflow-hidden">
|
<div v-show="showCoverBg" class="w-full h-full absolute top-0 left-0 z-0" ref="coverBg" />
|
||||||
|
<div class="w-full h-full absolute top-0 left-0 rounded overflow-hidden z-10">
|
||||||
<div v-show="audiobook && !imageReady" class="absolute top-0 left-0 w-full h-full flex items-center justify-center" :style="{ padding: sizeMultiplier * 0.5 + 'rem' }">
|
<div v-show="audiobook && !imageReady" class="absolute top-0 left-0 w-full h-full flex items-center justify-center" :style="{ padding: sizeMultiplier * 0.5 + 'rem' }">
|
||||||
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }" class="font-book text-gray-300 text-center">{{ title }}</p>
|
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }" class="font-book text-gray-300 text-center">{{ title }}</p>
|
||||||
</div>
|
</div>
|
||||||
<img v-show="audiobook" :src="bookCoverSrc" class="w-full h-full object-contain transition-opacity duration-300" @load="imageLoaded" :style="{ opacity: imageReady ? 1 : 0 }" />
|
|
||||||
<!-- <covers-book-cover v-show="audiobook" :audiobook="audiobook" :width="width" /> -->
|
<img v-show="audiobook" ref="cover" :src="bookCoverSrc" class="w-full h-full object-contain transition-opacity duration-300" @load="imageLoaded" :style="{ opacity: imageReady ? 1 : 0 }" />
|
||||||
|
|
||||||
|
<!-- Placeholder Cover Title & Author -->
|
||||||
|
<div v-if="!hasCover" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full flex items-center justify-center" :style="{ padding: placeholderCoverPadding + 'rem' }">
|
||||||
|
<div>
|
||||||
|
<p class="text-center font-book" style="color: rgb(247 223 187)" :style="{ fontSize: titleFontSize + 'rem' }">{{ titleCleaned }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="!hasCover" class="absolute left-0 right-0 w-full flex items-center justify-center" :style="{ padding: placeholderCoverPadding + 'rem', bottom: authorBottom + 'rem' }">
|
||||||
|
<p class="text-center font-book" style="color: rgb(247 223 187); opacity: 0.75" :style="{ fontSize: authorFontSize + 'rem' }">{{ authorCleaned }}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b" :class="userIsRead ? 'bg-success' : 'bg-yellow-400'" :style="{ width: width * userProgressPercent + 'px' }"></div>
|
<div class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b" :class="userIsRead ? 'bg-success' : 'bg-yellow-400'" :style="{ width: width * userProgressPercent + 'px' }"></div>
|
||||||
@ -35,7 +46,15 @@
|
|||||||
<span class="material-icons" :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">more_vert</span>
|
<span class="material-icons" :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">more_vert</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<ui-tooltip v-if="showError" :text="errorText" class="absolute bottom-4 left-0 z-10">
|
||||||
|
<div :style="{ height: 1.5 * sizeMultiplier + 'rem', width: 2.5 * sizeMultiplier + 'rem' }" class="bg-error rounded-r-full shadow-md flex items-center justify-end border-r border-b border-red-300">
|
||||||
|
<span class="material-icons text-red-100 pr-1" :style="{ fontSize: 0.875 * sizeMultiplier + 'rem' }">priority_high</span>
|
||||||
|
</div>
|
||||||
|
</ui-tooltip>
|
||||||
|
|
||||||
|
<div v-if="volumeNumber && showVolumeNumber && !isHovering && !isSelectionMode" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-10" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }">
|
||||||
|
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">#{{ volumeNumber }}</p>
|
||||||
|
</div>
|
||||||
<!-- <div ref="overlay-wrapper" class="w-full h-full relative box-shadow-book cursor-pointer" @click="clickCard" @mouseover="mouseover" @mouseleave="isHovering = false">
|
<!-- <div ref="overlay-wrapper" class="w-full h-full relative box-shadow-book cursor-pointer" @click="clickCard" @mouseover="mouseover" @mouseleave="isHovering = false">
|
||||||
<covers-book-cover :audiobook="audiobook" :width="width" />
|
<covers-book-cover :audiobook="audiobook" :width="width" />
|
||||||
<div v-if="false" ref="overlay">
|
<div v-if="false" ref="overlay">
|
||||||
@ -77,11 +96,7 @@
|
|||||||
|
|
||||||
<div v-show="!isSelectionMode" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full" :class="userIsRead ? 'bg-success' : 'bg-yellow-400'" :style="{ width: width * userProgressPercent + 'px' }"></div>
|
<div v-show="!isSelectionMode" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full" :class="userIsRead ? 'bg-success' : 'bg-yellow-400'" :style="{ width: width * userProgressPercent + 'px' }"></div>
|
||||||
|
|
||||||
<ui-tooltip v-if="showError" :text="errorText" class="absolute bottom-4 left-0">
|
|
||||||
<div :style="{ height: 1.5 * sizeMultiplier + 'rem', width: 2.5 * sizeMultiplier + 'rem' }" class="bg-error rounded-r-full shadow-md flex items-center justify-end border-r border-b border-red-300">
|
|
||||||
<span class="material-icons text-red-100 pr-1" :style="{ fontSize: 0.875 * sizeMultiplier + 'rem' }">priority_high</span>
|
|
||||||
</div>
|
|
||||||
</ui-tooltip>
|
|
||||||
</div>
|
</div>
|
||||||
</div> -->
|
</div> -->
|
||||||
</div>
|
</div>
|
||||||
@ -97,26 +112,32 @@ export default {
|
|||||||
width: {
|
width: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 120
|
default: 120
|
||||||
}
|
},
|
||||||
|
showVolumeNumber: Boolean
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
isAttached: false,
|
|
||||||
isHovering: false,
|
isHovering: false,
|
||||||
isMoreMenuOpen: false,
|
isMoreMenuOpen: false,
|
||||||
isProcessingReadUpdate: false,
|
isProcessingReadUpdate: false,
|
||||||
overlayEl: null,
|
|
||||||
audiobook: null,
|
audiobook: null,
|
||||||
imageReady: false,
|
imageReady: false,
|
||||||
rescanning: false,
|
rescanning: false,
|
||||||
selected: false,
|
selected: false,
|
||||||
isSelectionMode: false
|
isSelectionMode: false,
|
||||||
|
showCoverBg: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
showExperimentalFeatures() {
|
||||||
|
return this.store.state.showExperimentalFeatures
|
||||||
|
},
|
||||||
_audiobook() {
|
_audiobook() {
|
||||||
return this.audiobook || {}
|
return this.audiobook || {}
|
||||||
},
|
},
|
||||||
|
placeholderUrl() {
|
||||||
|
return '/book_placeholder.jpg'
|
||||||
|
},
|
||||||
bookCoverSrc() {
|
bookCoverSrc() {
|
||||||
return this.store.getters['audiobooks/getBookCoverSrc'](this._audiobook, this.placeholderUrl)
|
return this.store.getters['audiobooks/getBookCoverSrc'](this._audiobook, this.placeholderUrl)
|
||||||
},
|
},
|
||||||
@ -129,21 +150,15 @@ export default {
|
|||||||
hasTracks() {
|
hasTracks() {
|
||||||
return this._audiobook.numTracks
|
return this._audiobook.numTracks
|
||||||
},
|
},
|
||||||
// isSelectionMode() {
|
|
||||||
// return !!this.selectedAudiobooks.length
|
|
||||||
// },
|
|
||||||
// selectedAudiobooks() {
|
|
||||||
// return this.store.state.selectedAudiobooks
|
|
||||||
// },
|
|
||||||
// selected() {
|
|
||||||
// return this.store.getters['getIsAudiobookSelected'](this.audiobookId)
|
|
||||||
// },
|
|
||||||
processingBatch() {
|
processingBatch() {
|
||||||
return this.store.state.processingBatch
|
return this.store.state.processingBatch
|
||||||
},
|
},
|
||||||
book() {
|
book() {
|
||||||
return this._audiobook.book || {}
|
return this._audiobook.book || {}
|
||||||
},
|
},
|
||||||
|
hasCover() {
|
||||||
|
return !!this.book.cover
|
||||||
|
},
|
||||||
bookHeight() {
|
bookHeight() {
|
||||||
return this.width * 1.6
|
return this.width * 1.6
|
||||||
},
|
},
|
||||||
@ -169,8 +184,7 @@ export default {
|
|||||||
return this.book.volumeNumber || null
|
return this.book.volumeNumber || null
|
||||||
},
|
},
|
||||||
userProgress() {
|
userProgress() {
|
||||||
var store = this.$store || this.$nuxt.$store
|
return this.store.getters['user/getUserAudiobook'](this.audiobookId)
|
||||||
return store.getters['user/getUserAudiobook'](this.audiobookId)
|
|
||||||
},
|
},
|
||||||
userProgressPercent() {
|
userProgressPercent() {
|
||||||
return this.userProgress ? this.userProgress.progress || 0 : 0
|
return this.userProgress ? this.userProgress.progress || 0 : 0
|
||||||
@ -179,17 +193,16 @@ export default {
|
|||||||
return this.userProgress ? !!this.userProgress.isRead : false
|
return this.userProgress ? !!this.userProgress.isRead : false
|
||||||
},
|
},
|
||||||
showError() {
|
showError() {
|
||||||
return this.hasMissingParts || this.hasInvalidParts || this.isMissing || this.isIncomplete
|
return this.hasMissingParts || this.hasInvalidParts || this.isMissing || this.isInvalid
|
||||||
},
|
},
|
||||||
isStreaming() {
|
isStreaming() {
|
||||||
var store = this.$store || this.$nuxt.$store
|
return this.store.getters['getAudiobookIdStreaming'] === this.audiobookId
|
||||||
return store.getters['getAudiobookIdStreaming'] === this.audiobookId
|
|
||||||
},
|
},
|
||||||
showReadButton() {
|
showReadButton() {
|
||||||
return !this.isSelectionMode && this.showExperimentalFeatures && !this.showPlayButton && this.hasEbook
|
return !this.isSelectionMode && this.showExperimentalFeatures && !this.showPlayButton && this.hasEbook
|
||||||
},
|
},
|
||||||
showPlayButton() {
|
showPlayButton() {
|
||||||
return !this.isSelectionMode && !this.isMissing && !this.isIncomplete && this.hasTracks && !this.isStreaming
|
return !this.isSelectionMode && !this.isMissing && !this.isInvalid && this.hasTracks && !this.isStreaming
|
||||||
},
|
},
|
||||||
showSmallEBookIcon() {
|
showSmallEBookIcon() {
|
||||||
return !this.isSelectionMode && this.showExperimentalFeatures && this.hasEbook
|
return !this.isSelectionMode && this.showExperimentalFeatures && this.hasEbook
|
||||||
@ -197,8 +210,8 @@ export default {
|
|||||||
isMissing() {
|
isMissing() {
|
||||||
return this._audiobook.isMissing
|
return this._audiobook.isMissing
|
||||||
},
|
},
|
||||||
isIncomplete() {
|
isInvalid() {
|
||||||
return this._audiobook.isIncomplete
|
return this._audiobook.isInvalid
|
||||||
},
|
},
|
||||||
hasMissingParts() {
|
hasMissingParts() {
|
||||||
return this._audiobook.hasMissingParts
|
return this._audiobook.hasMissingParts
|
||||||
@ -208,7 +221,7 @@ export default {
|
|||||||
},
|
},
|
||||||
errorText() {
|
errorText() {
|
||||||
if (this.isMissing) return 'Audiobook directory is missing!'
|
if (this.isMissing) return 'Audiobook directory is missing!'
|
||||||
else if (this.isIncomplete) return 'Audiobook has no audio tracks & ebook'
|
else if (this.isInvalid) return 'Audiobook has no audio tracks & ebook'
|
||||||
var txt = ''
|
var txt = ''
|
||||||
if (this.hasMissingParts) {
|
if (this.hasMissingParts) {
|
||||||
txt = `${this.hasMissingParts} missing parts.`
|
txt = `${this.hasMissingParts} missing parts.`
|
||||||
@ -279,6 +292,35 @@ export default {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
return items
|
return items
|
||||||
|
},
|
||||||
|
_socket() {
|
||||||
|
return this.$root.socket || this.$nuxt.$root.socket
|
||||||
|
},
|
||||||
|
titleFontSize() {
|
||||||
|
return 0.75 * this.sizeMultiplier
|
||||||
|
},
|
||||||
|
authorFontSize() {
|
||||||
|
return 0.6 * this.sizeMultiplier
|
||||||
|
},
|
||||||
|
placeholderCoverPadding() {
|
||||||
|
return 0.8 * this.sizeMultiplier
|
||||||
|
},
|
||||||
|
authorBottom() {
|
||||||
|
return 0.75 * this.sizeMultiplier
|
||||||
|
},
|
||||||
|
titleCleaned() {
|
||||||
|
if (!this.title) return ''
|
||||||
|
if (this.title.length > 60) {
|
||||||
|
return this.title.slice(0, 57) + '...'
|
||||||
|
}
|
||||||
|
return this.title
|
||||||
|
},
|
||||||
|
authorCleaned() {
|
||||||
|
if (!this.authorFL) return ''
|
||||||
|
if (this.authorFL.length > 30) {
|
||||||
|
return this.authorFL.slice(0, 27) + '...'
|
||||||
|
}
|
||||||
|
return this.authorFL
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@ -336,10 +378,9 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
rescan() {
|
rescan() {
|
||||||
var socket = this.$root.socket || this.$nuxt.$root.socket
|
|
||||||
this.rescanning = true
|
this.rescanning = true
|
||||||
socket.once('audiobook_scan_complete', this.audiobookScanComplete)
|
this._socket.once('audiobook_scan_complete', this.audiobookScanComplete)
|
||||||
socket.emit('scan_audiobook', this.audiobookId)
|
this._socket.emit('scan_audiobook', this.audiobookId)
|
||||||
},
|
},
|
||||||
showEditModalTracks() {
|
showEditModalTracks() {
|
||||||
// More menu func
|
// More menu func
|
||||||
@ -407,31 +448,17 @@ export default {
|
|||||||
clickShowMore() {
|
clickShowMore() {
|
||||||
this.createMoreMenu()
|
this.createMoreMenu()
|
||||||
},
|
},
|
||||||
clickReadEBook() {},
|
clickReadEBook() {
|
||||||
editBtnClick() {},
|
this.store.commit('showEReader', this.audiobook)
|
||||||
|
},
|
||||||
selectBtnClick() {
|
selectBtnClick() {
|
||||||
if (this.processingBatch) return
|
if (this.processingBatch) return
|
||||||
this.selected = !this.selected
|
this.selected = !this.selected
|
||||||
this.$emit('select', this.audiobook)
|
this.$emit('select', this.audiobook)
|
||||||
},
|
},
|
||||||
play() {},
|
play() {
|
||||||
detach() {
|
this.store.commit('setStreamAudiobook', this.audiobook)
|
||||||
if (!this.isAttached) return
|
this._socket.emit('open_stream', this.audiobookId)
|
||||||
if (this.$refs.overlay) {
|
|
||||||
this.overlayEl = this.$refs.overlay
|
|
||||||
this.overlayEl.remove()
|
|
||||||
} else if (this.overlayEl) {
|
|
||||||
this.overlayEl.remove()
|
|
||||||
}
|
|
||||||
this.isAttached = false
|
|
||||||
},
|
|
||||||
attach() {
|
|
||||||
if (this.isAttached) return
|
|
||||||
this.isAttached = true
|
|
||||||
|
|
||||||
if (this.overlayEl) {
|
|
||||||
this.$refs['overlay-wrapper'].appendChild(this.overlayEl)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
mouseover() {
|
mouseover() {
|
||||||
this.isHovering = true
|
this.isHovering = true
|
||||||
@ -450,8 +477,31 @@ export default {
|
|||||||
this.$el.remove()
|
this.$el.remove()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
setCoverBg() {
|
||||||
|
if (this.$refs.coverBg) {
|
||||||
|
this.$refs.coverBg.style.backgroundImage = `url("${this.bookCoverSrc}")`
|
||||||
|
this.$refs.coverBg.style.backgroundSize = 'cover'
|
||||||
|
this.$refs.coverBg.style.backgroundPosition = 'center'
|
||||||
|
this.$refs.coverBg.style.opacity = 0.25
|
||||||
|
this.$refs.coverBg.style.filter = 'blur(1px)'
|
||||||
|
}
|
||||||
|
},
|
||||||
imageLoaded() {
|
imageLoaded() {
|
||||||
this.imageReady = true
|
this.imageReady = true
|
||||||
|
|
||||||
|
if (this.$refs.cover && this.bookCoverSrc !== this.placeholderUrl) {
|
||||||
|
var { naturalWidth, naturalHeight } = this.$refs.cover
|
||||||
|
var aspectRatio = naturalHeight / naturalWidth
|
||||||
|
var arDiff = Math.abs(aspectRatio - 1.6)
|
||||||
|
|
||||||
|
// If image aspect ratio is <= 1.45 or >= 1.75 then use cover bg, otherwise stretch to fit
|
||||||
|
if (arDiff > 0.15) {
|
||||||
|
this.showCoverBg = true
|
||||||
|
this.$nextTick(this.setCoverBg)
|
||||||
|
} else {
|
||||||
|
this.showCoverBg = false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {}
|
mounted() {}
|
||||||
|
@ -3,7 +3,14 @@
|
|||||||
<div class="w-full h-full bg-primary relative rounded overflow-hidden">
|
<div class="w-full h-full bg-primary relative rounded overflow-hidden">
|
||||||
<covers-collection-cover ref="cover" :book-items="books" :width="width" :height="width" />
|
<covers-collection-cover ref="cover" :book-items="books" :width="width" :height="width" />
|
||||||
</div>
|
</div>
|
||||||
|
<div v-show="isHovering" class="w-full h-full absolute top-0 left-0 z-10 bg-black bg-opacity-40 pointer-events-none">
|
||||||
|
<!-- <div class="absolute pointer-events-auto" :style="{ top: 0.5 * sizeMultiplier + 'rem', left: 0.5 * sizeMultiplier + 'rem' }" @click.stop.prevent="toggleSelected">
|
||||||
|
<span class="material-icons text-xl text-white text-opacity-75 hover:text-opacity-100">radio_button_unchecked</span>
|
||||||
|
</div> -->
|
||||||
|
<div class="absolute pointer-events-auto" :style="{ top: 0.5 * sizeMultiplier + 'rem', right: 0.5 * sizeMultiplier + 'rem' }" @click.stop.prevent="clickEdit">
|
||||||
|
<span class="material-icons text-xl text-white text-opacity-75 hover:text-opacity-100">edit</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<!-- <div v-if="isHovering || isSelectionMode" class="absolute top-0 left-0 w-full h-full bg-black bg-opacity-40">
|
<!-- <div v-if="isHovering || isSelectionMode" class="absolute top-0 left-0 w-full h-full bg-black bg-opacity-40">
|
||||||
</div> -->
|
</div> -->
|
||||||
<div class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md font-book text-center" :style="{ width: Math.min(160, width) + 'px' }">
|
<div class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md font-book text-center" :style="{ width: Math.min(160, width) + 'px' }">
|
||||||
@ -70,6 +77,9 @@ export default {
|
|||||||
var router = this.$router || this.$nuxt.$router
|
var router = this.$router || this.$nuxt.$router
|
||||||
router.push(`/collection/${this.collection.id}`)
|
router.push(`/collection/${this.collection.id}`)
|
||||||
},
|
},
|
||||||
|
clickEdit() {
|
||||||
|
this.$emit('edit', this.collection)
|
||||||
|
},
|
||||||
destroy() {
|
destroy() {
|
||||||
// destroy the vue listeners, etc
|
// destroy the vue listeners, etc
|
||||||
this.$destroy()
|
this.$destroy()
|
||||||
|
@ -1,11 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="card" :id="`series-card-${index}`" :style="{ width: width + 'px', height: cardHeight + 'px' }" class="absolute top-0 left-0 rounded-sm z-10 cursor-pointer box-shadow-book" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
|
<div ref="card" :id="`series-card-${index}`" :style="{ width: width + 'px', height: cardHeight + 'px' }" class="absolute top-0 left-0 rounded-sm z-10 cursor-pointer box-shadow-book" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
|
||||||
<div class="w-full h-full bg-primary relative rounded overflow-hidden">
|
<div class="w-full h-full bg-primary relative rounded overflow-hidden">
|
||||||
<covers-group-cover ref="cover" :name="title" :book-items="books" :width="width" :height="width" />
|
<covers-group-cover ref="cover" :name="title" :book-items="books" :width="width" :height="width" :group-to="seriesBooksRoute" />
|
||||||
<!-- <div v-show="series && !imageReady" class="absolute top-0 left-0 w-full h-full flex items-center justify-center" :style="{ padding: sizeMultiplier * 0.5 + 'rem' }">
|
|
||||||
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }" class="font-book text-gray-300 text-center">{{ title }}</p>
|
|
||||||
</div>
|
|
||||||
<img v-show="series" :src="bookCoverSrc" class="w-full h-full object-contain transition-opacity duration-300" @load="imageLoaded" :style="{ opacity: imageReady ? 1 : 0 }" /> -->
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- <div v-if="isHovering || isSelectionMode" class="absolute top-0 left-0 w-full h-full bg-black bg-opacity-40">
|
<!-- <div v-if="isHovering || isSelectionMode" class="absolute top-0 left-0 w-full h-full bg-black bg-opacity-40">
|
||||||
@ -53,15 +49,11 @@ export default {
|
|||||||
store() {
|
store() {
|
||||||
return this.$store || this.$nuxt.$store
|
return this.$store || this.$nuxt.$store
|
||||||
},
|
},
|
||||||
firstBookInSeries() {
|
|
||||||
if (!this.series || !this.series.books.length) return null
|
|
||||||
return this.series.books[0]
|
|
||||||
},
|
|
||||||
bookCoverSrc() {
|
|
||||||
return this.store.getters['audiobooks/getBookCoverSrc'](this.firstBookInSeries)
|
|
||||||
},
|
|
||||||
currentLibraryId() {
|
currentLibraryId() {
|
||||||
return this.store.state.libraries.currentLibraryId
|
return this.store.state.libraries.currentLibraryId
|
||||||
|
},
|
||||||
|
seriesBooksRoute() {
|
||||||
|
return `/library/${this.currentLibraryId}/series/${this.$encode(this.title)}`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -141,23 +141,18 @@ export default {
|
|||||||
return _sel.text
|
return _sel.text
|
||||||
},
|
},
|
||||||
genres() {
|
genres() {
|
||||||
// return this.$store.getters['audiobooks/getGenresUsed']
|
|
||||||
return this.filterData.genres || []
|
return this.filterData.genres || []
|
||||||
},
|
},
|
||||||
tags() {
|
tags() {
|
||||||
// return this.$store.state.audiobooks.tags
|
|
||||||
return this.filterData.tags || []
|
return this.filterData.tags || []
|
||||||
},
|
},
|
||||||
series() {
|
series() {
|
||||||
// return this.$store.state.audiobooks.series
|
|
||||||
return this.filterData.series || []
|
return this.filterData.series || []
|
||||||
},
|
},
|
||||||
authors() {
|
authors() {
|
||||||
// return this.$store.getters['audiobooks/getUniqueAuthors']
|
|
||||||
return this.filterData.authors || []
|
return this.filterData.authors || []
|
||||||
},
|
},
|
||||||
narrators() {
|
narrators() {
|
||||||
// return this.$store.getters['audiobooks/getUniqueNarrators']
|
|
||||||
return this.filterData.narrators || []
|
return this.filterData.narrators || []
|
||||||
},
|
},
|
||||||
progress() {
|
progress() {
|
||||||
|
@ -79,9 +79,6 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
audiobooks() {
|
|
||||||
return this.$store.state.audiobooks.audiobooks
|
|
||||||
},
|
|
||||||
currentLibraryId() {
|
currentLibraryId() {
|
||||||
return this.$store.state.libraries.currentLibraryId
|
return this.$store.state.libraries.currentLibraryId
|
||||||
},
|
},
|
||||||
@ -131,7 +128,7 @@ export default {
|
|||||||
}
|
}
|
||||||
this.isFetching = true
|
this.isFetching = true
|
||||||
|
|
||||||
var searchResults = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/search?q=${value}`).catch((error) => {
|
var searchResults = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/search?q=${value}&limit=3`).catch((error) => {
|
||||||
console.error('Search error', error)
|
console.error('Search error', error)
|
||||||
return []
|
return []
|
||||||
})
|
})
|
||||||
|
@ -29,12 +29,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="!hasCover" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full flex items-center justify-center" :style="{ padding: placeholderCoverPadding + 'rem' }">
|
<div v-if="!hasCover" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full flex items-center justify-center z-10" :style="{ padding: placeholderCoverPadding + 'rem' }">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-center font-book" style="color: rgb(247 223 187)" :style="{ fontSize: titleFontSize + 'rem' }">{{ titleCleaned }}</p>
|
<p class="text-center font-book" style="color: rgb(247 223 187)" :style="{ fontSize: titleFontSize + 'rem' }">{{ titleCleaned }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!hasCover" class="absolute left-0 right-0 w-full flex items-center justify-center" :style="{ padding: placeholderCoverPadding + 'rem', bottom: authorBottom + 'rem' }">
|
<div v-if="!hasCover" class="absolute left-0 right-0 w-full flex items-center justify-center z-10" :style="{ padding: placeholderCoverPadding + 'rem', bottom: authorBottom + 'rem' }">
|
||||||
<p class="text-center font-book" style="color: rgb(247 223 187); opacity: 0.75" :style="{ fontSize: authorFontSize + 'rem' }">{{ authorCleaned }}</p>
|
<p class="text-center font-book" style="color: rgb(247 223 187); opacity: 0.75" :style="{ fontSize: authorFontSize + 'rem' }">{{ authorCleaned }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -17,7 +17,7 @@ export default {
|
|||||||
width: Number,
|
width: Number,
|
||||||
height: Number,
|
height: Number,
|
||||||
groupTo: String,
|
groupTo: String,
|
||||||
isSearch: Boolean
|
isCategorized: Boolean
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@ -53,7 +53,7 @@ export default {
|
|||||||
return this.store.state.showExperimentalFeatures
|
return this.store.state.showExperimentalFeatures
|
||||||
},
|
},
|
||||||
showCoverFan() {
|
showCoverFan() {
|
||||||
return this.showExperimentalFeatures && this.windowWidth > 1024 && !this.isSearch
|
return this.showExperimentalFeatures && this.windowWidth > 1024 && !this.isCategorized
|
||||||
},
|
},
|
||||||
store() {
|
store() {
|
||||||
return this.$store || this.$nuxt.$store
|
return this.$store || this.$nuxt.$store
|
||||||
|
@ -179,10 +179,16 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
goPrevBook() {
|
async goPrevBook() {
|
||||||
if (this.currentBookshelfIndex - 1 < 0) return
|
if (this.currentBookshelfIndex - 1 < 0) return
|
||||||
var prevBookId = this.bookshelfBookIds[this.currentBookshelfIndex - 1]
|
var prevBookId = this.bookshelfBookIds[this.currentBookshelfIndex - 1]
|
||||||
var prevBook = this.$store.getters['audiobooks/getAudiobook'](prevBookId)
|
this.processing = true
|
||||||
|
var prevBook = await this.$axios.$get(`/api/books/${prevBookId}`).catch((error) => {
|
||||||
|
var errorMsg = error.response && error.response.data ? error.response.data : 'Failed to fetch book'
|
||||||
|
this.$toast.error(errorMsg)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
this.processing = false
|
||||||
if (prevBook) {
|
if (prevBook) {
|
||||||
this.$store.commit('showEditModalOnTab', { audiobook: prevBook, tab: this.selectedTab })
|
this.$store.commit('showEditModalOnTab', { audiobook: prevBook, tab: this.selectedTab })
|
||||||
this.$nextTick(this.init)
|
this.$nextTick(this.init)
|
||||||
@ -190,11 +196,16 @@ export default {
|
|||||||
console.error('Book not found', prevBookId)
|
console.error('Book not found', prevBookId)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
goNextBook() {
|
async goNextBook() {
|
||||||
if (this.currentBookshelfIndex >= this.bookshelfBookIds.length - 1) return
|
if (this.currentBookshelfIndex >= this.bookshelfBookIds.length - 1) return
|
||||||
|
this.processing = true
|
||||||
var nextBookId = this.bookshelfBookIds[this.currentBookshelfIndex + 1]
|
var nextBookId = this.bookshelfBookIds[this.currentBookshelfIndex + 1]
|
||||||
var nextBook = this.$store.getters['audiobooks/getAudiobook'](nextBookId)
|
var nextBook = await this.$axios.$get(`/api/books/${nextBookId}`).catch((error) => {
|
||||||
|
var errorMsg = error.response && error.response.data ? error.response.data : 'Failed to fetch book'
|
||||||
|
this.$toast.error(errorMsg)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
this.processing = false
|
||||||
if (nextBook) {
|
if (nextBook) {
|
||||||
this.$store.commit('showEditModalOnTab', { audiobook: nextBook, tab: this.selectedTab })
|
this.$store.commit('showEditModalOnTab', { audiobook: nextBook, tab: this.selectedTab })
|
||||||
this.$nextTick(this.init)
|
this.$nextTick(this.init)
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
<div v-if="show" class="w-full h-full">
|
<div v-if="show" class="w-full h-full">
|
||||||
<div class="py-4 px-4">
|
<div class="py-4 px-4">
|
||||||
<h1 v-if="!showBatchUserCollectionModal" class="text-2xl">Add to Collection</h1>
|
<h1 v-if="!showBatchUserCollectionModal" class="text-2xl">Add to Collection</h1>
|
||||||
<h1 v-else class="text-2xl">Add {{ selectedBooks.length }} Books to Collection</h1>
|
<h1 v-else class="text-2xl">Add {{ selectedBookIds.length }} Books to Collection</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full overflow-y-auto overflow-x-hidden max-h-96">
|
<div class="w-full overflow-y-auto overflow-x-hidden max-h-96">
|
||||||
<transition-group name="list-complete" tag="div">
|
<transition-group name="list-complete" tag="div">
|
||||||
@ -65,12 +65,7 @@ export default {
|
|||||||
},
|
},
|
||||||
title() {
|
title() {
|
||||||
if (this.showBatchUserCollectionModal) {
|
if (this.showBatchUserCollectionModal) {
|
||||||
var title = this.selectedBooks[0] ? this.selectedBooks[0].book.title || '' : ''
|
return `${this.selectedBookIds.length} Books Selected`
|
||||||
if (this.selectedBooks.length > 1 && this.selectedBooks[1]) {
|
|
||||||
title += ', ' + this.selectedBooks[1].book.title || ''
|
|
||||||
if (this.selectedBooks.length > 2) title += `, and ${this.selectedBooks.length - 2} other${this.selectedBooks.length > 3 ? 's' : ''}`
|
|
||||||
}
|
|
||||||
return title
|
|
||||||
}
|
}
|
||||||
return this.selectedAudiobook ? this.selectedAudiobook.book.title : ''
|
return this.selectedAudiobook ? this.selectedAudiobook.book.title : ''
|
||||||
},
|
},
|
||||||
@ -107,11 +102,6 @@ export default {
|
|||||||
},
|
},
|
||||||
selectedBookIds() {
|
selectedBookIds() {
|
||||||
return this.$store.state.selectedAudiobooks || []
|
return this.$store.state.selectedAudiobooks || []
|
||||||
},
|
|
||||||
selectedBooks() {
|
|
||||||
return this.selectedBookIds.map((id) => {
|
|
||||||
return this.$store.getters['audiobooks/getAudiobook'](id)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
|
|
||||||
<div class="flex mt-2 -mx-1">
|
<div class="flex mt-2 -mx-1">
|
||||||
<div class="w-3/4 px-1">
|
<div class="w-3/4 px-1">
|
||||||
<ui-input-dropdown v-model="details.series" label="Series" :items="series" />
|
<ui-input-dropdown ref="seriesDropdown" v-model="details.series" label="Series" :items="series" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow px-1">
|
<div class="flex-grow px-1">
|
||||||
<ui-text-input-with-label v-model="details.volumeNumber" label="Volume #" />
|
<ui-text-input-with-label v-model="details.volumeNumber" label="Volume #" />
|
||||||
@ -28,10 +28,10 @@
|
|||||||
|
|
||||||
<div class="flex mt-2 -mx-1">
|
<div class="flex mt-2 -mx-1">
|
||||||
<div class="w-1/2 px-1">
|
<div class="w-1/2 px-1">
|
||||||
<ui-multi-select v-model="details.genres" label="Genres" :items="genres" />
|
<ui-multi-select ref="genresSelect" v-model="details.genres" label="Genres" :items="genres" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow px-1">
|
<div class="flex-grow px-1">
|
||||||
<ui-multi-select v-model="newTags" label="Tags" :items="tags" />
|
<ui-multi-select ref="tagsSelect" v-model="newTags" label="Tags" :items="tags" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -133,13 +133,16 @@ export default {
|
|||||||
return this.$store.getters['user/getUserCanDelete']
|
return this.$store.getters['user/getUserCanDelete']
|
||||||
},
|
},
|
||||||
genres() {
|
genres() {
|
||||||
return this.$store.state.audiobooks.genres
|
return this.filterData.genres || []
|
||||||
},
|
},
|
||||||
tags() {
|
tags() {
|
||||||
return this.$store.state.audiobooks.tags
|
return this.filterData.tags || []
|
||||||
},
|
},
|
||||||
series() {
|
series() {
|
||||||
return this.$store.state.audiobooks.series
|
return this.filterData.series || []
|
||||||
|
},
|
||||||
|
filterData() {
|
||||||
|
return this.$store.state.libraries.filterData || {}
|
||||||
},
|
},
|
||||||
libraryId() {
|
libraryId() {
|
||||||
return this.audiobook ? this.audiobook.libraryId : null
|
return this.audiobook ? this.audiobook.libraryId : null
|
||||||
@ -185,11 +188,23 @@ export default {
|
|||||||
this.$root.socket.once('save_metadata_complete', this.saveMetadataComplete)
|
this.$root.socket.once('save_metadata_complete', this.saveMetadataComplete)
|
||||||
this.$root.socket.emit('save_metadata', this.audiobookId)
|
this.$root.socket.emit('save_metadata', this.audiobookId)
|
||||||
},
|
},
|
||||||
async submitForm() {
|
submitForm() {
|
||||||
if (this.isProcessing) {
|
if (this.isProcessing) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.isProcessing = true
|
this.isProcessing = true
|
||||||
|
if (this.$refs.seriesDropdown && this.$refs.seriesDropdown.isFocused) {
|
||||||
|
this.$refs.seriesDropdown.blur()
|
||||||
|
}
|
||||||
|
if (this.$refs.genresSelect && this.$refs.genresSelect.isFocused) {
|
||||||
|
this.$refs.genresSelect.forceBlur()
|
||||||
|
}
|
||||||
|
if (this.$refs.tagsSelect && this.$refs.tagsSelect.isFocused) {
|
||||||
|
this.$refs.tagsSelect.forceBlur()
|
||||||
|
}
|
||||||
|
this.$nextTick(this.handleForm)
|
||||||
|
},
|
||||||
|
async handleForm() {
|
||||||
const updatePayload = {
|
const updatePayload = {
|
||||||
book: this.details,
|
book: this.details,
|
||||||
tags: this.newTags
|
tags: this.newTags
|
||||||
|
@ -125,7 +125,7 @@ export default {
|
|||||||
return this.audioFiles.length + this.otherFiles.length
|
return this.audioFiles.length + this.otherFiles.length
|
||||||
},
|
},
|
||||||
showM4bDownload() {
|
showM4bDownload() {
|
||||||
return !this._audiobook.isMissing && !this._audiobook.isIncomplete && this._audiobook.tracks.length
|
return !this._audiobook.isMissing && !this._audiobook.isInvalid && this._audiobook.tracks.length
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -139,7 +139,7 @@ export default {
|
|||||||
return Math.ceil(factor / 5) * 5
|
return Math.ceil(factor / 5) * 5
|
||||||
}
|
}
|
||||||
|
|
||||||
return factor
|
return Math.max(1, factor)
|
||||||
},
|
},
|
||||||
points() {
|
points() {
|
||||||
var data = []
|
var data = []
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
<path fill="currentColor" d="M9 3V18H12V3H9M12 5L16 18L19 17L15 4L12 5M5 5V18H8V5H5M3 19V21H21V19H3Z" />
|
<path fill="currentColor" d="M9 3V18H12V3H9M12 5L16 18L19 17L15 4L12 5M5 5V18H8V5H5M3 19V21H21V19H3Z" />
|
||||||
</svg>
|
</svg>
|
||||||
<div class="px-2">
|
<div class="px-2">
|
||||||
<p class="text-4xl md:text-5xl font-bold">{{ audiobooks.length }}</p>
|
<p class="text-4xl md:text-5xl font-bold">{{ totalBooks }}</p>
|
||||||
<p class="font-book text-xs md:text-sm text-white text-opacity-80">Books in Library</p>
|
<p class="font-book text-xs md:text-sm text-white text-opacity-80">Books in Library</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -35,7 +35,7 @@
|
|||||||
<path fill="currentColor" d="M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,6A2,2 0 0,0 10,8A2,2 0 0,0 12,10A2,2 0 0,0 14,8A2,2 0 0,0 12,6M12,13C14.67,13 20,14.33 20,17V20H4V17C4,14.33 9.33,13 12,13M12,14.9C9.03,14.9 5.9,16.36 5.9,17V18.1H18.1V17C18.1,16.36 14.97,14.9 12,14.9Z" />
|
<path fill="currentColor" d="M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,6A2,2 0 0,0 10,8A2,2 0 0,0 12,10A2,2 0 0,0 14,8A2,2 0 0,0 12,6M12,13C14.67,13 20,14.33 20,17V20H4V17C4,14.33 9.33,13 12,13M12,14.9C9.03,14.9 5.9,16.36 5.9,17V18.1H18.1V17C18.1,16.36 14.97,14.9 12,14.9Z" />
|
||||||
</svg>
|
</svg>
|
||||||
<div class="px-1">
|
<div class="px-1">
|
||||||
<p class="text-4xl md:text-5xl font-bold">{{ uniqueAuthors.length }}</p>
|
<p class="text-4xl md:text-5xl font-bold">{{ totalAuthors }}</p>
|
||||||
<p class="font-book text-xs md:text-sm text-white text-opacity-80">Authors</p>
|
<p class="font-book text-xs md:text-sm text-white text-opacity-80">Authors</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -56,20 +56,24 @@ export default {
|
|||||||
listeningStats: {
|
listeningStats: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => {}
|
default: () => {}
|
||||||
|
},
|
||||||
|
libraryStats: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {}
|
return {}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
audiobooks() {
|
|
||||||
return this.$store.state.audiobooks.audiobooks
|
|
||||||
},
|
|
||||||
user() {
|
user() {
|
||||||
return this.$store.state.user.user
|
return this.$store.state.user.user
|
||||||
},
|
},
|
||||||
uniqueAuthors() {
|
totalBooks() {
|
||||||
return this.$store.getters['audiobooks/getUniqueAuthors']
|
return this.libraryStats ? this.libraryStats.totalBooks : 0
|
||||||
|
},
|
||||||
|
totalAuthors() {
|
||||||
|
return this.libraryStats ? this.libraryStats.totalAuthors : 0
|
||||||
},
|
},
|
||||||
userAudiobooks() {
|
userAudiobooks() {
|
||||||
return Object.values(this.user.audiobooks || {})
|
return Object.values(this.user.audiobooks || {})
|
||||||
@ -78,11 +82,7 @@ export default {
|
|||||||
return this.userAudiobooks.filter((ab) => !!ab.isRead)
|
return this.userAudiobooks.filter((ab) => !!ab.isRead)
|
||||||
},
|
},
|
||||||
totalAudiobookDuration() {
|
totalAudiobookDuration() {
|
||||||
var _total = 0
|
return this.libraryStats ? this.libraryStats.totalDuration : 0
|
||||||
this.audiobooks.forEach((ab) => {
|
|
||||||
_total += ab.duration
|
|
||||||
})
|
|
||||||
return _total
|
|
||||||
},
|
},
|
||||||
totalAudiobookHours() {
|
totalAudiobookHours() {
|
||||||
var totalHours = Math.round(this.totalAudiobookDuration / (60 * 60))
|
var totalHours = Math.round(this.totalAudiobookDuration / (60 * 60))
|
||||||
|
@ -97,8 +97,8 @@ export default {
|
|||||||
isMissing() {
|
isMissing() {
|
||||||
return this.book.isMissing
|
return this.book.isMissing
|
||||||
},
|
},
|
||||||
isIncomplete() {
|
isInvalid() {
|
||||||
return this.book.isIncomplete
|
return this.book.isInvalid
|
||||||
},
|
},
|
||||||
numTracks() {
|
numTracks() {
|
||||||
return this.book.numTracks
|
return this.book.numTracks
|
||||||
@ -107,7 +107,7 @@ export default {
|
|||||||
return this.$store.getters['getAudiobookIdStreaming'] === this.book.id
|
return this.$store.getters['getAudiobookIdStreaming'] === this.book.id
|
||||||
},
|
},
|
||||||
showPlayBtn() {
|
showPlayBtn() {
|
||||||
return !this.isMissing && !this.isIncomplete && !this.isStreaming && this.numTracks
|
return !this.isMissing && !this.isInvalid && !this.isStreaming && this.numTracks
|
||||||
},
|
},
|
||||||
userAudiobooks() {
|
userAudiobooks() {
|
||||||
return this.$store.state.user.user ? this.$store.state.user.user.audiobooks || {} : {}
|
return this.$store.state.user.user ? this.$store.state.user.user.audiobooks || {} : {}
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
<button v-else class="btn outline-none rounded-md shadow-md relative border border-gray-600" :disabled="disabled || loading" :type="type" :class="classList" @click="click">
|
<button v-else class="btn outline-none rounded-md shadow-md relative border border-gray-600" :disabled="disabled || loading" :type="type" :class="classList" @mousedown.prevent @click="click">
|
||||||
<slot />
|
<slot />
|
||||||
<div v-if="loading" class="text-white absolute top-0 left-0 w-full h-full flex items-center justify-center text-opacity-100">
|
<div v-if="loading" class="text-white absolute top-0 left-0 w-full h-full flex items-center justify-center text-opacity-100">
|
||||||
<!-- <span class="material-icons animate-spin">refresh</span> -->
|
<!-- <span class="material-icons animate-spin">refresh</span> -->
|
||||||
|
@ -89,7 +89,24 @@ export default {
|
|||||||
inputFocus() {
|
inputFocus() {
|
||||||
this.isFocused = true
|
this.isFocused = true
|
||||||
},
|
},
|
||||||
|
blur() {
|
||||||
|
// Handle blur immediately
|
||||||
|
this.isFocused = false
|
||||||
|
if (this.input !== this.textInput) {
|
||||||
|
var val = this.textInput ? this.textInput.trim() : null
|
||||||
|
this.input = val
|
||||||
|
if (val && !this.items.includes(val)) {
|
||||||
|
this.$emit('newItem', val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.$refs.input) {
|
||||||
|
this.$refs.input.blur()
|
||||||
|
}
|
||||||
|
},
|
||||||
inputBlur() {
|
inputBlur() {
|
||||||
|
if (!this.isFocused) return
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (document.activeElement === this.$refs.input) {
|
if (document.activeElement === this.$refs.input) {
|
||||||
return
|
return
|
||||||
|
@ -79,7 +79,9 @@ export default {
|
|||||||
this.disabled = true
|
this.disabled = true
|
||||||
await this.$store.dispatch('libraries/fetch', library.id)
|
await this.$store.dispatch('libraries/fetch', library.id)
|
||||||
|
|
||||||
if (this.$route.name.startsWith('library')) {
|
if (this.$route.name.startsWith('config')) {
|
||||||
|
// No need to refresh
|
||||||
|
} else if (this.$route.name.startsWith('library')) {
|
||||||
var newRoute = this.$route.path.replace(currLibraryId, library.id)
|
var newRoute = this.$route.path.replace(currLibraryId, library.id)
|
||||||
this.$router.push(newRoute)
|
this.$router.push(newRoute)
|
||||||
} else {
|
} else {
|
||||||
|
@ -122,6 +122,8 @@ export default {
|
|||||||
this.recalcMenuPos()
|
this.recalcMenuPos()
|
||||||
},
|
},
|
||||||
inputBlur() {
|
inputBlur() {
|
||||||
|
if (!this.isFocused) return
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (document.activeElement === this.$refs.input) {
|
if (document.activeElement === this.$refs.input) {
|
||||||
return
|
return
|
||||||
@ -136,6 +138,11 @@ export default {
|
|||||||
blur() {
|
blur() {
|
||||||
if (this.$refs.input) this.$refs.input.blur()
|
if (this.$refs.input) this.$refs.input.blur()
|
||||||
},
|
},
|
||||||
|
forceBlur() {
|
||||||
|
this.isFocused = false
|
||||||
|
if (this.textInput) this.submitForm()
|
||||||
|
if (this.$refs.input) this.$refs.input.blur()
|
||||||
|
},
|
||||||
clickedOption(e, itemValue) {
|
clickedOption(e, itemValue) {
|
||||||
if (e) {
|
if (e) {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
|
51
client/components/widgets/CoverSizeWidget.vue
Normal file
51
client/components/widgets/CoverSizeWidget.vue
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="rounded-full py-1 bg-primary px-2 border border-black-100 text-center flex items-center box-shadow-md" @mousedown.prevent @mouseup.prevent>
|
||||||
|
<span class="material-icons" :class="selectedSizeIndex === 0 ? 'text-gray-400' : 'hover:text-yellow-300 cursor-pointer'" style="font-size: 0.9rem" @mousedown.prevent @click="decreaseSize">remove</span>
|
||||||
|
<p class="px-2 font-mono">{{ bookCoverWidth }}</p>
|
||||||
|
<span class="material-icons" :class="selectedSizeIndex === availableSizes.length - 1 ? 'text-gray-400' : 'hover:text-yellow-300 cursor-pointer'" style="font-size: 0.9rem" @mousedown.prevent @click="increaseSize">add</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
selectedSizeIndex: 3,
|
||||||
|
availableSizes: [60, 80, 100, 120, 140, 160, 180, 200, 220]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
selectedSize: {
|
||||||
|
immediate: true,
|
||||||
|
handler() {
|
||||||
|
this.setSelectedIndex()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
selectedSize() {
|
||||||
|
return this.$store.getters['user/getUserSetting']('bookshelfCoverSize')
|
||||||
|
},
|
||||||
|
bookCoverWidth() {
|
||||||
|
return this.availableSizes[this.selectedSizeIndex]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
increaseSize() {
|
||||||
|
this.selectedSizeIndex = Math.min(this.availableSizes.length - 1, this.selectedSizeIndex + 1)
|
||||||
|
this.$store.dispatch('user/updateUserSettings', { bookshelfCoverSize: this.bookCoverWidth })
|
||||||
|
},
|
||||||
|
decreaseSize() {
|
||||||
|
this.selectedSizeIndex = Math.max(0, this.selectedSizeIndex - 1)
|
||||||
|
this.$store.dispatch('user/updateUserSettings', { bookshelfCoverSize: this.bookCoverWidth })
|
||||||
|
},
|
||||||
|
setSelectedIndex() {
|
||||||
|
var sizeIndex = this.availableSizes.findIndex((s) => s === this.selectedSize)
|
||||||
|
if (!isNaN(sizeIndex)) this.selectedSizeIndex = sizeIndex
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
@ -152,10 +152,16 @@ export default {
|
|||||||
if (this.$refs.streamContainer) this.$refs.streamContainer.streamError(id)
|
if (this.$refs.streamContainer) this.$refs.streamContainer.streamError(id)
|
||||||
},
|
},
|
||||||
audiobookAdded(audiobook) {
|
audiobookAdded(audiobook) {
|
||||||
this.$store.commit('audiobooks/addUpdate', audiobook)
|
// this.$store.commit('audiobooks/addUpdate', audiobook)
|
||||||
},
|
},
|
||||||
audiobookUpdated(audiobook) {
|
audiobookUpdated(audiobook) {
|
||||||
this.$store.commit('audiobooks/addUpdate', audiobook)
|
if (this.$store.state.selectedAudiobook && this.$store.state.selectedAudiobook.id === audiobook.id) {
|
||||||
|
console.log('Updating selected audiobook', audiobook)
|
||||||
|
this.$store.commit('setSelectedAudiobook', audiobook)
|
||||||
|
}
|
||||||
|
// Just triggers the listeners
|
||||||
|
this.$store.commit('audiobooks/audiobookUpdated', audiobook)
|
||||||
|
// this.$store.commit('audiobooks/addUpdate', audiobook)
|
||||||
},
|
},
|
||||||
audiobookRemoved(audiobook) {
|
audiobookRemoved(audiobook) {
|
||||||
if (this.$route.name.startsWith('audiobook')) {
|
if (this.$route.name.startsWith('audiobook')) {
|
||||||
@ -163,16 +169,17 @@ export default {
|
|||||||
this.$router.replace(`/library/${this.$store.state.libraries.currentLibraryId}`)
|
this.$router.replace(`/library/${this.$store.state.libraries.currentLibraryId}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.$store.commit('audiobooks/remove', audiobook)
|
// this.$store.commit('audiobooks/remove', audiobook)
|
||||||
},
|
},
|
||||||
audiobooksAdded(audiobooks) {
|
audiobooksAdded(audiobooks) {
|
||||||
audiobooks.forEach((ab) => {
|
// audiobooks.forEach((ab) => {
|
||||||
this.$store.commit('audiobooks/addUpdate', ab)
|
// this.$store.commit('audiobooks/addUpdate', ab)
|
||||||
})
|
// })
|
||||||
},
|
},
|
||||||
audiobooksUpdated(audiobooks) {
|
audiobooksUpdated(audiobooks) {
|
||||||
audiobooks.forEach((ab) => {
|
audiobooks.forEach((ab) => {
|
||||||
this.$store.commit('audiobooks/addUpdate', ab)
|
this.audiobookUpdated(ab)
|
||||||
|
// this.$store.commit('audiobooks/addUpdate', ab)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
libraryAdded(library) {
|
libraryAdded(library) {
|
||||||
@ -244,7 +251,6 @@ export default {
|
|||||||
this.$store.commit('users/updateUser', user)
|
this.$store.commit('users/updateUser', user)
|
||||||
},
|
},
|
||||||
currentUserAudiobookUpdate(payload) {
|
currentUserAudiobookUpdate(payload) {
|
||||||
// console.log('Received user audiobook update', payload)
|
|
||||||
this.$store.commit('user/updateUserAudiobook', payload)
|
this.$store.commit('user/updateUserAudiobook', payload)
|
||||||
},
|
},
|
||||||
collectionAdded(collection) {
|
collectionAdded(collection) {
|
||||||
@ -266,11 +272,11 @@ export default {
|
|||||||
return console.error('Invalid download object', download)
|
return console.error('Invalid download object', download)
|
||||||
}
|
}
|
||||||
|
|
||||||
var audiobook = this.$store.getters['audiobooks/getAudiobook'](download.audiobookId)
|
// var audiobook = this.$store.getters['audiobooks/getAudiobook'](download.audiobookId)
|
||||||
if (!audiobook) {
|
// if (!audiobook) {
|
||||||
return console.error('Audiobook not found for download', download)
|
// return console.error('Audiobook not found for download', download)
|
||||||
}
|
// }
|
||||||
this.$store.commit('showEditModalOnTab', { audiobook, tab: 'download' })
|
// this.$store.commit('showEditModalOnTab', { audiobook, tab: 'download' })
|
||||||
},
|
},
|
||||||
downloadStarted(download) {
|
downloadStarted(download) {
|
||||||
download.status = this.$constants.DownloadStatus.PENDING
|
download.status = this.$constants.DownloadStatus.PENDING
|
||||||
@ -339,6 +345,7 @@ export default {
|
|||||||
reconnection: true
|
reconnection: true
|
||||||
})
|
})
|
||||||
this.$root.socket = this.socket
|
this.$root.socket = this.socket
|
||||||
|
console.log('Socket initialized')
|
||||||
|
|
||||||
this.socket.on('connect', this.connect)
|
this.socket.on('connect', this.connect)
|
||||||
this.socket.on('connect_error', this.connectError)
|
this.socket.on('connect_error', this.connectError)
|
||||||
@ -471,6 +478,7 @@ export default {
|
|||||||
if (this.$store.getters['getNumAudiobooksSelected'] && name === 'Escape') {
|
if (this.$store.getters['getNumAudiobooksSelected'] && name === 'Escape') {
|
||||||
// ESCAPE key cancels batch selection
|
// ESCAPE key cancels batch selection
|
||||||
this.$store.commit('setSelectedAudiobooks', [])
|
this.$store.commit('setSelectedAudiobooks', [])
|
||||||
|
this.$eventBus.$emit('bookshelf-clear-selection')
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -482,10 +490,11 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
beforeMount() {
|
||||||
|
this.initializeSocket()
|
||||||
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
window.addEventListener('keydown', this.keyDown)
|
window.addEventListener('keydown', this.keyDown)
|
||||||
|
|
||||||
this.initializeSocket()
|
|
||||||
this.$store.dispatch('libraries/load')
|
this.$store.dispatch('libraries/load')
|
||||||
|
|
||||||
// If experimental features set in local storage
|
// If experimental features set in local storage
|
||||||
|
@ -51,7 +51,8 @@ export default {
|
|||||||
var instance = new ComponentClass({
|
var instance = new ComponentClass({
|
||||||
propsData: {
|
propsData: {
|
||||||
index: index,
|
index: index,
|
||||||
width: this.entityWidth
|
width: this.entityWidth,
|
||||||
|
showVolumeNumber: this.entityName === 'series-books'
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.$on('edit', (entity) => {
|
this.$on('edit', (entity) => {
|
||||||
@ -73,7 +74,7 @@ export default {
|
|||||||
}
|
}
|
||||||
if (this.isSelectionMode) {
|
if (this.isSelectionMode) {
|
||||||
instance.setSelectionMode(true)
|
instance.setSelectionMode(true)
|
||||||
if (this.selectedAudiobooks.includes(instance.audiobookId) || this.isSelectAll) {
|
if (instance.audiobookId && this.selectedAudiobooks.includes(instance.audiobookId) || this.isSelectAll) {
|
||||||
instance.selected = true
|
instance.selected = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -95,7 +95,7 @@
|
|||||||
<span v-show="!streaming" class="material-icons -ml-2 pr-1 text-white">play_arrow</span>
|
<span v-show="!streaming" class="material-icons -ml-2 pr-1 text-white">play_arrow</span>
|
||||||
{{ streaming ? 'Streaming' : 'Play' }}
|
{{ streaming ? 'Streaming' : 'Play' }}
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
<ui-btn v-else-if="isMissing || isIncomplete" color="error" :padding-x="4" small class="flex items-center h-9 mr-2">
|
<ui-btn v-else-if="isMissing || isInvalid" color="error" :padding-x="4" small class="flex items-center h-9 mr-2">
|
||||||
<span v-show="!streaming" class="material-icons -ml-2 pr-1 text-white">error</span>
|
<span v-show="!streaming" class="material-icons -ml-2 pr-1 text-white">error</span>
|
||||||
{{ isMissing ? 'Missing' : 'Incomplete' }}
|
{{ isMissing ? 'Missing' : 'Incomplete' }}
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
@ -169,7 +169,6 @@ export default {
|
|||||||
console.error('No audiobook...', params.id)
|
console.error('No audiobook...', params.id)
|
||||||
return redirect('/')
|
return redirect('/')
|
||||||
}
|
}
|
||||||
store.commit('audiobooks/addUpdate', audiobook)
|
|
||||||
return {
|
return {
|
||||||
audiobook
|
audiobook
|
||||||
}
|
}
|
||||||
@ -234,11 +233,11 @@ export default {
|
|||||||
isMissing() {
|
isMissing() {
|
||||||
return this.audiobook.isMissing
|
return this.audiobook.isMissing
|
||||||
},
|
},
|
||||||
isIncomplete() {
|
isInvalid() {
|
||||||
return this.audiobook.isIncomplete
|
return this.audiobook.isInvalid
|
||||||
},
|
},
|
||||||
showPlayButton() {
|
showPlayButton() {
|
||||||
return !this.isMissing && !this.isIncomplete && this.tracks.length
|
return !this.isMissing && !this.isInvalid && this.tracks.length
|
||||||
},
|
},
|
||||||
missingParts() {
|
missingParts() {
|
||||||
return this.audiobook.missingParts || []
|
return this.audiobook.missingParts || []
|
||||||
@ -458,8 +457,8 @@ export default {
|
|||||||
window.addEventListener('resize', this.resize)
|
window.addEventListener('resize', this.resize)
|
||||||
this.$store.commit('audiobooks/addListener', { id: 'audiobook', audiobookId: this.audiobookId, meth: this.audiobookUpdated })
|
this.$store.commit('audiobooks/addListener', { id: 'audiobook', audiobookId: this.audiobookId, meth: this.audiobookUpdated })
|
||||||
|
|
||||||
// If a library has not yet been loaded, use this audiobooks library id as the current
|
// use this audiobooks library id as the current
|
||||||
if (!this.$store.state.audiobooks.loadedLibraryId && this.libraryId) {
|
if (this.libraryId) {
|
||||||
this.$store.commit('libraries/setCurrentLibrary', this.libraryId)
|
this.$store.commit('libraries/setCurrentLibrary', this.libraryId)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -62,11 +62,15 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
asyncData({ store, redirect }) {
|
async asyncData({ store, redirect, app }) {
|
||||||
if (!store.state.selectedAudiobooks.length) {
|
if (!store.state.selectedAudiobooks.length) {
|
||||||
return redirect('/')
|
return redirect('/')
|
||||||
}
|
}
|
||||||
var audiobooks = store.state.audiobooks.audiobooks.filter((ab) => store.state.selectedAudiobooks.includes(ab.id))
|
var audiobooks = await app.$axios.$post(`/api/books/batch/get`, { books: store.state.selectedAudiobooks }).catch((error) => {
|
||||||
|
var errorMsg = error.response.data || 'Failed to get audiobooks'
|
||||||
|
console.error(errorMsg, error)
|
||||||
|
return []
|
||||||
|
})
|
||||||
return {
|
return {
|
||||||
audiobooks
|
audiobooks
|
||||||
}
|
}
|
||||||
@ -85,24 +89,27 @@ export default {
|
|||||||
streamAudiobook() {
|
streamAudiobook() {
|
||||||
return this.$store.state.streamAudiobook
|
return this.$store.state.streamAudiobook
|
||||||
},
|
},
|
||||||
genres() {
|
|
||||||
return this.$store.state.audiobooks.genres
|
|
||||||
},
|
|
||||||
genreItems() {
|
genreItems() {
|
||||||
return this.genres.concat(this.newGenreItems)
|
return this.genres.concat(this.newGenreItems)
|
||||||
},
|
},
|
||||||
tags() {
|
|
||||||
return this.$store.state.audiobooks.tags
|
|
||||||
},
|
|
||||||
tagItems() {
|
tagItems() {
|
||||||
return this.tags.concat(this.newTagItems)
|
return this.tags.concat(this.newTagItems)
|
||||||
},
|
},
|
||||||
series() {
|
|
||||||
return this.$store.state.audiobooks.series
|
|
||||||
},
|
|
||||||
seriesItems() {
|
seriesItems() {
|
||||||
return [...this.series, ...this.newSeriesItems]
|
return [...this.series, ...this.newSeriesItems]
|
||||||
},
|
},
|
||||||
|
genres() {
|
||||||
|
return this.filterData.genres || []
|
||||||
|
},
|
||||||
|
tags() {
|
||||||
|
return this.filterData.tags || []
|
||||||
|
},
|
||||||
|
series() {
|
||||||
|
return this.filterData.series || []
|
||||||
|
},
|
||||||
|
filterData() {
|
||||||
|
return this.$store.state.libraries.filterData || {}
|
||||||
|
},
|
||||||
currentLibraryId() {
|
currentLibraryId() {
|
||||||
return this.$store.state.libraries.currentLibraryId
|
return this.$store.state.libraries.currentLibraryId
|
||||||
}
|
}
|
||||||
@ -174,7 +181,7 @@ export default {
|
|||||||
this.isProcessing = false
|
this.isProcessing = false
|
||||||
if (data.updates) {
|
if (data.updates) {
|
||||||
this.$toast.success(`Successfully updated ${data.updates} audiobooks`)
|
this.$toast.success(`Successfully updated ${data.updates} audiobooks`)
|
||||||
this.$router.replace(`/library/${this.currentLibraryId}`)
|
this.$router.replace(`/library/${this.currentLibraryId}/bookshelf`)
|
||||||
} else {
|
} else {
|
||||||
this.$toast.warning('No updates were necessary')
|
this.$toast.warning('No updates were necessary')
|
||||||
}
|
}
|
||||||
|
@ -83,7 +83,7 @@ export default {
|
|||||||
},
|
},
|
||||||
playableBooks() {
|
playableBooks() {
|
||||||
return this.bookItems.filter((book) => {
|
return this.bookItems.filter((book) => {
|
||||||
return !book.isMissing && !book.isIncomplete && book.numTracks
|
return !book.isMissing && !book.isInvalid && book.numTracks
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
streaming() {
|
streaming() {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<stats-preview-icons :listening-stats="listeningStats" />
|
<stats-preview-icons :listening-stats="listeningStats" :library-stats="libraryStats" />
|
||||||
|
|
||||||
<div class="flex md:flex-row flex-wrap justify-between flex-col mt-12">
|
<div class="flex md:flex-row flex-wrap justify-between flex-col mt-12">
|
||||||
<div class="w-80 my-6 mx-auto">
|
<div class="w-80 my-6 mx-auto">
|
||||||
@ -8,12 +8,12 @@
|
|||||||
<template v-for="genre in top5Genres">
|
<template v-for="genre in top5Genres">
|
||||||
<div :key="genre.genre" class="w-full py-2">
|
<div :key="genre.genre" class="w-full py-2">
|
||||||
<div class="flex items-end mb-1">
|
<div class="flex items-end mb-1">
|
||||||
<p class="text-2xl font-bold">{{ Math.round((100 * genre.count) / audiobooks.length) }} %</p>
|
<p class="text-2xl font-bold">{{ Math.round((100 * genre.count) / totalBooks) }} %</p>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<p class="text-base font-book text-white text-opacity-70">{{ genre.genre }}</p>
|
<p class="text-base font-book text-white text-opacity-70">{{ genre.genre }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full rounded-full h-3 bg-primary bg-opacity-50 overflow-hidden">
|
<div class="w-full rounded-full h-3 bg-primary bg-opacity-50 overflow-hidden">
|
||||||
<div class="bg-yellow-400 h-full rounded-full" :style="{ width: Math.round((100 * genre.count) / audiobooks.length) + '%' }" />
|
<div class="bg-yellow-400 h-full rounded-full" :style="{ width: Math.round((100 * genre.count) / totalBooks) + '%' }" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -43,49 +43,32 @@
|
|||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
listeningStats: null
|
listeningStats: null,
|
||||||
|
libraryStats: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
currentLibraryId(newVal, oldVal) {
|
||||||
|
if (newVal) {
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
audiobooks() {
|
|
||||||
return this.$store.state.audiobooks.audiobooks
|
|
||||||
},
|
|
||||||
user() {
|
user() {
|
||||||
return this.$store.state.user.user
|
return this.$store.state.user.user
|
||||||
},
|
},
|
||||||
|
totalBooks() {
|
||||||
|
return this.libraryStats ? this.libraryStats.totalBooks : 0
|
||||||
|
},
|
||||||
genresWithCount() {
|
genresWithCount() {
|
||||||
var genresMap = {}
|
return this.libraryStats ? this.libraryStats.genresWithCount : []
|
||||||
this.audiobooks.forEach((ab) => {
|
|
||||||
var genres = ab.book.genres || []
|
|
||||||
genres.forEach((genre) => {
|
|
||||||
if (genresMap[genre]) genresMap[genre].count++
|
|
||||||
else
|
|
||||||
genresMap[genre] = {
|
|
||||||
genre,
|
|
||||||
count: 1
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
var genres = Object.values(genresMap).sort((a, b) => b.count - a.count)
|
|
||||||
return genres
|
|
||||||
},
|
},
|
||||||
top5Genres() {
|
top5Genres() {
|
||||||
return this.genresWithCount.slice(0, 5)
|
return this.genresWithCount.slice(0, 5)
|
||||||
},
|
},
|
||||||
authorsWithCount() {
|
authorsWithCount() {
|
||||||
var authorsMap = {}
|
return this.libraryStats ? this.libraryStats.authorsWithCount : []
|
||||||
this.audiobooks.forEach((ab) => {
|
|
||||||
var authors = ab.book.authorFL ? ab.book.authorFL.split(', ') : []
|
|
||||||
authors.forEach((author) => {
|
|
||||||
if (authorsMap[author]) authorsMap[author].count++
|
|
||||||
else
|
|
||||||
authorsMap[author] = {
|
|
||||||
author,
|
|
||||||
count: 1
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
return Object.values(authorsMap).sort((a, b) => b.count - a.count)
|
|
||||||
},
|
},
|
||||||
mostUsedAuthorCount() {
|
mostUsedAuthorCount() {
|
||||||
if (!this.authorsWithCount.length) return 0
|
if (!this.authorsWithCount.length) return 0
|
||||||
@ -93,10 +76,19 @@ export default {
|
|||||||
},
|
},
|
||||||
top10Authors() {
|
top10Authors() {
|
||||||
return this.authorsWithCount.slice(0, 10)
|
return this.authorsWithCount.slice(0, 10)
|
||||||
|
},
|
||||||
|
currentLibraryId() {
|
||||||
|
return this.$store.state.libraries.currentLibraryId
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async init() {
|
async init() {
|
||||||
|
this.libraryStats = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/stats`).catch((err) => {
|
||||||
|
console.error('Failed to get library stats', err)
|
||||||
|
var errorMsg = err.response ? err.response.data || 'Unknown Error' : 'Unknown Error'
|
||||||
|
this.$toast.error(`Failed to get library stats: ${errorMsg}`)
|
||||||
|
})
|
||||||
|
console.log('lib stats', this.libraryStats)
|
||||||
this.listeningStats = await this.$axios.$get(`/api/me/listening-stats`).catch((err) => {
|
this.listeningStats = await this.$axios.$get(`/api/me/listening-stats`).catch((err) => {
|
||||||
console.error('Failed to load listening sesions', err)
|
console.error('Failed to load listening sesions', err)
|
||||||
return []
|
return []
|
||||||
@ -106,7 +98,6 @@ export default {
|
|||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.init()
|
this.init()
|
||||||
this.$store.dispatch('audiobooks/load')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
@ -1,13 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="page" :class="streamAudiobook ? 'streaming' : ''">
|
<div class="page" :class="streamAudiobook ? 'streaming' : ''"></div>
|
||||||
<!-- <div class="flex h-full">
|
|
||||||
<app-side-rail class="hidden md:block" />
|
|
||||||
<div class="flex-grow">
|
|
||||||
<app-book-shelf-toolbar is-home />
|
|
||||||
<app-book-shelf-categorized />
|
|
||||||
</div>
|
|
||||||
</div> -->
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
@ -3,9 +3,8 @@
|
|||||||
<div class="flex h-full">
|
<div class="flex h-full">
|
||||||
<app-side-rail class="hidden md:block" />
|
<app-side-rail class="hidden md:block" />
|
||||||
<div class="flex-grow">
|
<div class="flex-grow">
|
||||||
<app-book-shelf-toolbar :page="id || ''" :search-results="searchResults" :search-query="searchQuery" :selected-series.sync="selectedSeries" :view-mode.sync="viewMode" />
|
<app-book-shelf-toolbar :page="id || ''" :view-mode.sync="viewMode" />
|
||||||
<app-lazy-bookshelf :page="id || ''" />
|
<app-lazy-bookshelf :page="id || ''" :view-mode="viewMode" />
|
||||||
<!-- <app-book-shelf :page="id || ''" :search-results="searchResults" :search-query="searchQuery" :selected-series.sync="selectedSeries" :view-mode="viewMode" /> -->
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -25,39 +24,13 @@ export default {
|
|||||||
store.dispatch('user/updateUserSettings', { filterBy: query.filter })
|
store.dispatch('user/updateUserSettings', { filterBy: query.filter })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search page
|
// if (libraryPage === 'collections') {
|
||||||
var searchResults = {}
|
// store.dispatch('user/loadUserCollections')
|
||||||
var audiobookSearchResults = []
|
// }
|
||||||
var searchQuery = null
|
|
||||||
if (params.id === 'search' && query.query) {
|
|
||||||
searchQuery = query.query
|
|
||||||
|
|
||||||
searchResults = await app.$axios.$get(`/api/libraries/${libraryId}/search?q=${searchQuery}`).catch((error) => {
|
|
||||||
console.error('Search error', error)
|
|
||||||
return {}
|
|
||||||
})
|
|
||||||
audiobookSearchResults = searchResults.audiobooks || []
|
|
||||||
store.commit('audiobooks/setSearchResults', searchResults)
|
|
||||||
if (audiobookSearchResults.length) audiobookSearchResults.forEach((ab) => store.commit('audiobooks/addUpdate', ab.audiobook))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Series page
|
|
||||||
var selectedSeries = query.series ? app.$decode(query.series) : null
|
|
||||||
store.commit('audiobooks/setSelectedSeries', selectedSeries)
|
|
||||||
|
|
||||||
var libraryPage = params.id || ''
|
|
||||||
store.commit('audiobooks/setLibraryPage', libraryPage)
|
|
||||||
|
|
||||||
if (libraryPage === 'collections') {
|
|
||||||
store.dispatch('user/loadUserCollections')
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: libraryPage,
|
id: params.id || '',
|
||||||
libraryId,
|
libraryId
|
||||||
searchQuery,
|
|
||||||
searchResults,
|
|
||||||
selectedSeries
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@ -65,40 +38,11 @@ export default {
|
|||||||
viewMode: 'grid'
|
viewMode: 'grid'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
|
||||||
'$route.query'(newVal) {
|
|
||||||
if (this.id === 'search' && this.$route.query.query) {
|
|
||||||
if (this.$route.query.query !== this.searchQuery) {
|
|
||||||
this.newQuery()
|
|
||||||
}
|
|
||||||
} else if (this.id === 'series') {
|
|
||||||
if (this.selectedSeries && this.$route.query.series && this.$route.query.series !== this.$encode(this.selectedSeries)) {
|
|
||||||
// Series changed
|
|
||||||
this.selectedSeries = this.$decode(this.$route.query.series)
|
|
||||||
} else if (!this.selectedSeries && this.$route.query.series) {
|
|
||||||
// Series selected
|
|
||||||
this.selectedSeries = this.$decode(this.$route.query.series)
|
|
||||||
} else if (this.selectedSeries && !this.$route.query.series) {
|
|
||||||
// Series unselected
|
|
||||||
this.selectedSeries = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
streamAudiobook() {
|
streamAudiobook() {
|
||||||
return this.$store.state.streamAudiobook
|
return this.$store.state.streamAudiobook
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {}
|
||||||
async newQuery() {
|
|
||||||
var query = this.$route.query.query
|
|
||||||
this.searchResults = await this.$axios.$get(`/api/libraries/${this.libraryId}/search?q=${query}`).catch((error) => {
|
|
||||||
console.error('Search error', error)
|
|
||||||
return {}
|
|
||||||
})
|
|
||||||
this.searchQuery = query
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
@ -3,8 +3,8 @@
|
|||||||
<div class="flex h-full">
|
<div class="flex h-full">
|
||||||
<app-side-rail class="hidden md:block" />
|
<app-side-rail class="hidden md:block" />
|
||||||
<div class="flex-grow">
|
<div class="flex-grow">
|
||||||
<app-book-shelf-toolbar is-home />
|
<app-book-shelf-toolbar is-home page="search" :search-query="query" />
|
||||||
<app-book-shelf-categorized v-if="hasResults" 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">No Search results for "{{ query }}"</p>
|
<p class="text-xl text-center">No Search results for "{{ query }}"</p>
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
@ -20,6 +20,10 @@
|
|||||||
export default {
|
export default {
|
||||||
async asyncData({ store, params, redirect, query, app }) {
|
async asyncData({ store, params, redirect, query, app }) {
|
||||||
var libraryId = params.library
|
var libraryId = params.library
|
||||||
|
var library = await store.dispatch('libraries/fetch', libraryId)
|
||||||
|
if (!library) {
|
||||||
|
return redirect('/oops?message=Library not found')
|
||||||
|
}
|
||||||
var query = query.q
|
var query = query.q
|
||||||
var results = await app.$axios.$get(`/api/libraries/${libraryId}/search?q=${query}`).catch((error) => {
|
var results = await app.$axios.$get(`/api/libraries/${libraryId}/search?q=${query}`).catch((error) => {
|
||||||
console.error('Failed to search library', error)
|
console.error('Failed to search library', error)
|
||||||
@ -31,8 +35,8 @@ export default {
|
|||||||
series: results && results.series.length ? results.series : null,
|
series: results && results.series.length ? results.series : null,
|
||||||
tags: results && results.tags.length ? results.tags : null
|
tags: results && results.tags.length ? results.tags : null
|
||||||
}
|
}
|
||||||
console.log('SEARCH RESULTS', results)
|
|
||||||
return {
|
return {
|
||||||
|
libraryId,
|
||||||
results,
|
results,
|
||||||
query
|
query
|
||||||
}
|
}
|
||||||
@ -40,6 +44,14 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {}
|
return {}
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
'$route.query'(newVal, oldVal) {
|
||||||
|
if (newVal && newVal.q && newVal.q !== this.query) {
|
||||||
|
this.query = newVal.q
|
||||||
|
this.search()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
streamAudiobook() {
|
streamAudiobook() {
|
||||||
return this.$store.state.streamAudiobook
|
return this.$store.state.streamAudiobook
|
||||||
@ -49,6 +61,23 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
async search() {
|
||||||
|
var results = await this.$axios.$get(`/api/libraries/${this.libraryId}/search?q=${this.query}`).catch((error) => {
|
||||||
|
console.error('Failed to search library', error)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
this.results = {
|
||||||
|
audiobooks: results && results.audiobooks.length ? results.audiobooks : null,
|
||||||
|
authors: results && results.authors.length ? results.authors : null,
|
||||||
|
series: results && results.series.length ? results.series : null,
|
||||||
|
tags: results && results.tags.length ? results.tags : null
|
||||||
|
}
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.$refs.bookshelf) {
|
||||||
|
this.$refs.bookshelf.setShelvesFromSearch()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
async back() {
|
async back() {
|
||||||
var popped = await this.$store.dispatch('popRoute')
|
var popped = await this.$store.dispatch('popRoute')
|
||||||
if (popped) this.$store.commit('setIsRoutingBack', true)
|
if (popped) this.$store.commit('setIsRoutingBack', true)
|
||||||
|
38
client/pages/library/_library/series/_id.vue
Normal file
38
client/pages/library/_library/series/_id.vue
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<template>
|
||||||
|
<div class="page" :class="streamAudiobook ? 'streaming' : ''">
|
||||||
|
<div class="flex h-full">
|
||||||
|
<app-side-rail class="hidden md:block" />
|
||||||
|
<div class="flex-grow">
|
||||||
|
<app-book-shelf-toolbar :selected-series="series" />
|
||||||
|
<app-lazy-bookshelf page="series-books" :series-id="seriesId" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
async asyncData({ store, params, redirect, query, app }) {
|
||||||
|
var libraryId = params.library
|
||||||
|
var library = await store.dispatch('libraries/fetch', libraryId)
|
||||||
|
if (!library) {
|
||||||
|
return redirect('/oops?message=Library not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
series: app.$decode(params.id),
|
||||||
|
seriesId: params.id
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
streamAudiobook() {
|
||||||
|
return this.$store.state.streamAudiobook
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {},
|
||||||
|
beforeDestroy() {}
|
||||||
|
}
|
||||||
|
</script>
|
@ -180,4 +180,5 @@ export {
|
|||||||
}
|
}
|
||||||
export default ({ app }, inject) => {
|
export default ({ app }, inject) => {
|
||||||
app.$decode = decode
|
app.$decode = decode
|
||||||
|
app.$encode = encode
|
||||||
}
|
}
|
@ -1,159 +1,17 @@
|
|||||||
import { sort } from '@/assets/fastSort'
|
|
||||||
import { decode } from '@/plugins/init.client'
|
|
||||||
|
|
||||||
const STANDARD_GENRES = ['Adventure', 'Autobiography', 'Biography', 'Childrens', 'Comedy', 'Crime', 'Dystopian', 'Fantasy', 'Fiction', 'Health', 'History', 'Horror', 'Mystery', 'New Adult', 'Nonfiction', 'Philosophy', 'Politics', 'Religion', 'Romance', 'Sci-Fi', 'Self-Help', 'Short Story', 'Technology', 'Thriller', 'True Crime', 'Western', 'Young Adult']
|
const STANDARD_GENRES = ['Adventure', 'Autobiography', 'Biography', 'Childrens', 'Comedy', 'Crime', 'Dystopian', 'Fantasy', 'Fiction', 'Health', 'History', 'Horror', 'Mystery', 'New Adult', 'Nonfiction', 'Philosophy', 'Politics', 'Religion', 'Romance', 'Sci-Fi', 'Self-Help', 'Short Story', 'Technology', 'Thriller', 'True Crime', 'Western', 'Young Adult']
|
||||||
|
|
||||||
export const state = () => ({
|
export const state = () => ({
|
||||||
audiobooks: [],
|
audiobooks: [],
|
||||||
loadedLibraryId: '',
|
loadedLibraryId: '',
|
||||||
lastLoad: 0,
|
|
||||||
listeners: [],
|
listeners: [],
|
||||||
genres: [...STANDARD_GENRES],
|
genres: [...STANDARD_GENRES],
|
||||||
tags: [],
|
tags: [],
|
||||||
series: [],
|
series: [],
|
||||||
keywordFilter: null,
|
keywordFilter: null,
|
||||||
selectedSeries: null,
|
selectedSeries: null
|
||||||
libraryPage: null,
|
|
||||||
searchResults: {},
|
|
||||||
searchResultAudiobooks: []
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getters = {
|
export const getters = {
|
||||||
getAudiobook: (state) => id => {
|
|
||||||
return state.audiobooks.find(ab => ab.id === id)
|
|
||||||
},
|
|
||||||
getAudiobooksWithIssues: (state) => {
|
|
||||||
return state.audiobooks.filter(ab => {
|
|
||||||
return ab.hasMissingParts || ab.hasInvalidParts || ab.isMissing || ab.isIncomplete
|
|
||||||
})
|
|
||||||
},
|
|
||||||
getEntitiesShowing: (state, getters, rootState, rootGetters) => () => {
|
|
||||||
if (!state.libraryPage) {
|
|
||||||
return getters.getFiltered()
|
|
||||||
} else if (state.libraryPage === 'search') {
|
|
||||||
return state.searchResultAudiobooks
|
|
||||||
} else if (state.libraryPage === 'series') {
|
|
||||||
var series = getters.getSeriesGroups()
|
|
||||||
if (state.selectedSeries) {
|
|
||||||
var _series = series.find(__series => __series.name === state.selectedSeries)
|
|
||||||
if (!_series) return []
|
|
||||||
return _series.books || []
|
|
||||||
}
|
|
||||||
return series
|
|
||||||
}
|
|
||||||
return []
|
|
||||||
},
|
|
||||||
getFiltered: (state, getters, rootState, rootGetters) => () => {
|
|
||||||
var filtered = state.audiobooks
|
|
||||||
var settings = rootState.user.settings || {}
|
|
||||||
var filterBy = settings.filterBy || ''
|
|
||||||
|
|
||||||
var searchGroups = ['genres', 'tags', 'series', 'authors', 'progress', 'narrators']
|
|
||||||
var group = searchGroups.find(_group => filterBy.startsWith(_group + '.'))
|
|
||||||
if (group) {
|
|
||||||
var filterVal = filterBy.replace(`${group}.`, '')
|
|
||||||
var filter = decode(filterVal)
|
|
||||||
if (group === 'genres') filtered = filtered.filter(ab => ab.book && ab.book.genres.includes(filter))
|
|
||||||
else if (group === 'tags') filtered = filtered.filter(ab => ab.tags.includes(filter))
|
|
||||||
else if (group === 'series') {
|
|
||||||
if (filter === 'No Series') filtered = filtered.filter(ab => ab.book && !ab.book.series)
|
|
||||||
else filtered = filtered.filter(ab => ab.book && ab.book.series === filter)
|
|
||||||
}
|
|
||||||
else if (group === 'authors') filtered = filtered.filter(ab => ab.book && ab.book.authorFL && ab.book.authorFL.split(', ').includes(filter))
|
|
||||||
else if (group === 'narrators') filtered = filtered.filter(ab => ab.book && ab.book.narratorFL && ab.book.narratorFL.split(', ').includes(filter))
|
|
||||||
else if (group === 'progress') {
|
|
||||||
filtered = filtered.filter(ab => {
|
|
||||||
var userAudiobook = rootGetters['user/getUserAudiobook'](ab.id)
|
|
||||||
var isRead = userAudiobook && userAudiobook.isRead
|
|
||||||
if (filter === 'Read' && isRead) return true
|
|
||||||
if (filter === 'Unread' && !isRead) return true
|
|
||||||
if (filter === 'In Progress' && (userAudiobook && !userAudiobook.isRead && userAudiobook.progress > 0)) return true
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} else if (filterBy === 'issues') {
|
|
||||||
filtered = filtered.filter(ab => {
|
|
||||||
return ab.hasMissingParts || ab.hasInvalidParts || ab.isMissing || ab.isIncomplete
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.keywordFilter) {
|
|
||||||
const keywordFilterKeys = ['title', 'subtitle', 'author', 'series', 'narrator']
|
|
||||||
const keyworkFilter = state.keywordFilter.toLowerCase()
|
|
||||||
return filtered.filter(ab => {
|
|
||||||
if (!ab.book) return false
|
|
||||||
return !!keywordFilterKeys.find(key => (ab.book[key] && ab.book[key].toLowerCase().includes(keyworkFilter)))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return filtered
|
|
||||||
},
|
|
||||||
getFilteredAndSorted: (state, getters, rootState) => () => {
|
|
||||||
var settings = rootState.user.settings
|
|
||||||
var direction = settings.orderDesc ? 'desc' : 'asc'
|
|
||||||
|
|
||||||
var filtered = getters.getFiltered()
|
|
||||||
|
|
||||||
var orderByNumber = settings.orderBy === 'book.volumeNumber'
|
|
||||||
return sort(filtered)[direction]((ab) => {
|
|
||||||
// Supports dot notation strings i.e. "book.title"
|
|
||||||
var value = settings.orderBy.split('.').reduce((a, b) => a[b], ab)
|
|
||||||
if (orderByNumber && !isNaN(value)) return Number(value)
|
|
||||||
return value
|
|
||||||
})
|
|
||||||
},
|
|
||||||
getSeriesGroups: (state, getters, rootState) => () => {
|
|
||||||
var series = {}
|
|
||||||
state.audiobooks.forEach((audiobook) => {
|
|
||||||
if (audiobook.book && audiobook.book.series) {
|
|
||||||
if (series[audiobook.book.series]) {
|
|
||||||
var bookLastUpdate = audiobook.book.lastUpdate
|
|
||||||
if (bookLastUpdate > series[audiobook.book.series].lastUpdate) series[audiobook.book.series].lastUpdate = bookLastUpdate
|
|
||||||
series[audiobook.book.series].books.push(audiobook)
|
|
||||||
} else {
|
|
||||||
series[audiobook.book.series] = {
|
|
||||||
type: 'series',
|
|
||||||
name: audiobook.book.series || '',
|
|
||||||
books: [audiobook],
|
|
||||||
lastUpdate: audiobook.book.lastUpdate
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
var seriesArray = Object.values(series).map((_series) => {
|
|
||||||
_series.books = sort(_series.books)['asc']((ab) => {
|
|
||||||
return ab.book && ab.book.volumeNumber && !isNaN(ab.book.volumeNumber) ? Number(ab.book.volumeNumber) : null
|
|
||||||
})
|
|
||||||
return _series
|
|
||||||
})
|
|
||||||
if (state.keywordFilter) {
|
|
||||||
const keywordFilter = state.keywordFilter.toLowerCase()
|
|
||||||
return seriesArray.filter((_series) => _series.name.toLowerCase().includes(keywordFilter))
|
|
||||||
}
|
|
||||||
return seriesArray
|
|
||||||
},
|
|
||||||
getUniqueAuthors: (state) => {
|
|
||||||
var abAuthors = []
|
|
||||||
state.audiobooks.forEach((ab) => {
|
|
||||||
if (ab.book && ab.book.authorFL) {
|
|
||||||
abAuthors = abAuthors.concat(ab.book.authorFL.split(', '))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return [...new Set(abAuthors)].sort((a, b) => a.toLowerCase() < b.toLowerCase() ? -1 : 1)
|
|
||||||
},
|
|
||||||
getUniqueNarrators: (state) => {
|
|
||||||
var narrators = []
|
|
||||||
state.audiobooks.forEach((ab) => {
|
|
||||||
if (ab.book && ab.book.narratorFL) {
|
|
||||||
narrators = narrators.concat(ab.book.narratorFL.split(', '))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return [...new Set(narrators)].sort((a, b) => a.toLowerCase() < b.toLowerCase() ? -1 : 1)
|
|
||||||
},
|
|
||||||
getGenresUsed: (state) => {
|
|
||||||
var _genres = []
|
|
||||||
state.audiobooks.filter(ab => !!(ab.book && ab.book.genres)).forEach(ab => _genres = _genres.concat(ab.book.genres))
|
|
||||||
return [...new Set(_genres)].sort((a, b) => a.toLowerCase() < b.toLowerCase() ? -1 : 1)
|
|
||||||
},
|
|
||||||
getBookCoverSrc: (state, getters, rootState, rootGetters) => (bookItem, placeholder = '/book_placeholder.jpg') => {
|
getBookCoverSrc: (state, getters, rootState, rootGetters) => (bookItem, placeholder = '/book_placeholder.jpg') => {
|
||||||
if (!bookItem) return placeholder
|
if (!bookItem) return placeholder
|
||||||
var book = bookItem.book
|
var book = bookItem.book
|
||||||
@ -192,60 +50,16 @@ export const getters = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
// Return true if calling load
|
|
||||||
load({ state, commit, rootState }) {
|
|
||||||
if (!rootState.user || !rootState.user.user) {
|
|
||||||
console.error('audiobooks/load - User not set')
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
var currentLibraryId = rootState.libraries.currentLibraryId
|
|
||||||
|
|
||||||
if (currentLibraryId === state.loadedLibraryId) {
|
|
||||||
// Don't load again if already loaded in the last 5 minutes
|
|
||||||
var lastLoadDiff = Date.now() - state.lastLoad
|
|
||||||
if (lastLoadDiff < 5 * 60 * 1000) {
|
|
||||||
// Already up to date
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
commit('setLoadedLibrary', currentLibraryId)
|
|
||||||
|
|
||||||
this.$axios
|
|
||||||
.$get(`/api/libraries/${currentLibraryId}/books`)
|
|
||||||
.then((data) => {
|
|
||||||
commit('set', data)
|
|
||||||
commit('setLastLoad')
|
|
||||||
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error('Failed', error)
|
|
||||||
commit('set', [])
|
|
||||||
})
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const mutations = {
|
export const mutations = {
|
||||||
setLoadedLibrary(state, val) {
|
|
||||||
state.loadedLibraryId = val
|
|
||||||
},
|
|
||||||
setLastLoad(state) {
|
|
||||||
state.lastLoad = Date.now()
|
|
||||||
},
|
|
||||||
setKeywordFilter(state, val) {
|
setKeywordFilter(state, val) {
|
||||||
state.keywordFilter = val
|
state.keywordFilter = val
|
||||||
},
|
},
|
||||||
setSelectedSeries(state, val) {
|
setSelectedSeries(state, val) {
|
||||||
state.selectedSeries = val
|
state.selectedSeries = val
|
||||||
},
|
},
|
||||||
setLibraryPage(state, val) {
|
|
||||||
state.libraryPage = val
|
|
||||||
},
|
|
||||||
setSearchResults(state, val) {
|
|
||||||
state.searchResults = val
|
|
||||||
state.searchResultAudiobooks = val && val.audiobooks ? val.audiobooks.map(ab => ab.audiobook) : []
|
|
||||||
},
|
|
||||||
set(state, audiobooks) {
|
set(state, audiobooks) {
|
||||||
// GENRES
|
// GENRES
|
||||||
var genres = [...state.genres]
|
var genres = [...state.genres]
|
||||||
@ -382,5 +196,12 @@ export const mutations = {
|
|||||||
},
|
},
|
||||||
removeListener(state, listenerId) {
|
removeListener(state, listenerId) {
|
||||||
state.listeners = state.listeners.filter(l => l.id !== listenerId)
|
state.listeners = state.listeners.filter(l => l.id !== listenerId)
|
||||||
|
},
|
||||||
|
audiobookUpdated(state, audiobook) {
|
||||||
|
state.listeners.forEach((listener) => {
|
||||||
|
if (!listener.audiobookId || listener.audiobookId === audiobook.id) {
|
||||||
|
listener.meth()
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -4,6 +4,7 @@ export const state = () => ({
|
|||||||
listeners: [],
|
listeners: [],
|
||||||
currentLibraryId: 'main',
|
currentLibraryId: 'main',
|
||||||
folders: [],
|
folders: [],
|
||||||
|
issues: 0,
|
||||||
folderLastUpdate: 0,
|
folderLastUpdate: 0,
|
||||||
filterData: null
|
filterData: null
|
||||||
})
|
})
|
||||||
@ -53,7 +54,6 @@ export const actions = {
|
|||||||
console.warn('Access not allowed to library')
|
console.warn('Access not allowed to library')
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// var library = state.libraries.find(lib => lib.id === libraryId)
|
// var library = state.libraries.find(lib => lib.id === libraryId)
|
||||||
// if (library) {
|
// if (library) {
|
||||||
// commit('setCurrentLibrary', libraryId)
|
// commit('setCurrentLibrary', libraryId)
|
||||||
@ -65,7 +65,9 @@ export const actions = {
|
|||||||
.then((data) => {
|
.then((data) => {
|
||||||
var library = data.library
|
var library = data.library
|
||||||
var filterData = data.filterdata
|
var filterData = data.filterdata
|
||||||
|
var issues = data.issues || 0
|
||||||
commit('addUpdate', library)
|
commit('addUpdate', library)
|
||||||
|
commit('setLibraryIssues', issues)
|
||||||
commit('setLibraryFilterData', filterData)
|
commit('setLibraryFilterData', filterData)
|
||||||
commit('setCurrentLibrary', libraryId)
|
commit('setCurrentLibrary', libraryId)
|
||||||
return data
|
return data
|
||||||
@ -129,6 +131,9 @@ export const mutations = {
|
|||||||
setLastLoad(state) {
|
setLastLoad(state) {
|
||||||
state.lastLoad = Date.now()
|
state.lastLoad = Date.now()
|
||||||
},
|
},
|
||||||
|
setLibraryIssues(state, val) {
|
||||||
|
state.issues = val
|
||||||
|
},
|
||||||
setCurrentLibrary(state, val) {
|
setCurrentLibrary(state, val) {
|
||||||
state.currentLibraryId = val
|
state.currentLibraryId = val
|
||||||
},
|
},
|
||||||
|
@ -51,4 +51,4 @@
|
|||||||
"xml2js": "^0.4.23"
|
"xml2js": "^0.4.23"
|
||||||
},
|
},
|
||||||
"devDependencies": {}
|
"devDependencies": {}
|
||||||
}
|
}
|
@ -55,11 +55,13 @@ class ApiController {
|
|||||||
|
|
||||||
this.router.get('/libraries/:id/books/all', LibraryController.middleware.bind(this), LibraryController.getBooksForLibrary2.bind(this))
|
this.router.get('/libraries/:id/books/all', LibraryController.middleware.bind(this), LibraryController.getBooksForLibrary2.bind(this))
|
||||||
this.router.get('/libraries/:id/books', LibraryController.middleware.bind(this), LibraryController.getBooksForLibrary.bind(this))
|
this.router.get('/libraries/:id/books', LibraryController.middleware.bind(this), LibraryController.getBooksForLibrary.bind(this))
|
||||||
this.router.get('/libraries/:id/series', LibraryController.middleware.bind(this), LibraryController.getSeriesForLibrary.bind(this))
|
this.router.get('/libraries/:id/series', LibraryController.middleware.bind(this), LibraryController.getAllSeriesForLibrary.bind(this))
|
||||||
|
this.router.get('/libraries/:id/series/:series', LibraryController.middleware.bind(this), LibraryController.getSeriesForLibrary.bind(this))
|
||||||
this.router.get('/libraries/:id/collections', LibraryController.middleware.bind(this), LibraryController.getCollectionsForLibrary.bind(this))
|
this.router.get('/libraries/:id/collections', LibraryController.middleware.bind(this), LibraryController.getCollectionsForLibrary.bind(this))
|
||||||
this.router.get('/libraries/:id/categories', LibraryController.middleware.bind(this), LibraryController.getLibraryCategories.bind(this))
|
this.router.get('/libraries/:id/categories', LibraryController.middleware.bind(this), LibraryController.getLibraryCategories.bind(this))
|
||||||
this.router.get('/libraries/:id/filters', LibraryController.middleware.bind(this), LibraryController.getLibraryFilters.bind(this))
|
this.router.get('/libraries/:id/filters', LibraryController.middleware.bind(this), LibraryController.getLibraryFilters.bind(this))
|
||||||
this.router.get('/libraries/:id/search', LibraryController.middleware.bind(this), LibraryController.search.bind(this))
|
this.router.get('/libraries/:id/search', LibraryController.middleware.bind(this), LibraryController.search.bind(this))
|
||||||
|
this.router.get('/libraries/:id/stats', LibraryController.middleware.bind(this), LibraryController.stats.bind(this))
|
||||||
this.router.patch('/libraries/order', LibraryController.reorder.bind(this))
|
this.router.patch('/libraries/order', LibraryController.reorder.bind(this))
|
||||||
|
|
||||||
// TEMP: Support old syntax for mobile app
|
// TEMP: Support old syntax for mobile app
|
||||||
@ -78,6 +80,7 @@ class ApiController {
|
|||||||
this.router.delete('/books/all', BookController.deleteAll.bind(this))
|
this.router.delete('/books/all', BookController.deleteAll.bind(this))
|
||||||
this.router.post('/books/batch/delete', BookController.batchDelete.bind(this))
|
this.router.post('/books/batch/delete', BookController.batchDelete.bind(this))
|
||||||
this.router.post('/books/batch/update', BookController.batchUpdate.bind(this))
|
this.router.post('/books/batch/update', BookController.batchUpdate.bind(this))
|
||||||
|
this.router.post('/books/batch/get', BookController.batchGet.bind(this))
|
||||||
this.router.patch('/books/:id/tracks', BookController.updateTracks.bind(this))
|
this.router.patch('/books/:id/tracks', BookController.updateTracks.bind(this))
|
||||||
this.router.get('/books/:id/stream', BookController.openStream.bind(this))
|
this.router.get('/books/:id/stream', BookController.openStream.bind(this))
|
||||||
this.router.post('/books/:id/cover', BookController.uploadCover.bind(this))
|
this.router.post('/books/:id/cover', BookController.uploadCover.bind(this))
|
||||||
@ -493,105 +496,5 @@ class ApiController {
|
|||||||
})
|
})
|
||||||
return listeningStats
|
return listeningStats
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// decode(text) {
|
|
||||||
// return Buffer.from(decodeURIComponent(text), 'base64').toString()
|
|
||||||
// }
|
|
||||||
|
|
||||||
// getFiltered(audiobooks, filterBy, user) {
|
|
||||||
// var filtered = audiobooks
|
|
||||||
|
|
||||||
// var searchGroups = ['genres', 'tags', 'series', 'authors', 'progress', 'narrators']
|
|
||||||
// var group = searchGroups.find(_group => filterBy.startsWith(_group + '.'))
|
|
||||||
// if (group) {
|
|
||||||
// var filterVal = filterBy.replace(`${group}.`, '')
|
|
||||||
// var filter = this.decode(filterVal)
|
|
||||||
// if (group === 'genres') filtered = filtered.filter(ab => ab.book && ab.book.genres.includes(filter))
|
|
||||||
// else if (group === 'tags') filtered = filtered.filter(ab => ab.tags.includes(filter))
|
|
||||||
// else if (group === 'series') {
|
|
||||||
// if (filter === 'No Series') filtered = filtered.filter(ab => ab.book && !ab.book.series)
|
|
||||||
// else filtered = filtered.filter(ab => ab.book && ab.book.series === filter)
|
|
||||||
// }
|
|
||||||
// else if (group === 'authors') filtered = filtered.filter(ab => ab.book && ab.book.authorFL && ab.book.authorFL.split(', ').includes(filter))
|
|
||||||
// else if (group === 'narrators') filtered = filtered.filter(ab => ab.book && ab.book.narratorFL && ab.book.narratorFL.split(', ').includes(filter))
|
|
||||||
// else if (group === 'progress') {
|
|
||||||
// filtered = filtered.filter(ab => {
|
|
||||||
// var userAudiobook = user.getAudiobookJSON(ab.id)
|
|
||||||
// var isRead = userAudiobook && userAudiobook.isRead
|
|
||||||
// if (filter === 'Read' && isRead) return true
|
|
||||||
// if (filter === 'Unread' && !isRead) return true
|
|
||||||
// if (filter === 'In Progress' && (userAudiobook && !userAudiobook.isRead && userAudiobook.progress > 0)) return true
|
|
||||||
// return false
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
// } else if (filterBy === 'issues') {
|
|
||||||
// filtered = filtered.filter(ab => {
|
|
||||||
// return ab.hasMissingParts || ab.hasInvalidParts || ab.isMissing || ab.isIncomplete
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return filtered
|
|
||||||
// }
|
|
||||||
|
|
||||||
// getDistinctFilterData(audiobooks) {
|
|
||||||
// var data = {
|
|
||||||
// authors: [],
|
|
||||||
// genres: [],
|
|
||||||
// tags: [],
|
|
||||||
// series: [],
|
|
||||||
// narrators: []
|
|
||||||
// }
|
|
||||||
// audiobooks.forEach((ab) => {
|
|
||||||
// if (ab.book._authorsList.length) {
|
|
||||||
// ab.book._authorsList.forEach((author) => {
|
|
||||||
// if (author && !data.authors.includes(author)) data.authors.push(author)
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
// if (ab.book._genres.length) {
|
|
||||||
// ab.book._genres.forEach((genre) => {
|
|
||||||
// if (genre && !data.genres.includes(genre)) data.genres.push(genre)
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
// if (ab.tags.length) {
|
|
||||||
// ab.tags.forEach((tag) => {
|
|
||||||
// if (tag && !data.tags.includes(tag)) data.tags.push(tag)
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
// if (ab.book._series && !data.series.includes(ab.book._series)) data.series.push(ab.book._series)
|
|
||||||
// if (ab.book._narratorsList.length) {
|
|
||||||
// ab.book._narratorsList.forEach((narrator) => {
|
|
||||||
// if (narrator && !data.narrators.includes(narrator)) data.narrators.push(narrator)
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// return data
|
|
||||||
// }
|
|
||||||
|
|
||||||
// getBooksMostRecentlyRead(user, books, limit) {
|
|
||||||
// var booksWithProgress = books.map(book => {
|
|
||||||
// return {
|
|
||||||
// userAudiobook: user.getAudiobookJSON(book.id),
|
|
||||||
// book
|
|
||||||
// }
|
|
||||||
// }).filter((data) => data.userAudiobook && !data.userAudiobook.isRead)
|
|
||||||
// booksWithProgress.sort((a, b) => {
|
|
||||||
// return b.userAudiobook.lastUpdate - a.userAudiobook.lastUpdate
|
|
||||||
// })
|
|
||||||
// return booksWithProgress.map(b => b.book).slice(0, limit)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// getBooksMostRecentlyAdded(user, books, limit) {
|
|
||||||
// var booksWithProgress = books.map(book => {
|
|
||||||
// return {
|
|
||||||
// userAudiobook: user.getAudiobookJSON(book.id),
|
|
||||||
// book
|
|
||||||
// }
|
|
||||||
// }).filter((data) => data.userAudiobook && !data.userAudiobook.isRead)
|
|
||||||
// booksWithProgress.sort((a, b) => {
|
|
||||||
// return b.userAudiobook.lastUpdate - a.userAudiobook.lastUpdate
|
|
||||||
// })
|
|
||||||
// return booksWithProgress.map(b => b.book).slice(0, limit)
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
module.exports = ApiController
|
module.exports = ApiController
|
@ -149,13 +149,13 @@ class Scanner {
|
|||||||
if (!audiobookData.audioFiles.length && !ebookFiles.length) {
|
if (!audiobookData.audioFiles.length && !ebookFiles.length) {
|
||||||
Logger.error(`[Scanner] "${existingAudiobook.title}" no valid book files found - marking as incomplete`)
|
Logger.error(`[Scanner] "${existingAudiobook.title}" no valid book files found - marking as incomplete`)
|
||||||
existingAudiobook.setLastScan(version)
|
existingAudiobook.setLastScan(version)
|
||||||
existingAudiobook.isIncomplete = true
|
existingAudiobook.isInvalid = true
|
||||||
await this.db.updateAudiobook(existingAudiobook)
|
await this.db.updateAudiobook(existingAudiobook)
|
||||||
this.emitter('audiobook_updated', existingAudiobook.toJSONMinified())
|
this.emitter('audiobook_updated', existingAudiobook.toJSONMinified())
|
||||||
return ScanResult.UPDATED
|
return ScanResult.UPDATED
|
||||||
} else if (existingAudiobook.isIncomplete) { // Was incomplete but now is not
|
} else if (existingAudiobook.isInvalid) { // Was incomplete but now is not
|
||||||
Logger.info(`[Scanner] "${existingAudiobook.title}" was incomplete but now has book files`)
|
Logger.info(`[Scanner] "${existingAudiobook.title}" was incomplete but now has book files`)
|
||||||
existingAudiobook.isIncomplete = false
|
existingAudiobook.isInvalid = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for audio files that were removed
|
// Check for audio files that were removed
|
||||||
@ -241,7 +241,7 @@ class Scanner {
|
|||||||
if (!existingAudiobook.tracks.length && !ebookFiles.length) {
|
if (!existingAudiobook.tracks.length && !ebookFiles.length) {
|
||||||
Logger.error(`[Scanner] "${existingAudiobook.title}" no valid book files found after update - marking as incomplete`)
|
Logger.error(`[Scanner] "${existingAudiobook.title}" no valid book files found after update - marking as incomplete`)
|
||||||
existingAudiobook.setLastScan(version)
|
existingAudiobook.setLastScan(version)
|
||||||
existingAudiobook.isIncomplete = true
|
existingAudiobook.isInvalid = true
|
||||||
await this.db.updateAudiobook(existingAudiobook)
|
await this.db.updateAudiobook(existingAudiobook)
|
||||||
this.emitter('audiobook_updated', existingAudiobook.toJSONMinified())
|
this.emitter('audiobook_updated', existingAudiobook.toJSONMinified())
|
||||||
return ScanResult.UPDATED
|
return ScanResult.UPDATED
|
||||||
|
@ -240,7 +240,6 @@ class Server {
|
|||||||
methods: ["GET", "POST"]
|
methods: ["GET", "POST"]
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
this.io.on('connection', (socket) => {
|
this.io.on('connection', (socket) => {
|
||||||
this.clients[socket.id] = {
|
this.clients[socket.id] = {
|
||||||
id: socket.id,
|
id: socket.id,
|
||||||
|
@ -124,8 +124,6 @@ class FolderWatcher extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
addFileUpdate(libraryId, path, type) {
|
addFileUpdate(libraryId, path, type) {
|
||||||
console.log('add file update', libraryId, path, type)
|
|
||||||
return
|
|
||||||
path = path.replace(/\\/g, '/')
|
path = path.replace(/\\/g, '/')
|
||||||
if (this.pendingFilePaths.includes(path)) return
|
if (this.pendingFilePaths.includes(path)) return
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@ class BookController {
|
|||||||
if (hasUpdates) {
|
if (hasUpdates) {
|
||||||
await this.db.updateAudiobook(audiobook)
|
await this.db.updateAudiobook(audiobook)
|
||||||
}
|
}
|
||||||
this.emitter('audiobook_updated', audiobook.toJSONMinified())
|
this.emitter('audiobook_updated', audiobook.toJSONExpanded())
|
||||||
res.json(audiobook.toJSON())
|
res.json(audiobook.toJSON())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,7 +118,7 @@ class BookController {
|
|||||||
Logger.info(`[ApiController] ${audiobooksUpdated} Audiobooks have updates`)
|
Logger.info(`[ApiController] ${audiobooksUpdated} Audiobooks have updates`)
|
||||||
for (let i = 0; i < audiobooks.length; i++) {
|
for (let i = 0; i < audiobooks.length; i++) {
|
||||||
await this.db.updateAudiobook(audiobooks[i])
|
await this.db.updateAudiobook(audiobooks[i])
|
||||||
this.emitter('audiobook_updated', audiobooks[i].toJSONMinified())
|
this.emitter('audiobook_updated', audiobooks[i].toJSONExpanded())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -128,6 +128,16 @@ class BookController {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// POST: api/books/batch/get
|
||||||
|
async batchGet(req, res) {
|
||||||
|
var bookIds = req.body.books || []
|
||||||
|
if (!bookIds.length) {
|
||||||
|
return res.status(403).send('Invalid payload')
|
||||||
|
}
|
||||||
|
var audiobooks = this.db.audiobooks.filter(ab => bookIds.includes(ab.id)).map((ab) => ab.toJSONExpanded())
|
||||||
|
res.json(audiobooks)
|
||||||
|
}
|
||||||
|
|
||||||
// PATCH: api/books/:id/tracks
|
// PATCH: api/books/:id/tracks
|
||||||
async updateTracks(req, res) {
|
async updateTracks(req, res) {
|
||||||
if (!req.user.canUpdate) {
|
if (!req.user.canUpdate) {
|
||||||
@ -140,7 +150,7 @@ class BookController {
|
|||||||
Logger.info(`Updating audiobook tracks called ${audiobook.id}`)
|
Logger.info(`Updating audiobook tracks called ${audiobook.id}`)
|
||||||
audiobook.updateAudioTracks(orderedFileData)
|
audiobook.updateAudioTracks(orderedFileData)
|
||||||
await this.db.updateAudiobook(audiobook)
|
await this.db.updateAudiobook(audiobook)
|
||||||
this.emitter('audiobook_updated', audiobook.toJSONMinified())
|
this.emitter('audiobook_updated', audiobook.toJSONExpanded())
|
||||||
res.json(audiobook.toJSON())
|
res.json(audiobook.toJSON())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -184,7 +194,7 @@ class BookController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await this.db.updateAudiobook(audiobook)
|
await this.db.updateAudiobook(audiobook)
|
||||||
this.emitter('audiobook_updated', audiobook.toJSONMinified())
|
this.emitter('audiobook_updated', audiobook.toJSONExpanded())
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
cover: result.cover
|
cover: result.cover
|
||||||
@ -205,7 +215,7 @@ class BookController {
|
|||||||
|
|
||||||
if (updated) {
|
if (updated) {
|
||||||
await this.db.updateAudiobook(audiobook)
|
await this.db.updateAudiobook(audiobook)
|
||||||
this.emitter('audiobook_updated', audiobook.toJSONMinified())
|
this.emitter('audiobook_updated', audiobook.toJSONExpanded())
|
||||||
}
|
}
|
||||||
|
|
||||||
if (updated) res.status(200).send('Cover updated successfully')
|
if (updated) res.status(200).send('Cover updated successfully')
|
||||||
|
@ -35,6 +35,7 @@ class LibraryController {
|
|||||||
var books = this.db.audiobooks.filter(ab => ab.libraryId === req.library.id)
|
var books = this.db.audiobooks.filter(ab => ab.libraryId === req.library.id)
|
||||||
return res.json({
|
return res.json({
|
||||||
filterdata: libraryHelpers.getDistinctFilterData(books),
|
filterdata: libraryHelpers.getDistinctFilterData(books),
|
||||||
|
issues: libraryHelpers.getNumIssues(books),
|
||||||
library: req.library
|
library: req.library
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -85,13 +86,6 @@ class LibraryController {
|
|||||||
getBooksForLibrary(req, res) {
|
getBooksForLibrary(req, res) {
|
||||||
var libraryId = req.library.id
|
var libraryId = req.library.id
|
||||||
var audiobooks = this.db.audiobooks.filter(ab => ab.libraryId === libraryId)
|
var audiobooks = this.db.audiobooks.filter(ab => ab.libraryId === libraryId)
|
||||||
// if (req.query.q) {
|
|
||||||
// audiobooks = this.db.audiobooks.filter(ab => {
|
|
||||||
// return ab.libraryId === libraryId && ab.isSearchMatch(req.query.q)
|
|
||||||
// }).map(ab => ab.toJSONMinified())
|
|
||||||
// } else {
|
|
||||||
// audiobooks = this.db.audiobooks.filter(ab => ab.libraryId === libraryId).map(ab => ab.toJSONMinified())
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (req.query.filter) {
|
if (req.query.filter) {
|
||||||
audiobooks = libraryHelpers.getFiltered(audiobooks, req.query.filter, req.user)
|
audiobooks = libraryHelpers.getFiltered(audiobooks, req.query.filter, req.user)
|
||||||
@ -154,13 +148,11 @@ class LibraryController {
|
|||||||
audiobooks = audiobooks.slice(startIndex, startIndex + payload.limit)
|
audiobooks = audiobooks.slice(startIndex, startIndex + payload.limit)
|
||||||
}
|
}
|
||||||
payload.results = audiobooks.map(ab => ab.toJSONExpanded())
|
payload.results = audiobooks.map(ab => ab.toJSONExpanded())
|
||||||
console.log('returning books', audiobooks.length)
|
|
||||||
|
|
||||||
res.json(payload)
|
res.json(payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
// api/libraries/:id/series
|
// api/libraries/:id/series
|
||||||
async getSeriesForLibrary(req, res) {
|
async getAllSeriesForLibrary(req, res) {
|
||||||
var audiobooks = this.db.audiobooks.filter(ab => ab.libraryId === req.library.id)
|
var audiobooks = this.db.audiobooks.filter(ab => ab.libraryId === req.library.id)
|
||||||
|
|
||||||
var payload = {
|
var payload = {
|
||||||
@ -182,11 +174,28 @@ class LibraryController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
payload.results = series
|
payload.results = series
|
||||||
console.log('returning series', series.length)
|
|
||||||
|
|
||||||
res.json(payload)
|
res.json(payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GET: api/libraries/:id/series/:series
|
||||||
|
async getSeriesForLibrary(req, res) {
|
||||||
|
var series = libraryHelpers.decode(req.params.series)
|
||||||
|
if (!series) {
|
||||||
|
return res.status(403).send('Invalid series')
|
||||||
|
}
|
||||||
|
var audiobooks = this.db.audiobooks.filter(ab => ab.libraryId === req.library.id && ab.book.series === series)
|
||||||
|
if (!audiobooks.length) {
|
||||||
|
return res.status(404).send('Series not found')
|
||||||
|
}
|
||||||
|
audiobooks = sort(audiobooks).asc(ab => {
|
||||||
|
return ab.book.volumeNumber
|
||||||
|
})
|
||||||
|
res.json({
|
||||||
|
results: audiobooks,
|
||||||
|
total: audiobooks.length
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// api/libraries/:id/series
|
// api/libraries/:id/series
|
||||||
async getCollectionsForLibrary(req, res) {
|
async getCollectionsForLibrary(req, res) {
|
||||||
var audiobooks = this.db.audiobooks.filter(ab => ab.libraryId === req.library.id)
|
var audiobooks = this.db.audiobooks.filter(ab => ab.libraryId === req.library.id)
|
||||||
@ -210,8 +219,6 @@ class LibraryController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
payload.results = collections
|
payload.results = collections
|
||||||
console.log('returning collections', collections.length)
|
|
||||||
|
|
||||||
res.json(payload)
|
res.json(payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -300,7 +307,7 @@ class LibraryController {
|
|||||||
if (!req.query.q) {
|
if (!req.query.q) {
|
||||||
return res.status(400).send('No query string')
|
return res.status(400).send('No query string')
|
||||||
}
|
}
|
||||||
var maxResults = req.query.max || 3
|
var maxResults = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 12
|
||||||
|
|
||||||
var bookMatches = []
|
var bookMatches = []
|
||||||
var authorMatches = {}
|
var authorMatches = {}
|
||||||
@ -350,13 +357,30 @@ class LibraryController {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
var results = {
|
||||||
res.json({
|
|
||||||
audiobooks: bookMatches.slice(0, maxResults),
|
audiobooks: bookMatches.slice(0, maxResults),
|
||||||
tags: Object.values(tagMatches).slice(0, maxResults),
|
tags: Object.values(tagMatches).slice(0, maxResults),
|
||||||
authors: Object.values(authorMatches).slice(0, maxResults),
|
authors: Object.values(authorMatches).slice(0, maxResults),
|
||||||
series: Object.values(seriesMatches).slice(0, maxResults)
|
series: Object.values(seriesMatches).slice(0, maxResults)
|
||||||
})
|
}
|
||||||
|
res.json(results)
|
||||||
|
}
|
||||||
|
|
||||||
|
async stats(req, res) {
|
||||||
|
var audiobooksInLibrary = this.db.audiobooks.filter(ab => ab.libraryId === req.library.id)
|
||||||
|
|
||||||
|
var authorsWithCount = libraryHelpers.getAuthorsWithCount(audiobooksInLibrary)
|
||||||
|
var genresWithCount = libraryHelpers.getGenresWithCount(audiobooksInLibrary)
|
||||||
|
var stats = {
|
||||||
|
totalBooks: audiobooksInLibrary.length,
|
||||||
|
totalAuthors: Object.keys(authorsWithCount).length,
|
||||||
|
totalGenres: Object.keys(genresWithCount).length,
|
||||||
|
totalDuration: libraryHelpers.getAudiobooksTotalDuration(audiobooksInLibrary),
|
||||||
|
totalSize: libraryHelpers.getAudiobooksTotalSize(audiobooksInLibrary),
|
||||||
|
authorsWithCount,
|
||||||
|
genresWithCount
|
||||||
|
}
|
||||||
|
res.json(stats)
|
||||||
}
|
}
|
||||||
|
|
||||||
middleware(req, res, next) {
|
middleware(req, res, next) {
|
||||||
|
@ -124,6 +124,14 @@ class Audiobook {
|
|||||||
return this._audioFiles.filter(af => af.invalid).map(af => ({ filename: af.filename, error: af.error || 'Unknown Error' }))
|
return this._audioFiles.filter(af => af.invalid).map(af => ({ filename: af.filename, error: af.error || 'Unknown Error' }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get numMissingParts() {
|
||||||
|
return this.missingParts ? this.missingParts.length : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
get numInvalidParts() {
|
||||||
|
return this.invalidParts ? this.invalidParts.length : 0
|
||||||
|
}
|
||||||
|
|
||||||
get _audioFiles() { return this.audioFiles || [] }
|
get _audioFiles() { return this.audioFiles || [] }
|
||||||
get _otherFiles() { return this.otherFiles || [] }
|
get _otherFiles() { return this.otherFiles || [] }
|
||||||
get _tracks() { return this.tracks || [] }
|
get _tracks() { return this.tracks || [] }
|
||||||
@ -206,8 +214,8 @@ class Audiobook {
|
|||||||
chapters: this.chapters || [],
|
chapters: this.chapters || [],
|
||||||
isMissing: !!this.isMissing,
|
isMissing: !!this.isMissing,
|
||||||
isInvalid: !!this.isInvalid,
|
isInvalid: !!this.isInvalid,
|
||||||
hasMissingParts: this.missingParts ? this.missingParts.length : 0,
|
hasMissingParts: this.numMissingParts,
|
||||||
hasInvalidParts: this.invalidParts ? this.invalidParts.length : 0
|
hasInvalidParts: this.numInvalidParts
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -238,8 +246,8 @@ class Audiobook {
|
|||||||
chapters: this.chapters || [],
|
chapters: this.chapters || [],
|
||||||
isMissing: !!this.isMissing,
|
isMissing: !!this.isMissing,
|
||||||
isInvalid: !!this.isInvalid,
|
isInvalid: !!this.isInvalid,
|
||||||
hasMissingParts: this.missingParts ? this.missingParts.length : 0,
|
hasMissingParts: this.numMissingParts,
|
||||||
hasInvalidParts: this.invalidParts ? this.invalidParts.length : 0
|
hasInvalidParts: this.numInvalidParts
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -419,7 +427,6 @@ class Audiobook {
|
|||||||
|
|
||||||
update(payload) {
|
update(payload) {
|
||||||
var hasUpdates = false
|
var hasUpdates = false
|
||||||
|
|
||||||
if (payload.tags && payload.tags.join(',') !== this.tags.join(',')) {
|
if (payload.tags && payload.tags.join(',') !== this.tags.join(',')) {
|
||||||
this.tags = payload.tags
|
this.tags = payload.tags
|
||||||
hasUpdates = true
|
hasUpdates = true
|
||||||
|
@ -215,7 +215,7 @@ class User {
|
|||||||
}
|
}
|
||||||
var wasUpdated = this.audiobooks[audiobook.id].update(updatePayload)
|
var wasUpdated = this.audiobooks[audiobook.id].update(updatePayload)
|
||||||
if (wasUpdated) {
|
if (wasUpdated) {
|
||||||
Logger.debug(`[User] UserAudiobookData was updated ${JSON.stringify(this.audiobooks[audiobook.id])}`)
|
// Logger.debug(`[User] UserAudiobookData was updated ${JSON.stringify(this.audiobooks[audiobook.id])}`)
|
||||||
return this.audiobooks[audiobook.id]
|
return this.audiobooks[audiobook.id]
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@ -276,6 +276,7 @@ class User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getAudiobookJSON(audiobookId) {
|
getAudiobookJSON(audiobookId) {
|
||||||
|
if (!this.audiobooks) return null
|
||||||
return this.audiobooks[audiobookId] ? this.audiobooks[audiobookId].toJSON() : null
|
return this.audiobooks[audiobookId] ? this.audiobooks[audiobookId].toJSON() : null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,7 +85,6 @@ class UserAudiobookData {
|
|||||||
|
|
||||||
update(payload) {
|
update(payload) {
|
||||||
var hasUpdates = false
|
var hasUpdates = false
|
||||||
Logger.debug(`[UserAudiobookData] Update called ${JSON.stringify(payload)}`)
|
|
||||||
for (const key in payload) {
|
for (const key in payload) {
|
||||||
if (payload[key] !== this[key]) {
|
if (payload[key] !== this[key]) {
|
||||||
if (key === 'isRead') {
|
if (key === 'isRead') {
|
||||||
|
@ -33,7 +33,7 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
} else if (filterBy === 'issues') {
|
} else if (filterBy === 'issues') {
|
||||||
filtered = filtered.filter(ab => {
|
filtered = filtered.filter(ab => {
|
||||||
return ab.hasMissingParts || ab.hasInvalidParts || ab.isMissing || ab.isIncomplete
|
return ab.numMissingParts || ab.numInvalidParts || ab.isMissing || ab.isInvalid
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,6 +82,7 @@ module.exports = {
|
|||||||
_series[audiobook.book.series] = {
|
_series[audiobook.book.series] = {
|
||||||
id: audiobook.book.series,
|
id: audiobook.book.series,
|
||||||
name: audiobook.book.series,
|
name: audiobook.book.series,
|
||||||
|
type: 'series',
|
||||||
books: [audiobook.toJSONExpanded()]
|
books: [audiobook.toJSONExpanded()]
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -102,16 +103,16 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
getBooksMostRecentlyRead(booksWithUserAb, limit) {
|
getBooksMostRecentlyRead(booksWithUserAb, limit) {
|
||||||
var booksWithProgress = booksWithUserAb.filter((data) => data.userAudiobook && !data.userAudiobook.isRead)
|
var booksWithProgress = booksWithUserAb.filter((data) => data.userAudiobook && data.userAudiobook.progress > 0 && !data.userAudiobook.isRead)
|
||||||
booksWithProgress.sort((a, b) => {
|
booksWithProgress.sort((a, b) => {
|
||||||
return b.userAudiobook.lastUpdate - a.userAudiobook.lastUpdate
|
return b.userAudiobook.lastUpdate - a.userAudiobook.lastUpdate
|
||||||
})
|
})
|
||||||
return booksWithProgress.map(b => b.book).slice(0, limit)
|
return booksWithProgress.map(b => b.book.toJSONExpanded()).slice(0, limit)
|
||||||
},
|
},
|
||||||
|
|
||||||
getBooksMostRecentlyAdded(books, limit) {
|
getBooksMostRecentlyAdded(books, limit) {
|
||||||
var booksSortedByAddedAt = sort(books).desc(book => book.addedAt)
|
var booksSortedByAddedAt = sort(books).desc(book => book.addedAt)
|
||||||
return booksSortedByAddedAt.slice(0, limit)
|
return booksSortedByAddedAt.map(b => b.toJSONExpanded()).slice(0, limit)
|
||||||
},
|
},
|
||||||
|
|
||||||
getBooksMostRecentlyFinished(booksWithUserAb, limit) {
|
getBooksMostRecentlyFinished(booksWithUserAb, limit) {
|
||||||
@ -119,7 +120,7 @@ module.exports = {
|
|||||||
booksRead.sort((a, b) => {
|
booksRead.sort((a, b) => {
|
||||||
return b.userAudiobook.finishedAt - a.userAudiobook.finishedAt
|
return b.userAudiobook.finishedAt - a.userAudiobook.finishedAt
|
||||||
})
|
})
|
||||||
return booksRead.map(b => b.book).slice(0, limit)
|
return booksRead.map(b => b.book.toJSONExpanded()).slice(0, limit)
|
||||||
},
|
},
|
||||||
|
|
||||||
getSeriesMostRecentlyAdded(series, limit) {
|
getSeriesMostRecentlyAdded(series, limit) {
|
||||||
@ -128,5 +129,59 @@ module.exports = {
|
|||||||
return booksSortedByMostRecent[0].addedAt
|
return booksSortedByMostRecent[0].addedAt
|
||||||
})
|
})
|
||||||
return seriesSortedByAddedAt.slice(0, limit)
|
return seriesSortedByAddedAt.slice(0, limit)
|
||||||
|
},
|
||||||
|
|
||||||
|
getGenresWithCount(audiobooks) {
|
||||||
|
var genresMap = {}
|
||||||
|
audiobooks.forEach((ab) => {
|
||||||
|
var genres = ab.book.genres || []
|
||||||
|
genres.forEach((genre) => {
|
||||||
|
if (genresMap[genre]) genresMap[genre].count++
|
||||||
|
else
|
||||||
|
genresMap[genre] = {
|
||||||
|
genre,
|
||||||
|
count: 1
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return Object.values(genresMap).sort((a, b) => b.count - a.count)
|
||||||
|
},
|
||||||
|
|
||||||
|
getAuthorsWithCount(audiobooks) {
|
||||||
|
var authorsMap = {}
|
||||||
|
audiobooks.forEach((ab) => {
|
||||||
|
var authors = ab.book.authorFL ? ab.book.authorFL.split(', ') : []
|
||||||
|
authors.forEach((author) => {
|
||||||
|
if (authorsMap[author]) authorsMap[author].count++
|
||||||
|
else
|
||||||
|
authorsMap[author] = {
|
||||||
|
author,
|
||||||
|
count: 1
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return Object.values(authorsMap).sort((a, b) => b.count - a.count)
|
||||||
|
},
|
||||||
|
|
||||||
|
getAudiobooksTotalDuration(audiobooks) {
|
||||||
|
var totalDuration = 0
|
||||||
|
audiobooks.forEach((ab) => {
|
||||||
|
totalDuration += ab.totalDuration
|
||||||
|
})
|
||||||
|
return totalDuration
|
||||||
|
},
|
||||||
|
|
||||||
|
getAudiobooksTotalSize(audiobooks) {
|
||||||
|
var totalSize = 0
|
||||||
|
audiobooks.forEach((ab) => {
|
||||||
|
totalSize += ab.totalSize
|
||||||
|
})
|
||||||
|
return totalSize
|
||||||
|
},
|
||||||
|
|
||||||
|
getNumIssues(books) {
|
||||||
|
return books.filter(ab => {
|
||||||
|
return ab.numMissingParts || ab.numInvalidParts || ab.isMissing || ab.isInvalid
|
||||||
|
}).length
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user