mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2025-06-20 20:05:44 +02:00
Add:Lazy bookshelf
This commit is contained in:
parent
446c6756ed
commit
37d3021302
43 changed files with 2264 additions and 666 deletions
|
@ -14,6 +14,7 @@ class Server extends EventEmitter {
|
|||
|
||||
this.user = null
|
||||
this.connected = false
|
||||
this.initialized = false
|
||||
|
||||
this.stream = null
|
||||
|
||||
|
@ -231,6 +232,8 @@ class Server extends EventEmitter {
|
|||
console.log('[SOCKET] Socket Disconnected: ' + reason)
|
||||
this.connected = false
|
||||
this.emit('connected', false)
|
||||
this.emit('initialized', false)
|
||||
this.initialized = false
|
||||
this.store.commit('setSocketConnected', false)
|
||||
|
||||
// this.socket.removeAllListeners()
|
||||
|
@ -246,6 +249,11 @@ class Server extends EventEmitter {
|
|||
this.store.commit('setStreamAudiobook', data.stream.audiobook)
|
||||
this.emit('initialStream', data.stream)
|
||||
}
|
||||
if (data.serverSettings) {
|
||||
this.store.commit('setServerSettings', data.serverSettings)
|
||||
}
|
||||
this.initialized = true
|
||||
this.emit('initialized', true)
|
||||
})
|
||||
|
||||
this.socket.on('user_updated', (user) => {
|
||||
|
|
|
@ -19,6 +19,9 @@
|
|||
.box-shadow-book {
|
||||
box-shadow: 4px 1px 8px #11111166, -4px 1px 8px #11111166, 1px -4px 8px #11111166;
|
||||
}
|
||||
.shadow-height {
|
||||
height: calc(100% - 4px);
|
||||
}
|
||||
|
||||
.bookshelfRow {
|
||||
background-image: url(/wood_panels.jpg);
|
||||
|
@ -43,3 +46,14 @@ Bookshelf Label
|
|||
border-style: solid;
|
||||
color: #fce3a6;
|
||||
}
|
||||
|
||||
.cover-bg {
|
||||
width: calc(100% + 40px);
|
||||
height: calc(100% + 40px);
|
||||
top: -20px;
|
||||
left: -20px;
|
||||
background-size: 100% 100%;
|
||||
background-position: center;
|
||||
opacity: 1;
|
||||
filter: blur(20px);
|
||||
}
|
|
@ -22,8 +22,6 @@
|
|||
|
||||
<!-- <span class="material-icons cursor-pointer mx-4" :class="hasDownloadsFolder ? '' : 'text-warning'" @click="$store.commit('downloads/setShowModal', true)">source</span> -->
|
||||
|
||||
<!-- <widgets-connection-icon /> -->
|
||||
|
||||
<nuxt-link class="h-7 mx-2" to="/search">
|
||||
<span class="material-icons" style="font-size: 1.75rem">search</span>
|
||||
</nuxt-link>
|
||||
|
|
|
@ -54,7 +54,7 @@ export default {
|
|||
var booksPerShelf = Math.floor(this.pageWidth / (this.cardWidth + 32))
|
||||
var groupedBooks = []
|
||||
|
||||
var audiobooksSorted = this.$store.getters['audiobooks/getFilteredAndSorted']()
|
||||
var audiobooksSorted = []
|
||||
this.currFilterOrderKey = this.filterOrderKey
|
||||
|
||||
var numGroups = Math.ceil(audiobooksSorted.length / booksPerShelf)
|
||||
|
@ -86,7 +86,6 @@ export default {
|
|||
if (currentLibrary) {
|
||||
this.$store.commit('libraries/setCurrentLibrary', currentLibrary.id)
|
||||
}
|
||||
this.$store.dispatch('audiobooks/load')
|
||||
},
|
||||
socketConnected(isConnected) {
|
||||
if (isConnected) {
|
||||
|
|
|
@ -51,9 +51,7 @@ export default {
|
|||
mobileFilterBy: 'all'
|
||||
})
|
||||
},
|
||||
calcShelves() {
|
||||
this.audiobooks = this.$store.getters['audiobooks/getFilteredAndSorted']()
|
||||
},
|
||||
calcShelves() {},
|
||||
audiobooksUpdated() {
|
||||
this.calcShelves()
|
||||
},
|
||||
|
@ -76,7 +74,6 @@ export default {
|
|||
if (currentLibrary) {
|
||||
this.$store.commit('libraries/setCurrentLibrary', currentLibrary.id)
|
||||
}
|
||||
this.$store.dispatch('audiobooks/load')
|
||||
},
|
||||
socketConnected(isConnected) {
|
||||
if (isConnected) {
|
||||
|
|
|
@ -28,15 +28,25 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import TouchEvent from '@/objects/TouchEvent'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {}
|
||||
return {
|
||||
touchEvent: null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
$route: {
|
||||
handler() {
|
||||
this.show = false
|
||||
}
|
||||
},
|
||||
show: {
|
||||
handler(newVal) {
|
||||
if (newVal) this.registerListener()
|
||||
else this.removeListener()
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -106,6 +116,25 @@ export default {
|
|||
|
||||
this.$store.commit('audiobooks/reset')
|
||||
this.$store.dispatch('audiobooks/useDownloaded')
|
||||
},
|
||||
touchstart(e) {
|
||||
this.touchEvent = new TouchEvent(e)
|
||||
},
|
||||
touchend(e) {
|
||||
if (!this.touchEvent) return
|
||||
this.touchEvent.setEndEvent(e)
|
||||
if (this.touchEvent.isSwipeRight()) {
|
||||
this.show = false
|
||||
}
|
||||
this.touchEvent = null
|
||||
},
|
||||
registerListener() {
|
||||
document.addEventListener('touchstart', this.touchstart)
|
||||
document.addEventListener('touchend', this.touchend)
|
||||
},
|
||||
removeListener() {
|
||||
document.removeEventListener('touchstart', this.touchstart)
|
||||
document.removeEventListener('touchend', this.touchend)
|
||||
}
|
||||
},
|
||||
mounted() {},
|
||||
|
|
400
components/bookshelf/LazyBookshelf.vue
Normal file
400
components/bookshelf/LazyBookshelf.vue
Normal file
|
@ -0,0 +1,400 @@
|
|||
<template>
|
||||
<div id="bookshelf" class="w-full max-w-full h-full min-h-screen">
|
||||
<template v-for="shelf in totalShelves">
|
||||
<div :key="shelf" class="w-full px-2 bookshelfRow relative" :id="`shelf-${shelf - 1}`" :style="{ height: shelfHeight + 'px' }">
|
||||
<div class="bookshelfDivider w-full absolute bottom-0 left-0 right-0 z-20" :class="`h-${shelfDividerHeightIndex}`" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- <div class="w-full h-full px-4 py-2" v-show="isListView">
|
||||
<template v-for="book in books">
|
||||
<app-bookshelf-list-row :key="book.id" :audiobook="book" :page-width="pageWidth" class="my-2" />
|
||||
</template>
|
||||
</div> -->
|
||||
<div v-show="!entities.length && initialized" class="w-full py-16 text-center text-xl">
|
||||
<div class="py-4 capitalize">No {{ entityName }}</div>
|
||||
<ui-btn v-if="hasFilter" @click="clearFilter">Clear Filter</ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import bookshelfCardsHelpers from '@/mixins/bookshelfCardsHelpers'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
page: String,
|
||||
seriesId: String
|
||||
},
|
||||
mixins: [bookshelfCardsHelpers],
|
||||
data() {
|
||||
return {
|
||||
bookshelfHeight: 0,
|
||||
bookshelfWidth: 0,
|
||||
bookshelfMarginLeft: 0,
|
||||
shelvesPerPage: 0,
|
||||
entitiesPerShelf: 8,
|
||||
currentPage: 0,
|
||||
currentBookWidth: 0,
|
||||
booksPerFetch: 20,
|
||||
initialized: false,
|
||||
currentSFQueryString: null,
|
||||
isFetchingEntities: false,
|
||||
entities: [],
|
||||
totalEntities: 0,
|
||||
totalShelves: 0,
|
||||
entityComponentRefs: {},
|
||||
entityIndexesMounted: [],
|
||||
pagesLoaded: {},
|
||||
isFirstInit: false,
|
||||
pendingReset: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isSocketConnected() {
|
||||
return this.$store.state.socketConnected
|
||||
},
|
||||
isBookEntity() {
|
||||
return this.entityName === 'books' || this.entityName === 'series-books'
|
||||
},
|
||||
shelfDividerHeightIndex() {
|
||||
if (this.isBookEntity) return 4
|
||||
return 6
|
||||
},
|
||||
entityName() {
|
||||
return this.page
|
||||
},
|
||||
bookshelfView() {
|
||||
return this.$store.state.bookshelfView
|
||||
},
|
||||
hasFilter() {
|
||||
return this.filterBy !== 'all'
|
||||
},
|
||||
isListView() {
|
||||
return this.bookshelfView === 'list'
|
||||
},
|
||||
books() {
|
||||
return this.$store.getters['downloads/getAudiobooks']
|
||||
},
|
||||
orderBy() {
|
||||
return this.$store.getters['user/getUserSetting']('mobileOrderBy')
|
||||
},
|
||||
orderDesc() {
|
||||
return this.$store.getters['user/getUserSetting']('mobileOrderDesc')
|
||||
},
|
||||
filterBy() {
|
||||
return this.$store.getters['user/getUserSetting']('mobileFilterBy')
|
||||
},
|
||||
coverAspectRatio() {
|
||||
return this.$store.getters['getServerSetting']('coverAspectRatio')
|
||||
},
|
||||
isCoverSquareAspectRatio() {
|
||||
return this.coverAspectRatio === this.$constants.BookCoverAspectRatio.SQUARE
|
||||
},
|
||||
bookCoverAspectRatio() {
|
||||
return this.isCoverSquareAspectRatio ? 1 : 1.6
|
||||
},
|
||||
bookWidth() {
|
||||
// var coverSize = this.$store.getters['user/getUserSetting']('bookshelfCoverSize')
|
||||
var coverSize = 100
|
||||
if (this.isCoverSquareAspectRatio) return coverSize * 1.6
|
||||
return coverSize
|
||||
},
|
||||
bookHeight() {
|
||||
if (this.isCoverSquareAspectRatio) return this.bookWidth
|
||||
return this.bookWidth * 1.6
|
||||
},
|
||||
entityWidth() {
|
||||
if (this.isBookEntity) return this.bookWidth
|
||||
return this.bookWidth * 2
|
||||
},
|
||||
entityHeight() {
|
||||
return this.bookHeight
|
||||
},
|
||||
currentLibraryId() {
|
||||
return this.$store.state.libraries.currentLibraryId
|
||||
},
|
||||
shelfHeight() {
|
||||
return this.entityHeight + 40
|
||||
},
|
||||
totalEntityCardWidth() {
|
||||
// Includes margin
|
||||
return this.entityWidth + 24
|
||||
},
|
||||
downloadedBooks() {
|
||||
return this.$store.getters['downloads/getAudiobooks']
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
clearFilter() {
|
||||
this.$store.dispatch('user/updateUserSettings', {
|
||||
mobileFilterBy: 'all'
|
||||
})
|
||||
},
|
||||
async fetchEntities(page) {
|
||||
var startIndex = page * this.booksPerFetch
|
||||
|
||||
this.isFetchingEntities = true
|
||||
|
||||
if (!this.initialized) {
|
||||
this.currentSFQueryString = this.buildSearchParams()
|
||||
}
|
||||
var entityPath = this.entityName === 'books' ? `books/all` : this.entityName
|
||||
var sfQueryString = this.currentSFQueryString ? this.currentSFQueryString + '&' : ''
|
||||
var queryString = `?${sfQueryString}&limit=${this.booksPerFetch}&page=${page}`
|
||||
|
||||
if (this.entityName === 'series-books') {
|
||||
entityPath = `series/${this.seriesId}`
|
||||
queryString = ''
|
||||
}
|
||||
|
||||
var payload = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/${entityPath}${queryString}`).catch((error) => {
|
||||
console.error('failed to fetch books', error)
|
||||
return null
|
||||
})
|
||||
this.isFetchingEntities = false
|
||||
if (this.pendingReset) {
|
||||
this.pendingReset = false
|
||||
this.resetEntities()
|
||||
return
|
||||
}
|
||||
if (payload && payload.results) {
|
||||
console.log('Received payload', payload)
|
||||
if (!this.initialized) {
|
||||
this.initialized = true
|
||||
this.totalEntities = payload.total
|
||||
this.totalShelves = Math.ceil(this.totalEntities / this.entitiesPerShelf)
|
||||
this.entities = new Array(this.totalEntities)
|
||||
this.$eventBus.$emit('bookshelf-total-entities', this.totalEntities)
|
||||
}
|
||||
|
||||
for (let i = 0; i < payload.results.length; i++) {
|
||||
var index = i + startIndex
|
||||
this.entities[index] = payload.results[i]
|
||||
if (this.entityComponentRefs[index]) {
|
||||
this.entityComponentRefs[index].setEntity(this.entities[index])
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
async loadPage(page) {
|
||||
this.pagesLoaded[page] = true
|
||||
await this.fetchEntities(page)
|
||||
},
|
||||
mountEntites(fromIndex, toIndex) {
|
||||
for (let i = fromIndex; i < toIndex; i++) {
|
||||
if (!this.entityIndexesMounted.includes(i)) {
|
||||
this.cardsHelpers.mountEntityCard(i)
|
||||
}
|
||||
}
|
||||
},
|
||||
handleScroll(scrollTop) {
|
||||
this.currScrollTop = scrollTop
|
||||
var firstShelfIndex = Math.floor(scrollTop / this.shelfHeight)
|
||||
var lastShelfIndex = Math.ceil((scrollTop + this.bookshelfHeight) / this.shelfHeight)
|
||||
lastShelfIndex = Math.min(this.totalShelves - 1, lastShelfIndex)
|
||||
|
||||
var firstBookIndex = firstShelfIndex * this.entitiesPerShelf
|
||||
var lastBookIndex = lastShelfIndex * this.entitiesPerShelf + this.entitiesPerShelf
|
||||
lastBookIndex = Math.min(this.totalEntities, lastBookIndex)
|
||||
|
||||
var firstBookPage = Math.floor(firstBookIndex / this.booksPerFetch)
|
||||
var lastBookPage = Math.floor(lastBookIndex / this.booksPerFetch)
|
||||
if (!this.pagesLoaded[firstBookPage]) {
|
||||
console.log('Must load next batch', firstBookPage, 'book index', firstBookIndex)
|
||||
this.loadPage(firstBookPage)
|
||||
}
|
||||
if (!this.pagesLoaded[lastBookPage]) {
|
||||
console.log('Must load last next batch', lastBookPage, 'book index', lastBookIndex)
|
||||
this.loadPage(lastBookPage)
|
||||
}
|
||||
|
||||
this.entityIndexesMounted = this.entityIndexesMounted.filter((_index) => {
|
||||
if (_index < firstBookIndex || _index >= lastBookIndex) {
|
||||
var el = document.getElementById(`book-card-${_index}`)
|
||||
if (el) el.remove()
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
this.mountEntites(firstBookIndex, lastBookIndex)
|
||||
},
|
||||
destroyEntityComponents() {
|
||||
for (const key in this.entityComponentRefs) {
|
||||
if (this.entityComponentRefs[key] && this.entityComponentRefs[key].destroy) {
|
||||
this.entityComponentRefs[key].destroy()
|
||||
}
|
||||
}
|
||||
},
|
||||
setDownloads() {
|
||||
// TODO: Check entityName
|
||||
this.entities = this.downloadedBooks.map((db) => ({ ...db }))
|
||||
// TOOD: Sort and filter here
|
||||
this.totalEntities = this.entities.length
|
||||
this.totalShelves = Math.ceil(this.totalEntities / this.entitiesPerShelf)
|
||||
this.entities = new Array(this.totalEntities)
|
||||
this.$eventBus.$emit('bookshelf-total-entities', this.totalEntities)
|
||||
},
|
||||
async resetEntities() {
|
||||
if (this.isFetchingEntities) {
|
||||
this.pendingReset = true
|
||||
return
|
||||
}
|
||||
this.destroyEntityComponents()
|
||||
this.entityIndexesMounted = []
|
||||
this.entityComponentRefs = {}
|
||||
this.pagesLoaded = {}
|
||||
this.entities = []
|
||||
this.totalShelves = 0
|
||||
this.totalEntities = 0
|
||||
this.currentPage = 0
|
||||
this.initialized = false
|
||||
|
||||
this.initSizeData()
|
||||
if (this.isSocketConnected) {
|
||||
await this.loadPage(0)
|
||||
var lastBookIndex = Math.min(this.totalEntities, this.shelvesPerPage * this.entitiesPerShelf)
|
||||
this.mountEntites(0, lastBookIndex)
|
||||
} else {
|
||||
this.setDownloads()
|
||||
this.mountEntites(0, this.totalEntities - 1)
|
||||
}
|
||||
},
|
||||
initSizeData() {
|
||||
var bookshelf = document.getElementById('bookshelf')
|
||||
if (!bookshelf) {
|
||||
console.error('Failed to init size data')
|
||||
return
|
||||
}
|
||||
var entitiesPerShelfBefore = this.entitiesPerShelf
|
||||
|
||||
var { clientHeight, clientWidth } = bookshelf
|
||||
this.bookshelfHeight = clientHeight
|
||||
this.bookshelfWidth = clientWidth
|
||||
this.entitiesPerShelf = Math.floor((this.bookshelfWidth - 16) / this.totalEntityCardWidth)
|
||||
|
||||
this.shelvesPerPage = Math.ceil(this.bookshelfHeight / this.shelfHeight) + 2
|
||||
this.bookshelfMarginLeft = (this.bookshelfWidth - this.entitiesPerShelf * this.totalEntityCardWidth) / 2
|
||||
|
||||
this.currentBookWidth = this.bookWidth
|
||||
if (this.totalEntities) {
|
||||
this.totalShelves = Math.ceil(this.totalEntities / this.entitiesPerShelf)
|
||||
}
|
||||
return entitiesPerShelfBefore < this.entitiesPerShelf // Books per shelf has changed
|
||||
},
|
||||
async init(bookshelf) {
|
||||
if (this.isFirstInit) return
|
||||
this.isFirstInit = true
|
||||
this.initSizeData(bookshelf)
|
||||
|
||||
await this.loadPage(0)
|
||||
var lastBookIndex = Math.min(this.totalEntities, this.shelvesPerPage * this.entitiesPerShelf)
|
||||
this.mountEntites(0, lastBookIndex)
|
||||
},
|
||||
scroll(e) {
|
||||
if (!e || !e.target) return
|
||||
if (!this.isSocketConnected) return // Offline books are all mounted at once
|
||||
var { scrollTop } = e.target
|
||||
this.handleScroll(scrollTop)
|
||||
},
|
||||
socketInit(isConnected) {
|
||||
if (isConnected) {
|
||||
this.init()
|
||||
} else {
|
||||
this.isFirstInit = false
|
||||
this.resetEntities()
|
||||
}
|
||||
},
|
||||
buildSearchParams() {
|
||||
let searchParams = new URLSearchParams()
|
||||
if (this.filterBy && this.filterBy !== 'all') {
|
||||
searchParams.set('filter', this.filterBy)
|
||||
}
|
||||
if (this.orderBy) {
|
||||
searchParams.set('sort', this.orderBy)
|
||||
searchParams.set('desc', this.orderDesc ? 1 : 0)
|
||||
}
|
||||
return searchParams.toString()
|
||||
},
|
||||
checkUpdateSearchParams() {
|
||||
var newSearchParams = this.buildSearchParams()
|
||||
var currentQueryString = window.location.search
|
||||
if (currentQueryString && currentQueryString.startsWith('?')) currentQueryString = currentQueryString.slice(1)
|
||||
|
||||
if (newSearchParams === '') {
|
||||
return false
|
||||
}
|
||||
if (newSearchParams !== this.currentSFQueryString || newSearchParams !== currentQueryString) {
|
||||
let newurl = window.location.protocol + '//' + window.location.host + window.location.pathname + '?' + newSearchParams
|
||||
window.history.replaceState({ path: newurl }, '', newurl)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
settingsUpdated(settings) {
|
||||
var wasUpdated = this.checkUpdateSearchParams()
|
||||
if (wasUpdated) {
|
||||
this.resetEntities()
|
||||
}
|
||||
},
|
||||
libraryChanged(libid) {
|
||||
if (this.hasFilter) {
|
||||
this.clearFilter()
|
||||
} else {
|
||||
this.resetEntities()
|
||||
}
|
||||
},
|
||||
initListeners() {
|
||||
var bookshelf = document.getElementById('bookshelf-wrapper')
|
||||
if (bookshelf) {
|
||||
bookshelf.addEventListener('scroll', this.scroll)
|
||||
}
|
||||
// this.$eventBus.$on('bookshelf-clear-selection', this.clearSelectedEntities)
|
||||
// this.$eventBus.$on('bookshelf-select-all', this.selectAllEntities)
|
||||
// this.$eventBus.$on('bookshelf-keyword-filter', this.updateKeywordFilter)
|
||||
this.$eventBus.$on('library-changed', this.libraryChanged)
|
||||
this.$store.commit('user/addSettingsListener', { id: 'lazy-bookshelf', meth: this.settingsUpdated })
|
||||
|
||||
// if (this.$root.socket) {
|
||||
// this.$root.socket.on('audiobook_updated', this.audiobookUpdated)
|
||||
// this.$root.socket.on('audiobook_added', this.audiobookAdded)
|
||||
// this.$root.socket.on('audiobook_removed', this.audiobookRemoved)
|
||||
// this.$root.socket.on('audiobooks_updated', this.audiobooksUpdated)
|
||||
// this.$root.socket.on('audiobooks_added', this.audiobooksAdded)
|
||||
// } else {
|
||||
// console.error('Bookshelf - Socket not initialized')
|
||||
// }
|
||||
},
|
||||
removeListeners() {
|
||||
var bookshelf = document.getElementById('bookshelf-wrapper')
|
||||
if (bookshelf) {
|
||||
bookshelf.removeEventListener('scroll', this.scroll)
|
||||
}
|
||||
this.$eventBus.$off('library-changed', this.libraryChanged)
|
||||
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')
|
||||
// }
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.$server.initialized) {
|
||||
this.init()
|
||||
}
|
||||
this.$server.on('initialized', this.socketInit)
|
||||
this.initListeners()
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.$server.off('initialized', this.socketInit)
|
||||
this.removeListeners()
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,8 +1,10 @@
|
|||
<template>
|
||||
<div class="w-full relative">
|
||||
<div class="bookshelfRow h-44 flex items-end px-3 max-w-full overflow-x-auto">
|
||||
<template v-for="book in books">
|
||||
<cards-book-card :key="book.id" :audiobook="book" :width="100" class="mx-2" />
|
||||
<div class="bookshelfRow flex items-end px-3 max-w-full overflow-x-auto" :style="{ height: shelfHeight + 'px' }">
|
||||
<template v-for="(entity, index) in entities">
|
||||
<!-- <cards-book-card v-if="type === 'books'" :key="entity.id" :audiobook="entity" :width="bookWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" class="mx-2" /> -->
|
||||
<cards-lazy-book-card v-if="type === 'books'" :key="entity.id" :index="index" :book-mount="entity" :width="bookWidth" :height="entityHeight" :book-cover-aspect-ratio="bookCoverAspectRatio" is-categorized class="mx-2 relative" />
|
||||
<cards-lazy-series-card v-else-if="type === 'series'" :key="entity.id" :index="index" :series-mount="entity" :width="bookWidth * 2" :height="entityHeight" :book-cover-aspect-ratio="bookCoverAspectRatio" is-categorized class="mx-2 relative" />
|
||||
</template>
|
||||
</div>
|
||||
|
||||
|
@ -19,7 +21,8 @@
|
|||
export default {
|
||||
props: {
|
||||
label: String,
|
||||
books: {
|
||||
type: String,
|
||||
entities: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
|
@ -27,7 +30,26 @@ export default {
|
|||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {},
|
||||
computed: {
|
||||
shelfHeight() {
|
||||
return this.entityHeight + 40
|
||||
},
|
||||
bookWidth() {
|
||||
var coverSize = 100
|
||||
if (this.bookCoverAspectRatio === 1) return coverSize * 1.6
|
||||
return coverSize
|
||||
},
|
||||
bookHeight() {
|
||||
if (this.bookCoverAspectRatio === 1) return this.bookWidth
|
||||
return this.bookWidth * 1.6
|
||||
},
|
||||
entityHeight() {
|
||||
return this.bookHeight
|
||||
},
|
||||
bookCoverAspectRatio() {
|
||||
return this.$store.getters['getBookCoverAspectRatio']
|
||||
}
|
||||
},
|
||||
methods: {},
|
||||
mounted() {}
|
||||
}
|
||||
|
|
46
components/cards/AuthorSearchCard.vue
Normal file
46
components/cards/AuthorSearchCard.vue
Normal file
|
@ -0,0 +1,46 @@
|
|||
<template>
|
||||
<div class="flex h-full px-1 overflow-hidden shadow-sm">
|
||||
<!-- <img src="/icons/NoUserPhoto.png" class="w-40 h-40 max-h-40 object-contain" style="max-height: 40px; max-width: 40px" /> -->
|
||||
<div style="max-height: 48px; max-width: 48px" class="w-12 h-12 bg-primary overflow-hidden rounded">
|
||||
<svg width="140%" height="140%" style="margin-left: -20%; margin-top: -20%; opacity: 0.6" viewBox="0 0 177 266" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="white" d="M40.7156 165.47C10.2694 150.865 -31.5407 148.629 -38.0532 155.529L63.3191 204.159L76.9443 190.899C66.828 181.394 54.006 171.846 40.7156 165.47Z" stroke="white" stroke-width="4" transform="translate(-2 -1)" />
|
||||
<path d="M-38.0532 155.529C-31.5407 148.629 10.2694 150.865 40.7156 165.47C54.006 171.846 66.828 181.394 76.9443 190.899L95.0391 173.37C80.6681 159.403 64.7526 149.155 51.5747 142.834C21.3549 128.337 -46.2471 114.563 -60.6897 144.67L-71.5489 167.307L44.5864 223.019L63.3191 204.159L-38.0532 155.529Z" fill="white" />
|
||||
<path
|
||||
d="M105.87 29.6508C80.857 17.6515 50.8784 28.1923 38.879 53.2056C26.8797 78.219 37.4205 108.198 62.4338 120.197C87.4472 132.196 117.426 121.656 129.425 96.6422C141.425 71.6288 130.884 41.6502 105.87 29.6508ZM106.789 85.783C112.761 73.3329 107.461 58.2599 95.0112 52.2874C82.5611 46.3148 67.4881 51.6147 61.5156 64.0648C55.543 76.5149 60.8429 91.5879 73.293 97.5604C85.7431 103.533 100.816 98.2331 106.789 85.783Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M151.336 159.01L159.048 166.762L82.7048 242.703L74.973 242.683L74.9934 234.951L151.336 159.01ZM181.725 108.497C179.624 108.491 177.436 109.326 175.835 110.918L160.415 126.257L191.848 157.856L207.268 142.517C210.554 139.248 210.568 133.954 207.299 130.667L187.685 110.95C186.009 109.264 183.91 108.502 181.725 108.497ZM151.399 135.226L58.2034 227.931L58.1203 259.447L89.6359 259.53L182.831 166.825L151.399 135.226Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path d="M151.336 159.01L159.048 166.762L82.7048 242.703L74.973 242.683L74.9934 234.951L151.336 159.01Z" fill="white" stroke="white" stroke-width="10px" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-grow px-2 authorSearchCardContent h-full">
|
||||
<p class="truncate text-sm">{{ author }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
author: String
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {},
|
||||
methods: {},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.authorSearchCardContent {
|
||||
width: calc(100% - 50px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
|
@ -1,17 +1,9 @@
|
|||
<template>
|
||||
<div class="relative">
|
||||
<!-- New Book Flag -->
|
||||
<div v-if="isNew" class="absolute top-4 left-0 w-4 h-10 pr-2 bg-darkgreen box-shadow-xl">
|
||||
<div class="absolute top-0 left-0 w-full h-full transform -rotate-90 flex items-center justify-center">
|
||||
<p class="text-center text-sm">New</p>
|
||||
</div>
|
||||
<div class="absolute -bottom-4 left-0 triangle-right" />
|
||||
</div>
|
||||
|
||||
<div class="rounded-sm h-full overflow-hidden relative box-shadow-book">
|
||||
<nuxt-link :to="`/audiobook/${audiobookId}`" class="cursor-pointer">
|
||||
<div class="w-full relative" :style="{ height: height + 'px' }">
|
||||
<cards-book-cover :audiobook="audiobook" :download-cover="downloadCover" :author-override="authorFormat" :width="width" />
|
||||
<covers-book-cover :audiobook="audiobook" :download-cover="downloadCover" :width="width" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||
|
||||
<div v-if="download" class="absolute" :style="{ top: 0.5 * sizeMultiplier + 'rem', right: 0.5 * sizeMultiplier + 'rem' }">
|
||||
<span class="material-icons text-success" :style="{ fontSize: 1.1 * sizeMultiplier + 'rem' }">download_done</span>
|
||||
|
@ -38,15 +30,13 @@ export default {
|
|||
width: {
|
||||
type: Number,
|
||||
default: 140
|
||||
}
|
||||
},
|
||||
bookCoverAspectRatio: Number
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
isNew() {
|
||||
return this.tags.includes('new')
|
||||
},
|
||||
tags() {
|
||||
return this.audiobook.tags || []
|
||||
},
|
||||
|
@ -57,29 +47,11 @@ export default {
|
|||
return this.audiobook.book || {}
|
||||
},
|
||||
height() {
|
||||
return this.width * 1.6
|
||||
return this.width * this.bookCoverAspectRatio
|
||||
},
|
||||
sizeMultiplier() {
|
||||
return this.width / 120
|
||||
},
|
||||
paddingX() {
|
||||
return 16 * this.sizeMultiplier
|
||||
},
|
||||
author() {
|
||||
return this.book.author
|
||||
},
|
||||
authorFL() {
|
||||
return this.book.authorFL || this.author
|
||||
},
|
||||
authorLF() {
|
||||
return this.book.authorLF || this.author
|
||||
},
|
||||
authorFormat() {
|
||||
if (!this.orderBy || !this.orderBy.startsWith('book.author')) return null
|
||||
return this.orderBy === 'book.authorLF' ? this.authorLF : this.authorFL
|
||||
},
|
||||
orderBy() {
|
||||
return this.$store.getters['user/getUserSetting']('mobileOrderBy')
|
||||
if (this.bookCoverAspectRatio === 1) return this.width / 160
|
||||
return this.width / 100
|
||||
},
|
||||
mostRecentUserProgress() {
|
||||
return this.$store.getters['user/getUserAudiobookData'](this.audiobookId)
|
||||
|
|
89
components/cards/BookSearchCard.vue
Normal file
89
components/cards/BookSearchCard.vue
Normal file
|
@ -0,0 +1,89 @@
|
|||
<template>
|
||||
<div class="flex h-full px-1 overflow-hidden">
|
||||
<covers-book-cover :audiobook="audiobook" :width="coverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||
<div class="flex-grow px-2 h-full audiobookSearchCardContent">
|
||||
<p v-if="matchKey !== 'title'" class="truncate text-sm">{{ title }}</p>
|
||||
<p v-else class="truncate text-sm" v-html="matchHtml" />
|
||||
|
||||
<p v-if="matchKey === 'subtitle'" class="truncate text-xs text-gray-300">{{ matchHtml }}</p>
|
||||
|
||||
<p v-if="matchKey !== 'authorFL'" class="text-xs text-gray-200 truncate">by {{ authorFL }}</p>
|
||||
<p v-else class="truncate text-xs text-gray-200" v-html="matchHtml" />
|
||||
|
||||
<div v-if="matchKey === 'series' || matchKey === 'tags'" class="m-0 p-0 truncate" v-html="matchHtml" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
audiobook: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
search: String,
|
||||
matchKey: String,
|
||||
matchText: String
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
bookCoverAspectRatio() {
|
||||
return this.$store.getters['getBookCoverAspectRatio']
|
||||
},
|
||||
coverWidth() {
|
||||
if (this.bookCoverAspectRatio === 1) return 50 * 1.2
|
||||
return 50
|
||||
},
|
||||
book() {
|
||||
return this.audiobook ? this.audiobook.book || {} : {}
|
||||
},
|
||||
title() {
|
||||
return this.book ? this.book.title : 'No Title'
|
||||
},
|
||||
subtitle() {
|
||||
return this.book ? this.book.subtitle : ''
|
||||
},
|
||||
authorFL() {
|
||||
return this.book ? this.book.authorFL : 'Unknown'
|
||||
},
|
||||
matchHtml() {
|
||||
if (!this.matchText || !this.search) return ''
|
||||
if (this.matchKey === 'subtitle') return ''
|
||||
var matchSplit = this.matchText.toLowerCase().split(this.search.toLowerCase().trim())
|
||||
if (matchSplit.length < 2) return ''
|
||||
|
||||
var html = ''
|
||||
var totalLenSoFar = 0
|
||||
for (let i = 0; i < matchSplit.length - 1; i++) {
|
||||
var indexOf = matchSplit[i].length
|
||||
var firstPart = this.matchText.substr(totalLenSoFar, indexOf)
|
||||
var actualWasThere = this.matchText.substr(totalLenSoFar + indexOf, this.search.length)
|
||||
totalLenSoFar += indexOf + this.search.length
|
||||
|
||||
html += `${firstPart}<strong class="text-warning">${actualWasThere}</strong>`
|
||||
}
|
||||
var lastPart = this.matchText.substr(totalLenSoFar)
|
||||
html += lastPart
|
||||
|
||||
if (this.matchKey === 'tags') return `<p class="truncate">Tags: ${html}</p>`
|
||||
if (this.matchKey === 'authorFL') return `by ${html}`
|
||||
if (this.matchKey === 'series') return `<p class="truncate">Series: ${html}</p>`
|
||||
return `${html}`
|
||||
}
|
||||
},
|
||||
methods: {},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.audiobookSearchCardContent {
|
||||
width: calc(100% - 80px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
264
components/cards/LazyBookCard.vue
Normal file
264
components/cards/LazyBookCard.vue
Normal file
|
@ -0,0 +1,264 @@
|
|||
<template>
|
||||
<div ref="card" :id="`book-card-${index}`" :style="{ width: width + 'px', minWidth: width + 'px', height: height + 'px' }" class="rounded-sm z-10 bg-primary cursor-pointer box-shadow-book" @click="clickCard">
|
||||
<!-- When cover image does not fill -->
|
||||
<div v-show="showCoverBg" class="absolute top-0 left-0 w-full h-full overflow-hidden rounded-sm bg-primary">
|
||||
<div class="absolute cover-bg" ref="coverBg" />
|
||||
</div>
|
||||
|
||||
<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' }">
|
||||
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }" class="font-book text-gray-300 text-center">{{ title }}</p>
|
||||
</div>
|
||||
|
||||
<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 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 v-if="showError" :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>
|
||||
|
||||
<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>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
index: Number,
|
||||
width: {
|
||||
type: Number,
|
||||
default: 120
|
||||
},
|
||||
height: {
|
||||
type: Number,
|
||||
default: 192
|
||||
},
|
||||
bookCoverAspectRatio: Number,
|
||||
showVolumeNumber: Boolean,
|
||||
bookMount: {
|
||||
type: Object,
|
||||
default: () => null
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isHovering: false,
|
||||
isMoreMenuOpen: false,
|
||||
isProcessingReadUpdate: false,
|
||||
audiobook: null,
|
||||
imageReady: false,
|
||||
rescanning: false,
|
||||
selected: false,
|
||||
isSelectionMode: false,
|
||||
showCoverBg: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
_audiobook() {
|
||||
return this.audiobook || {}
|
||||
},
|
||||
placeholderUrl() {
|
||||
return '/book_placeholder.jpg'
|
||||
},
|
||||
bookCoverSrc() {
|
||||
return this.store.getters['audiobooks/getBookCoverSrc'](this._audiobook, this.placeholderUrl)
|
||||
},
|
||||
audiobookId() {
|
||||
return this._audiobook.id
|
||||
},
|
||||
hasEbook() {
|
||||
return this._audiobook.numEbooks
|
||||
},
|
||||
hasTracks() {
|
||||
return this._audiobook.numTracks
|
||||
},
|
||||
book() {
|
||||
return this._audiobook.book || {}
|
||||
},
|
||||
hasCover() {
|
||||
return !!this.book.cover
|
||||
},
|
||||
squareAspectRatio() {
|
||||
return this.bookCoverAspectRatio === 1
|
||||
},
|
||||
sizeMultiplier() {
|
||||
var baseSize = this.squareAspectRatio ? 192 : 120
|
||||
return this.width / baseSize
|
||||
},
|
||||
title() {
|
||||
return this.book.title || ''
|
||||
},
|
||||
author() {
|
||||
return this.book.author
|
||||
},
|
||||
authorFL() {
|
||||
return this.book.authorFL || this.author
|
||||
},
|
||||
authorLF() {
|
||||
return this.book.authorLF || this.author
|
||||
},
|
||||
volumeNumber() {
|
||||
return this.book.volumeNumber || null
|
||||
},
|
||||
userProgress() {
|
||||
return this.store.getters['user/getUserAudiobook'](this.audiobookId)
|
||||
},
|
||||
userProgressPercent() {
|
||||
return this.userProgress ? this.userProgress.progress || 0 : 0
|
||||
},
|
||||
userIsRead() {
|
||||
return this.userProgress ? !!this.userProgress.isRead : false
|
||||
},
|
||||
showError() {
|
||||
return this.hasMissingParts || this.hasInvalidParts || this.isMissing || this.isInvalid
|
||||
},
|
||||
isStreaming() {
|
||||
return this.store.getters['getAudiobookIdStreaming'] === this.audiobookId
|
||||
},
|
||||
isMissing() {
|
||||
return this._audiobook.isMissing
|
||||
},
|
||||
isInvalid() {
|
||||
return this._audiobook.isInvalid
|
||||
},
|
||||
hasMissingParts() {
|
||||
return this._audiobook.hasMissingParts
|
||||
},
|
||||
hasInvalidParts() {
|
||||
return this._audiobook.hasInvalidParts
|
||||
},
|
||||
errorText() {
|
||||
if (this.isMissing) return 'Audiobook directory is missing!'
|
||||
else if (this.isInvalid) return 'Audiobook has no audio tracks & ebook'
|
||||
var txt = ''
|
||||
if (this.hasMissingParts) {
|
||||
txt = `${this.hasMissingParts} missing parts.`
|
||||
}
|
||||
if (this.hasInvalidParts) {
|
||||
if (this.hasMissingParts) txt += ' '
|
||||
txt += `${this.hasInvalidParts} invalid parts.`
|
||||
}
|
||||
return txt || 'Unknown Error'
|
||||
},
|
||||
store() {
|
||||
return this.$store || this.$nuxt.$store
|
||||
},
|
||||
userCanUpdate() {
|
||||
return this.store.getters['user/getUserCanUpdate']
|
||||
},
|
||||
userCanDelete() {
|
||||
return this.store.getters['user/getUserCanDelete']
|
||||
},
|
||||
userCanDownload() {
|
||||
return this.store.getters['user/getUserCanDownload']
|
||||
},
|
||||
userIsRoot() {
|
||||
return this.store.getters['user/getIsRoot']
|
||||
},
|
||||
_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: {
|
||||
setSelectionMode(val) {
|
||||
this.isSelectionMode = val
|
||||
if (!val) this.selected = false
|
||||
},
|
||||
setEntity(audiobook) {
|
||||
this.audiobook = audiobook
|
||||
},
|
||||
clickCard(e) {
|
||||
if (this.isSelectionMode) {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
this.selectBtnClick()
|
||||
} else {
|
||||
var router = this.$router || this.$nuxt.$router
|
||||
if (router) router.push(`/audiobook/${this.audiobookId}`)
|
||||
}
|
||||
},
|
||||
selectBtnClick() {
|
||||
this.selected = !this.selected
|
||||
this.$emit('select', this.audiobook)
|
||||
},
|
||||
destroy() {
|
||||
// destroy the vue listeners, etc
|
||||
this.$destroy()
|
||||
|
||||
// remove the element from the DOM
|
||||
if (this.$el && this.$el.parentNode) {
|
||||
this.$el.parentNode.removeChild(this.$el)
|
||||
} else if (this.$el && this.$el.remove) {
|
||||
this.$el.remove()
|
||||
}
|
||||
},
|
||||
setCoverBg() {
|
||||
if (this.$refs.coverBg) {
|
||||
this.$refs.coverBg.style.backgroundImage = `url("${this.bookCoverSrc}")`
|
||||
}
|
||||
},
|
||||
imageLoaded() {
|
||||
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 - this.bookCoverAspectRatio)
|
||||
|
||||
// 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() {
|
||||
if (this.bookMount) {
|
||||
this.setEntity(this.bookMount)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
82
components/cards/LazyCollectionCard.vue
Normal file
82
components/cards/LazyCollectionCard.vue
Normal file
|
@ -0,0 +1,82 @@
|
|||
<template>
|
||||
<div ref="card" :id="`collection-card-${index}`" :style="{ width: width + 'px', height: height + 'px' }" class="rounded-sm cursor-pointer z-30" @click="clickCard">
|
||||
<div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" />
|
||||
<div class="w-full h-full bg-primary relative rounded overflow-hidden">
|
||||
<covers-collection-cover ref="cover" :book-items="books" :width="width" :height="height" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||
</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="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
|
||||
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
index: Number,
|
||||
width: Number,
|
||||
height: Number,
|
||||
bookCoverAspectRatio: Number
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
collection: null,
|
||||
isSelectionMode: false,
|
||||
selected: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
labelFontSize() {
|
||||
if (this.width < 160) return 0.75
|
||||
return 0.875
|
||||
},
|
||||
sizeMultiplier() {
|
||||
if (this.bookCoverAspectRatio === 1) return this.width / (120 * 1.6 * 2)
|
||||
return this.width / 240
|
||||
},
|
||||
title() {
|
||||
return this.collection ? this.collection.name : ''
|
||||
},
|
||||
books() {
|
||||
return this.collection ? this.collection.books || [] : []
|
||||
},
|
||||
store() {
|
||||
return this.$store || this.$nuxt.$store
|
||||
},
|
||||
currentLibraryId() {
|
||||
return this.store.state.libraries.currentLibraryId
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setEntity(_collection) {
|
||||
this.collection = _collection
|
||||
},
|
||||
setSelectionMode(val) {
|
||||
this.isSelectionMode = val
|
||||
},
|
||||
clickCard() {
|
||||
if (!this.collection) return
|
||||
var router = this.$router || this.$nuxt.$router
|
||||
router.push(`/collection/${this.collection.id}`)
|
||||
},
|
||||
clickEdit() {
|
||||
this.$emit('edit', this.collection)
|
||||
},
|
||||
destroy() {
|
||||
// destroy the vue listeners, etc
|
||||
this.$destroy()
|
||||
|
||||
// remove the element from the DOM
|
||||
if (this.$el && this.$el.parentNode) {
|
||||
this.$el.parentNode.removeChild(this.$el)
|
||||
} else if (this.$el && this.$el.remove) {
|
||||
this.$el.remove()
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
100
components/cards/LazySeriesCard.vue
Normal file
100
components/cards/LazySeriesCard.vue
Normal file
|
@ -0,0 +1,100 @@
|
|||
<template>
|
||||
<div ref="card" :id="`series-card-${index}`" :style="{ width: width + 'px', height: height + 'px' }" class="rounded-sm cursor-pointer z-30" @click="clickCard">
|
||||
<div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" />
|
||||
<div class="w-full h-full bg-primary relative rounded overflow-hidden">
|
||||
<covers-group-cover v-if="series" ref="cover" :id="seriesId" :name="title" :book-items="books" :width="width" :height="height" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||
</div>
|
||||
|
||||
<div v-if="!isCategorized" 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="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
|
||||
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
index: Number,
|
||||
width: Number,
|
||||
height: Number,
|
||||
bookCoverAspectRatio: Number,
|
||||
seriesMount: {
|
||||
type: Object,
|
||||
default: () => null
|
||||
},
|
||||
isCategorized: Boolean
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
series: null,
|
||||
isSelectionMode: false,
|
||||
selected: false,
|
||||
imageReady: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
labelFontSize() {
|
||||
if (this.width < 160) return 0.75
|
||||
return 0.875
|
||||
},
|
||||
sizeMultiplier() {
|
||||
if (this.bookCoverAspectRatio === 1) return this.width / (120 * 1.6 * 2)
|
||||
return this.width / 240
|
||||
},
|
||||
title() {
|
||||
return this.series ? this.series.name : ''
|
||||
},
|
||||
books() {
|
||||
return this.series ? this.series.books || [] : []
|
||||
},
|
||||
store() {
|
||||
return this.$store || this.$nuxt.$store
|
||||
},
|
||||
currentLibraryId() {
|
||||
return this.store.state.libraries.currentLibraryId
|
||||
},
|
||||
seriesId() {
|
||||
return this.series ? this.$encode(this.series.id) : null
|
||||
},
|
||||
hasValidCovers() {
|
||||
var validCovers = this.books.map((bookItem) => bookItem.book.cover)
|
||||
return !!validCovers.length
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setEntity(_series) {
|
||||
this.series = _series
|
||||
},
|
||||
setSelectionMode(val) {
|
||||
this.isSelectionMode = val
|
||||
},
|
||||
clickCard() {
|
||||
if (!this.series) return
|
||||
var router = this.$router || this.$nuxt.$router
|
||||
router.push(`/bookshelf/series/${this.seriesId}`)
|
||||
},
|
||||
imageLoaded() {
|
||||
this.imageReady = true
|
||||
},
|
||||
destroy() {
|
||||
// destroy the vue listeners, etc
|
||||
this.$destroy()
|
||||
|
||||
// remove the element from the DOM
|
||||
if (this.$el && this.$el.parentNode) {
|
||||
this.$el.parentNode.removeChild(this.$el)
|
||||
} else if (this.$el && this.$el.remove) {
|
||||
this.$el.remove()
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.seriesMount) {
|
||||
this.setEntity(this.seriesMount)
|
||||
}
|
||||
},
|
||||
beforeDestroy() {}
|
||||
}
|
||||
</script>
|
39
components/cards/SeriesSearchCard.vue
Normal file
39
components/cards/SeriesSearchCard.vue
Normal file
|
@ -0,0 +1,39 @@
|
|||
<template>
|
||||
<div class="flex h-full px-1 overflow-hidden">
|
||||
<covers-group-cover :name="series" :book-items="bookItems" :width="80" :height="60" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||
<div class="flex-grow px-2 seriesSearchCardContent h-full">
|
||||
<p class="truncate text-sm">{{ series }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
series: String,
|
||||
bookItems: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
bookCoverAspectRatio() {
|
||||
return this.$store.getters['getBookCoverAspectRatio']
|
||||
}
|
||||
},
|
||||
methods: {},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.seriesSearchCardContent {
|
||||
width: calc(100% - 80px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
383
components/covers/BookCover.vue
Normal file
383
components/covers/BookCover.vue
Normal file
|
@ -0,0 +1,383 @@
|
|||
<template>
|
||||
<div class="relative rounded-sm overflow-hidden" :style="{ height: height + 'px', width: width + 'px', maxWidth: width + 'px', minWidth: width + 'px' }">
|
||||
<div class="w-full h-full relative bg-bg">
|
||||
<div v-show="showCoverBg" class="absolute top-0 left-0 w-full h-full overflow-hidden rounded-sm bg-primary">
|
||||
<div class="absolute cover-bg" ref="coverBg" />
|
||||
</div>
|
||||
|
||||
<img v-if="audiobook" ref="cover" :src="fullCoverUrl" loading="lazy" @error="imageError" @load="imageLoaded" class="w-full h-full absolute top-0 left-0 z-10 duration-300 transition-opacity" :style="{ opacity: imageReady ? '1' : '0' }" :class="showCoverBg ? 'object-contain' : 'object-cover'" />
|
||||
<div v-show="loading && audiobook" class="absolute top-0 left-0 h-full w-full flex items-center justify-center">
|
||||
<p class="font-book text-center" :style="{ fontSize: 0.75 * sizeMultiplier + 'rem' }">{{ title }}</p>
|
||||
<div class="absolute top-2 right-2">
|
||||
<div class="la-ball-spin-clockwise la-sm">
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="imageFailed" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full bg-red-100" :style="{ padding: placeholderCoverPadding + 'rem' }">
|
||||
<div class="w-full h-full border-2 border-error flex flex-col items-center justify-center">
|
||||
<img src="/Logo.png" loading="lazy" class="mb-2" :style="{ height: 64 * sizeMultiplier + 'px' }" />
|
||||
<p class="text-center font-book text-error" :style="{ fontSize: titleFontSize + 'rem' }">Invalid Cover</p>
|
||||
</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 z-10" :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 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>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
audiobook: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
authorOverride: String,
|
||||
width: {
|
||||
type: Number,
|
||||
default: 120
|
||||
},
|
||||
bookCoverAspectRatio: Number,
|
||||
downloadCover: String
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
imageFailed: false,
|
||||
showCoverBg: false,
|
||||
imageReady: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
cover() {
|
||||
this.imageFailed = false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
squareAspectRatio() {
|
||||
return this.bookCoverAspectRatio === 1
|
||||
},
|
||||
height() {
|
||||
return this.width * this.bookCoverAspectRatio
|
||||
},
|
||||
book() {
|
||||
if (!this.audiobook) return {}
|
||||
return this.audiobook.book || {}
|
||||
},
|
||||
title() {
|
||||
return this.book.title || 'No Title'
|
||||
},
|
||||
titleCleaned() {
|
||||
if (this.title.length > 60) {
|
||||
return this.title.slice(0, 57) + '...'
|
||||
}
|
||||
return this.title
|
||||
},
|
||||
author() {
|
||||
if (this.authorOverride) return this.authorOverride
|
||||
return this.book.author || 'Unknown'
|
||||
},
|
||||
authorCleaned() {
|
||||
if (this.author.length > 30) {
|
||||
return this.author.slice(0, 27) + '...'
|
||||
}
|
||||
return this.author
|
||||
},
|
||||
placeholderUrl() {
|
||||
return '/book_placeholder.jpg'
|
||||
},
|
||||
fullCoverUrl() {
|
||||
if (this.downloadCover) return this.downloadCover
|
||||
if (!this.audiobook) return null
|
||||
var store = this.$store || this.$nuxt.$store
|
||||
return store.getters['audiobooks/getBookCoverSrc'](this.audiobook, this.placeholderUrl)
|
||||
},
|
||||
cover() {
|
||||
return this.book.cover || this.placeholderUrl
|
||||
},
|
||||
hasCover() {
|
||||
return !!this.book.cover
|
||||
},
|
||||
sizeMultiplier() {
|
||||
var baseSize = this.squareAspectRatio ? 192 : 120
|
||||
return this.width / baseSize
|
||||
},
|
||||
titleFontSize() {
|
||||
return 0.75 * this.sizeMultiplier
|
||||
},
|
||||
authorFontSize() {
|
||||
return 0.6 * this.sizeMultiplier
|
||||
},
|
||||
placeholderCoverPadding() {
|
||||
return 0.8 * this.sizeMultiplier
|
||||
},
|
||||
authorBottom() {
|
||||
return 0.75 * this.sizeMultiplier
|
||||
},
|
||||
userToken() {
|
||||
return this.$store.getters['user/getToken']
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setCoverBg() {
|
||||
if (this.$refs.coverBg) {
|
||||
this.$refs.coverBg.style.backgroundImage = `url("${this.fullCoverUrl}")`
|
||||
}
|
||||
},
|
||||
hideCoverBg() {},
|
||||
imageLoaded() {
|
||||
this.loading = false
|
||||
this.$nextTick(() => {
|
||||
this.imageReady = true
|
||||
})
|
||||
if (this.$refs.cover && this.cover !== this.placeholderUrl) {
|
||||
var { naturalWidth, naturalHeight } = this.$refs.cover
|
||||
var aspectRatio = naturalHeight / naturalWidth
|
||||
var arDiff = Math.abs(aspectRatio - this.bookCoverAspectRatio)
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
},
|
||||
imageError(err) {
|
||||
this.loading = false
|
||||
console.error('ImgError', err)
|
||||
this.imageFailed = true
|
||||
}
|
||||
},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/*!
|
||||
* Load Awesome v1.1.0 (http://github.danielcardoso.net/load-awesome/)
|
||||
* Copyright 2015 Daniel Cardoso <@DanielCardoso>
|
||||
* Licensed under MIT
|
||||
*/
|
||||
.la-ball-spin-clockwise,
|
||||
.la-ball-spin-clockwise > div {
|
||||
position: relative;
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.la-ball-spin-clockwise {
|
||||
display: block;
|
||||
font-size: 0;
|
||||
color: #fff;
|
||||
}
|
||||
.la-ball-spin-clockwise.la-dark {
|
||||
color: #262626;
|
||||
}
|
||||
.la-ball-spin-clockwise > div {
|
||||
display: inline-block;
|
||||
float: none;
|
||||
background-color: currentColor;
|
||||
border: 0 solid currentColor;
|
||||
}
|
||||
.la-ball-spin-clockwise {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
.la-ball-spin-clockwise > div {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
margin-top: -4px;
|
||||
margin-left: -4px;
|
||||
border-radius: 100%;
|
||||
-webkit-animation: ball-spin-clockwise 1s infinite ease-in-out;
|
||||
-moz-animation: ball-spin-clockwise 1s infinite ease-in-out;
|
||||
-o-animation: ball-spin-clockwise 1s infinite ease-in-out;
|
||||
animation: ball-spin-clockwise 1s infinite ease-in-out;
|
||||
}
|
||||
.la-ball-spin-clockwise > div:nth-child(1) {
|
||||
top: 5%;
|
||||
left: 50%;
|
||||
-webkit-animation-delay: -0.875s;
|
||||
-moz-animation-delay: -0.875s;
|
||||
-o-animation-delay: -0.875s;
|
||||
animation-delay: -0.875s;
|
||||
}
|
||||
.la-ball-spin-clockwise > div:nth-child(2) {
|
||||
top: 18.1801948466%;
|
||||
left: 81.8198051534%;
|
||||
-webkit-animation-delay: -0.75s;
|
||||
-moz-animation-delay: -0.75s;
|
||||
-o-animation-delay: -0.75s;
|
||||
animation-delay: -0.75s;
|
||||
}
|
||||
.la-ball-spin-clockwise > div:nth-child(3) {
|
||||
top: 50%;
|
||||
left: 95%;
|
||||
-webkit-animation-delay: -0.625s;
|
||||
-moz-animation-delay: -0.625s;
|
||||
-o-animation-delay: -0.625s;
|
||||
animation-delay: -0.625s;
|
||||
}
|
||||
.la-ball-spin-clockwise > div:nth-child(4) {
|
||||
top: 81.8198051534%;
|
||||
left: 81.8198051534%;
|
||||
-webkit-animation-delay: -0.5s;
|
||||
-moz-animation-delay: -0.5s;
|
||||
-o-animation-delay: -0.5s;
|
||||
animation-delay: -0.5s;
|
||||
}
|
||||
.la-ball-spin-clockwise > div:nth-child(5) {
|
||||
top: 94.9999999966%;
|
||||
left: 50.0000000005%;
|
||||
-webkit-animation-delay: -0.375s;
|
||||
-moz-animation-delay: -0.375s;
|
||||
-o-animation-delay: -0.375s;
|
||||
animation-delay: -0.375s;
|
||||
}
|
||||
.la-ball-spin-clockwise > div:nth-child(6) {
|
||||
top: 81.8198046966%;
|
||||
left: 18.1801949248%;
|
||||
-webkit-animation-delay: -0.25s;
|
||||
-moz-animation-delay: -0.25s;
|
||||
-o-animation-delay: -0.25s;
|
||||
animation-delay: -0.25s;
|
||||
}
|
||||
.la-ball-spin-clockwise > div:nth-child(7) {
|
||||
top: 49.9999750815%;
|
||||
left: 5.0000051215%;
|
||||
-webkit-animation-delay: -0.125s;
|
||||
-moz-animation-delay: -0.125s;
|
||||
-o-animation-delay: -0.125s;
|
||||
animation-delay: -0.125s;
|
||||
}
|
||||
.la-ball-spin-clockwise > div:nth-child(8) {
|
||||
top: 18.179464974%;
|
||||
left: 18.1803700518%;
|
||||
-webkit-animation-delay: 0s;
|
||||
-moz-animation-delay: 0s;
|
||||
-o-animation-delay: 0s;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
.la-ball-spin-clockwise.la-sm {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
.la-ball-spin-clockwise.la-sm > div {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
margin-top: -2px;
|
||||
margin-left: -2px;
|
||||
}
|
||||
.la-ball-spin-clockwise.la-2x {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
.la-ball-spin-clockwise.la-2x > div {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-top: -8px;
|
||||
margin-left: -8px;
|
||||
}
|
||||
.la-ball-spin-clockwise.la-3x {
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
}
|
||||
.la-ball-spin-clockwise.la-3x > div {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-top: -12px;
|
||||
margin-left: -12px;
|
||||
}
|
||||
/*
|
||||
* Animation
|
||||
*/
|
||||
@-webkit-keyframes ball-spin-clockwise {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
-webkit-transform: scale(1);
|
||||
transform: scale(1);
|
||||
}
|
||||
20% {
|
||||
opacity: 1;
|
||||
}
|
||||
80% {
|
||||
opacity: 0;
|
||||
-webkit-transform: scale(0);
|
||||
transform: scale(0);
|
||||
}
|
||||
}
|
||||
@-moz-keyframes ball-spin-clockwise {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
-moz-transform: scale(1);
|
||||
transform: scale(1);
|
||||
}
|
||||
20% {
|
||||
opacity: 1;
|
||||
}
|
||||
80% {
|
||||
opacity: 0;
|
||||
-moz-transform: scale(0);
|
||||
transform: scale(0);
|
||||
}
|
||||
}
|
||||
@-o-keyframes ball-spin-clockwise {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
-o-transform: scale(1);
|
||||
transform: scale(1);
|
||||
}
|
||||
20% {
|
||||
opacity: 1;
|
||||
}
|
||||
80% {
|
||||
opacity: 0;
|
||||
-o-transform: scale(0);
|
||||
transform: scale(0);
|
||||
}
|
||||
}
|
||||
@keyframes ball-spin-clockwise {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
-webkit-transform: scale(1);
|
||||
-moz-transform: scale(1);
|
||||
-o-transform: scale(1);
|
||||
transform: scale(1);
|
||||
}
|
||||
20% {
|
||||
opacity: 1;
|
||||
}
|
||||
80% {
|
||||
opacity: 0;
|
||||
-webkit-transform: scale(0);
|
||||
-moz-transform: scale(0);
|
||||
-o-transform: scale(0);
|
||||
transform: scale(0);
|
||||
}
|
||||
}
|
||||
</style>
|
61
components/covers/CollectionCover.vue
Normal file
61
components/covers/CollectionCover.vue
Normal file
|
@ -0,0 +1,61 @@
|
|||
<template>
|
||||
<div class="relative rounded-sm overflow-hidden" :style="{ width: width + 'px', height: height + 'px' }">
|
||||
<div v-if="hasOwnCover" class="w-full h-full relative rounded-sm">
|
||||
<div v-if="showCoverBg" class="bg-primary absolute top-0 left-0 w-full h-full">
|
||||
<div class="w-full h-full z-0" ref="coverBg" />
|
||||
</div>
|
||||
<img ref="cover" :src="fullCoverUrl" @error="imageError" @load="imageLoaded" class="w-full h-full absolute top-0 left-0" :class="showCoverBg ? 'object-contain' : 'object-cover'" />
|
||||
</div>
|
||||
<div v-else-if="books.length" class="flex justify-center h-full relative bg-primary bg-opacity-95 rounded-sm">
|
||||
<div class="absolute top-0 left-0 w-full h-full bg-gray-400 bg-opacity-5" />
|
||||
|
||||
<covers-book-cover :audiobook="books[0]" :width="width / 2" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||
<covers-book-cover v-if="books.length > 1" :audiobook="books[1]" :width="width / 2" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||
</div>
|
||||
<div v-else class="relative w-full h-full flex items-center justify-center p-2 bg-primary rounded-sm">
|
||||
<div class="absolute top-0 left-0 w-full h-full bg-gray-400 bg-opacity-5" />
|
||||
|
||||
<p class="font-book text-white text-opacity-60 text-center" :style="{ fontSize: Math.min(1, sizeMultiplier) + 'rem' }">Empty Collection</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
bookItems: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
width: Number,
|
||||
height: Number,
|
||||
bookCoverAspectRatio: Number
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
imageFailed: false,
|
||||
showCoverBg: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
sizeMultiplier() {
|
||||
if (this.bookCoverAspectRatio === 1) return this.width / (120 * 1.6 * 2)
|
||||
return this.width / 240
|
||||
},
|
||||
hasOwnCover() {
|
||||
return false
|
||||
},
|
||||
fullCoverUrl() {
|
||||
return null
|
||||
},
|
||||
books() {
|
||||
return this.bookItems || []
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
imageError() {},
|
||||
imageLoaded() {}
|
||||
},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
225
components/covers/GroupCover.vue
Normal file
225
components/covers/GroupCover.vue
Normal file
|
@ -0,0 +1,225 @@
|
|||
<template>
|
||||
<div ref="wrapper" :style="{ height: height + 'px', width: width + 'px' }" class="relative">
|
||||
<div v-if="noValidCovers" class="absolute top-0 left-0 w-full h-full flex items-center justify-center box-shadow-book" :style="{ padding: `${sizeMultiplier}rem` }">
|
||||
<p :style="{ fontSize: sizeMultiplier + 'rem' }">{{ name }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
id: String,
|
||||
name: String,
|
||||
bookItems: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
width: Number,
|
||||
height: Number,
|
||||
bookCoverAspectRatio: Number
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
noValidCovers: false,
|
||||
coverDiv: null,
|
||||
isHovering: false,
|
||||
coverWrapperEl: null,
|
||||
coverImageEls: [],
|
||||
coverWidth: 0,
|
||||
offsetIncrement: 0,
|
||||
isFannedOut: false,
|
||||
isDetached: false,
|
||||
isAttaching: false,
|
||||
windowWidth: 0,
|
||||
isInit: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
bookItems: {
|
||||
immediate: true,
|
||||
handler(newVal) {
|
||||
if (newVal) {
|
||||
// ensure wrapper is initialized
|
||||
this.$nextTick(this.init)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
sizeMultiplier() {
|
||||
if (this.bookCoverAspectRatio === 1) return this.width / (100 * 1.6 * 2)
|
||||
return this.width / 200
|
||||
},
|
||||
store() {
|
||||
return this.$store || this.$nuxt.$store
|
||||
},
|
||||
router() {
|
||||
return this.$router || this.$nuxt.$router
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
detchCoverWrapper() {
|
||||
if (!this.coverWrapperEl || !this.$refs.wrapper || this.isDetached) return
|
||||
|
||||
this.coverWrapperEl.remove()
|
||||
|
||||
this.isDetached = true
|
||||
document.body.appendChild(this.coverWrapperEl)
|
||||
this.coverWrapperEl.addEventListener('mouseleave', this.mouseleaveCover)
|
||||
|
||||
this.coverWrapperEl.style.position = 'absolute'
|
||||
this.coverWrapperEl.style.zIndex = 40
|
||||
|
||||
this.updatePosition()
|
||||
},
|
||||
attachCoverWrapper() {
|
||||
if (!this.coverWrapperEl || !this.$refs.wrapper || !this.isDetached) return
|
||||
|
||||
this.coverWrapperEl.remove()
|
||||
this.coverWrapperEl.style.position = 'relative'
|
||||
this.coverWrapperEl.style.left = 'unset'
|
||||
this.coverWrapperEl.style.top = 'unset'
|
||||
this.coverWrapperEl.style.width = this.$refs.wrapper.clientWidth + 'px'
|
||||
|
||||
this.$refs.wrapper.appendChild(this.coverWrapperEl)
|
||||
|
||||
this.isDetached = false
|
||||
},
|
||||
updatePosition() {
|
||||
var rect = this.$refs.wrapper.getBoundingClientRect()
|
||||
this.coverWrapperEl.style.top = rect.top + window.scrollY + 'px'
|
||||
|
||||
this.coverWrapperEl.style.left = rect.left + window.scrollX + 4 + 'px'
|
||||
|
||||
this.coverWrapperEl.style.height = rect.height + 'px'
|
||||
this.coverWrapperEl.style.width = rect.width + 'px'
|
||||
},
|
||||
getCoverUrl(book) {
|
||||
return this.store.getters['audiobooks/getBookCoverSrc'](book, '')
|
||||
},
|
||||
async buildCoverImg(coverData, bgCoverWidth, offsetLeft, zIndex, forceCoverBg = false) {
|
||||
var src = coverData.coverUrl
|
||||
|
||||
var showCoverBg =
|
||||
forceCoverBg ||
|
||||
(await new Promise((resolve) => {
|
||||
var image = new Image()
|
||||
|
||||
image.onload = () => {
|
||||
var { naturalWidth, naturalHeight } = image
|
||||
var aspectRatio = naturalHeight / naturalWidth
|
||||
var arDiff = Math.abs(aspectRatio - this.bookCoverAspectRatio)
|
||||
|
||||
// If image aspect ratio is <= 1.45 or >= 1.75 then use cover bg, otherwise stretch to fit
|
||||
if (arDiff > 0.15) {
|
||||
resolve(true)
|
||||
} else {
|
||||
resolve(false)
|
||||
}
|
||||
}
|
||||
image.onerror = (err) => {
|
||||
console.error(err)
|
||||
resolve(false)
|
||||
}
|
||||
image.src = src
|
||||
}))
|
||||
|
||||
var imgdiv = document.createElement('div')
|
||||
imgdiv.style.height = this.height + 'px'
|
||||
imgdiv.style.width = bgCoverWidth + 'px'
|
||||
imgdiv.style.left = offsetLeft + 'px'
|
||||
imgdiv.style.zIndex = zIndex
|
||||
imgdiv.dataset.audiobookId = coverData.id
|
||||
imgdiv.dataset.volumeNumber = coverData.volumeNumber || ''
|
||||
imgdiv.className = 'absolute top-0 box-shadow-book transition-transform'
|
||||
imgdiv.style.boxShadow = '4px 0px 4px #11111166'
|
||||
// imgdiv.style.transform = 'skew(0deg, 15deg)'
|
||||
|
||||
if (showCoverBg) {
|
||||
var coverbgwrapper = document.createElement('div')
|
||||
coverbgwrapper.className = 'absolute top-0 left-0 w-full h-full overflow-hidden rounded-sm bg-primary'
|
||||
|
||||
var coverbg = document.createElement('div')
|
||||
coverbg.className = 'absolute cover-bg'
|
||||
coverbg.style.backgroundImage = `url("${src}")`
|
||||
|
||||
coverbgwrapper.appendChild(coverbg)
|
||||
imgdiv.appendChild(coverbgwrapper)
|
||||
}
|
||||
|
||||
var img = document.createElement('img')
|
||||
img.src = src
|
||||
img.className = 'absolute top-0 left-0 w-full h-full'
|
||||
img.style.objectFit = showCoverBg ? 'contain' : 'cover'
|
||||
|
||||
imgdiv.appendChild(img)
|
||||
return imgdiv
|
||||
},
|
||||
async init() {
|
||||
if (this.isInit) return
|
||||
this.isInit = true
|
||||
|
||||
if (this.coverDiv) {
|
||||
this.coverDiv.remove()
|
||||
this.coverDiv = null
|
||||
}
|
||||
var validCovers = this.bookItems
|
||||
.map((bookItem) => {
|
||||
return {
|
||||
id: bookItem.id,
|
||||
volumeNumber: bookItem.book ? bookItem.book.volumeNumber : null,
|
||||
coverUrl: this.getCoverUrl(bookItem)
|
||||
}
|
||||
})
|
||||
.filter((b) => b.coverUrl !== '')
|
||||
if (!validCovers.length) {
|
||||
this.noValidCovers = true
|
||||
return
|
||||
}
|
||||
this.noValidCovers = false
|
||||
|
||||
var coverWidth = this.width
|
||||
var widthPer = this.width
|
||||
if (validCovers.length > 1) {
|
||||
coverWidth = this.height / this.bookCoverAspectRatio
|
||||
widthPer = (this.width - coverWidth) / (validCovers.length - 1)
|
||||
}
|
||||
this.coverWidth = coverWidth
|
||||
this.offsetIncrement = widthPer
|
||||
|
||||
var outerdiv = document.createElement('div')
|
||||
outerdiv.id = `group-cover-${this.id || this.$encode(this.name)}`
|
||||
this.coverWrapperEl = outerdiv
|
||||
outerdiv.className = 'w-full h-full relative box-shadow-book'
|
||||
|
||||
var coverImageEls = []
|
||||
var offsetLeft = 0
|
||||
for (let i = 0; i < validCovers.length; i++) {
|
||||
offsetLeft = widthPer * i
|
||||
var zIndex = validCovers.length - i
|
||||
var img = await this.buildCoverImg(validCovers[i], coverWidth, offsetLeft, zIndex, validCovers.length === 1)
|
||||
outerdiv.appendChild(img)
|
||||
coverImageEls.push(img)
|
||||
}
|
||||
|
||||
this.coverImageEls = coverImageEls
|
||||
|
||||
if (this.$refs.wrapper) {
|
||||
this.coverDiv = outerdiv
|
||||
this.$refs.wrapper.appendChild(outerdiv)
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.windowWidth = window.innerWidth
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.coverWrapperEl) this.coverWrapperEl.remove()
|
||||
if (this.coverImageEls && this.coverImageEls.length) {
|
||||
this.coverImageEls.forEach((el) => el.remove())
|
||||
}
|
||||
if (this.coverDiv) this.coverDiv.remove()
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -5,9 +5,8 @@
|
|||
<nuxt-link to="/bookshelf/series" v-if="selectedSeriesName" class="pt-1">
|
||||
<span class="material-icons">arrow_back</span>
|
||||
</nuxt-link>
|
||||
<p v-show="!selectedSeriesName" class="font-book pt-1">{{ numEntities }} {{ entityTitle }}</p>
|
||||
<p v-show="selectedSeriesName" class="ml-2 font-book pt-1">{{ selectedSeriesName }}</p>
|
||||
|
||||
<p v-show="!selectedSeriesName" class="font-book pt-1">{{ totalEntities }} {{ entityTitle }}</p>
|
||||
<p v-show="selectedSeriesName" class="ml-2 font-book pt-1">{{ selectedSeriesName }} ({{ totalEntities }})</p>
|
||||
<div class="flex-grow" />
|
||||
<template v-if="page === 'library'">
|
||||
<span class="material-icons px-2" @click="changeView">{{ viewIcon }}</span>
|
||||
|
@ -32,12 +31,13 @@ export default {
|
|||
showSortModal: false,
|
||||
showFilterModal: false,
|
||||
settings: {},
|
||||
isListView: false
|
||||
isListView: false,
|
||||
totalEntities: 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
hasFilters() {
|
||||
return this.$store.getters['user/getUserSetting']('filterBy') !== 'all'
|
||||
return this.$store.getters['user/getUserSetting']('mobileFilterBy') !== 'all'
|
||||
},
|
||||
page() {
|
||||
var routeName = this.$route.name || ''
|
||||
|
@ -47,42 +47,17 @@ export default {
|
|||
return this.$route.query || {}
|
||||
},
|
||||
entityTitle() {
|
||||
if (this.page === 'library') return 'Audiobooks'
|
||||
if (this.page === 'library') return 'Books'
|
||||
else if (this.page === 'series') {
|
||||
if (this.selectedSeriesName) return 'Books in ' + this.selectedSeriesName
|
||||
return 'Series'
|
||||
} else if (this.page === 'collections') {
|
||||
return 'Collections'
|
||||
}
|
||||
return ''
|
||||
},
|
||||
numEntities() {
|
||||
if (this.page === 'library') return this.numAudiobooks
|
||||
else if (this.page === 'series') {
|
||||
if (this.selectedSeriesName) return this.numBooksInSeries
|
||||
return this.series.length
|
||||
} else if (this.page === 'collections') return this.numCollections
|
||||
return 0
|
||||
},
|
||||
series() {
|
||||
return this.$store.getters['audiobooks/getSeriesGroups']() || []
|
||||
},
|
||||
numCollections() {
|
||||
return (this.$store.state.user.collections || []).length
|
||||
},
|
||||
numAudiobooks() {
|
||||
return this.$store.getters['audiobooks/getFiltered']().length
|
||||
},
|
||||
numBooksInSeries() {
|
||||
return this.selectedSeries ? (this.selectedSeries.books || []).length : 0
|
||||
},
|
||||
selectedSeries() {
|
||||
if (!this.selectedSeriesName) return null
|
||||
return this.series.find((s) => s.name === this.selectedSeriesName)
|
||||
},
|
||||
selectedSeriesName() {
|
||||
if (this.page === 'series' && this.routeQuery.series) {
|
||||
return this.$decode(this.routeQuery.series)
|
||||
if (this.page === 'series' && this.$route.params.id) {
|
||||
return this.$decode(this.$route.params.id)
|
||||
}
|
||||
return null
|
||||
},
|
||||
|
@ -120,13 +95,18 @@ export default {
|
|||
for (const key in settings) {
|
||||
this.settings[key] = settings[key]
|
||||
}
|
||||
},
|
||||
setTotalEntities(total) {
|
||||
this.totalEntities = total
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.init()
|
||||
this.$eventBus.$on('bookshelf-total-entities', this.setTotalEntities)
|
||||
this.$store.commit('user/addSettingsListener', { id: 'bookshelftoolbar', meth: this.settingsUpdated })
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.$eventBus.$off('bookshelf-total-entities', this.setTotalEntities)
|
||||
this.$store.commit('user/removeSettingsListener', 'bookshelftoolbar')
|
||||
}
|
||||
}
|
||||
|
|
|
@ -81,10 +81,20 @@ export default {
|
|||
value: 'authors',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: 'Narrator',
|
||||
value: 'narrators',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: 'Progress',
|
||||
value: 'progress',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: 'Issues',
|
||||
value: 'issues',
|
||||
sublist: false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -118,16 +128,19 @@ export default {
|
|||
return this.selected && this.selected.includes('.') ? this.selected.split('.')[0] : false
|
||||
},
|
||||
genres() {
|
||||
return this.$store.getters['audiobooks/getGenresUsed']
|
||||
return this.filterData.genres || []
|
||||
},
|
||||
tags() {
|
||||
return this.$store.state.audiobooks.tags
|
||||
return this.filterData.tags || []
|
||||
},
|
||||
series() {
|
||||
return this.$store.state.audiobooks.series
|
||||
return this.filterData.series || []
|
||||
},
|
||||
authors() {
|
||||
return this.$store.getters['audiobooks/getUniqueAuthors']
|
||||
return this.filterData.authors || []
|
||||
},
|
||||
narrators() {
|
||||
return this.filterData.narrators || []
|
||||
},
|
||||
progress() {
|
||||
return ['Read', 'Unread', 'In Progress']
|
||||
|
@ -139,6 +152,9 @@ export default {
|
|||
value: this.$encode(item)
|
||||
}
|
||||
})
|
||||
},
|
||||
filterData() {
|
||||
return this.$store.state.libraries.filterData || {}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
|
|
@ -53,10 +53,8 @@ export default {
|
|||
methods: {
|
||||
async clickedOption(lib) {
|
||||
this.show = false
|
||||
|
||||
this.$store.commit('libraries/setCurrentLibrary', lib.id)
|
||||
await this.$store.dispatch('audiobooks/load')
|
||||
|
||||
await this.$store.dispatch('libraries/fetch', lib.id)
|
||||
this.$eventBus.$emit('library-changed', lib.id)
|
||||
this.$localStore.setCurrentLibrary(lib)
|
||||
}
|
||||
},
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
<p v-if="totalDuration">{{ totalDurationPretty }}</p>
|
||||
</div>
|
||||
<template v-for="book in booksCopy">
|
||||
<tables-collection-book-table-row :key="book.id" :book="book" :collection-id="collectionId" class="item" :class="drag ? '' : 'collection-book-item'" @edit="editBook" />
|
||||
<tables-collection-book-table-row :key="book.id" :book="book" :collection-id="collectionId" class="item collection-book-item" @edit="editBook" />
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -1,117 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<nuxt-link v-if="isConnected" to="/account" class="p-2 bg-white bg-opacity-10 border border-white border-opacity-40 rounded-full h-11 w-11 flex items-center justify-center">
|
||||
<span class="material-icons">person</span>
|
||||
</nuxt-link>
|
||||
<div v-else-if="processing" class="relative p-2 bg-warning bg-opacity-10 border border-warning border-opacity-40 rounded-full h-11 w-11 flex items-center justify-center">
|
||||
<div class="loader-dots block relative w-10 h-2.5">
|
||||
<div class="absolute top-0 mt-0.5 w-1.5 h-1.5 rounded-full bg-warning"></div>
|
||||
<div class="absolute top-0 mt-0.5 w-1.5 h-1.5 rounded-full bg-warning"></div>
|
||||
<div class="absolute top-0 mt-0.5 w-1.5 h-1.5 rounded-full bg-warning"></div>
|
||||
<div class="absolute top-0 mt-0.5 w-1.5 h-1.5 rounded-full bg-warning"></div>
|
||||
</div>
|
||||
</div>
|
||||
<nuxt-link v-else to="/connect" class="relative p-2 bg-warning bg-opacity-10 border border-warning border-opacity-40 rounded-full h-11 w-11 flex items-center justify-center">
|
||||
<span class="material-icons">{{ networkIcon }}</span>
|
||||
<!-- <div class="absolute top-0 left-0"> -->
|
||||
<!-- <div class="absolute -top-5 -right-5 overflow-hidden">
|
||||
<svg class="w-20 h-20 animate-spin" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
||||
<path clip-rule="evenodd" d="M15.165 8.53a.5.5 0 01-.404.58A7 7 0 1023 16a.5.5 0 011 0 8 8 0 11-9.416-7.874.5.5 0 01.58.404z" fill="currentColor" fill-rule="evenodd" />
|
||||
</svg>
|
||||
</div> -->
|
||||
</nuxt-link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {},
|
||||
data() {
|
||||
return {
|
||||
processing: false,
|
||||
serverUrl: null,
|
||||
isConnected: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
networkConnected(newVal) {
|
||||
if (newVal) {
|
||||
this.init()
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
user() {
|
||||
return this.$store.state.user.user
|
||||
},
|
||||
networkIcon() {
|
||||
if (!this.networkConnected) return 'signal_wifi_connected_no_internet_4'
|
||||
return 'cloud_off'
|
||||
},
|
||||
networkConnected() {
|
||||
return this.$store.state.networkConnected
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
socketConnected(val) {
|
||||
this.processing = false
|
||||
this.isConnected = val
|
||||
},
|
||||
async init() {
|
||||
if (this.isConnected) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.$server) {
|
||||
console.error('Invalid server not initialized')
|
||||
return
|
||||
}
|
||||
|
||||
if (!this.networkConnected) return
|
||||
|
||||
var localServerUrl = await this.$localStore.getServerUrl()
|
||||
var localUserToken = await this.$localStore.getToken()
|
||||
if (localServerUrl) {
|
||||
this.serverUrl = localServerUrl
|
||||
|
||||
// Server and Token are stored
|
||||
if (localUserToken) {
|
||||
this.processing = true
|
||||
var isSocketAlreadyEstablished = this.$server.socket
|
||||
var success = await this.$server.connect(localServerUrl, localUserToken)
|
||||
if (!success && !this.$server.url) {
|
||||
this.processing = false
|
||||
this.serverUrl = null
|
||||
} else if (!success) {
|
||||
this.processing = false
|
||||
} else if (isSocketAlreadyEstablished) {
|
||||
// No need to wait for connect event
|
||||
this.processing = false
|
||||
}
|
||||
} else {
|
||||
// Server only is stored
|
||||
var success = await this.$server.check(this.serverUrl)
|
||||
if (!success) {
|
||||
console.error('Invalid server')
|
||||
this.$server.setServerUrl(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (!this.$server) {
|
||||
console.error('Server not initalized in connection icon')
|
||||
return
|
||||
}
|
||||
if (this.$server.connected) {
|
||||
this.isConnected = true
|
||||
}
|
||||
this.$server.on('connected', this.socketConnected)
|
||||
this.init()
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.$server) this.$server.off('connected', this.socketConnected)
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -39,6 +39,12 @@ export default {
|
|||
},
|
||||
networkConnected() {
|
||||
return this.$store.state.networkConnected
|
||||
},
|
||||
currentLibraryId() {
|
||||
return this.$store.state.libraries.currentLibraryId
|
||||
},
|
||||
isSocketConnected() {
|
||||
return this.$store.state.socketConnected
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
@ -49,6 +55,7 @@ export default {
|
|||
|
||||
// Load libraries
|
||||
this.$store.dispatch('libraries/load')
|
||||
this.$store.dispatch('libraries/fetch', this.currentLibraryId)
|
||||
}
|
||||
},
|
||||
socketConnectionFailed(err) {
|
||||
|
@ -223,23 +230,23 @@ export default {
|
|||
var mediaFolder = mediaFolders.find((mf) => mf.name === download.folderName)
|
||||
if (mediaFolder) {
|
||||
console.log('Found download ' + download.folderName)
|
||||
|
||||
if (download.isPreparing || download.isDownloading) {
|
||||
download.isIncomplete = true
|
||||
download.isPreparing = false
|
||||
download.isDownloading = false
|
||||
if (download.isMissing) {
|
||||
download.isMissing = false
|
||||
this.$store.commit('downloads/addUpdateDownload', download)
|
||||
}
|
||||
|
||||
this.$store.commit('downloads/addUpdateDownload', download)
|
||||
this.$store.commit('audiobooks/addUpdate', download.audiobook)
|
||||
} else {
|
||||
console.error('Download not found ' + download.folderName)
|
||||
download.isMissing = true
|
||||
download.isPreparing = false
|
||||
download.isDownloading = false
|
||||
this.$store.commit('downloads/addUpdateDownload', download)
|
||||
if (!download.isMissing) {
|
||||
download.isMissing = true
|
||||
this.$store.commit('downloads/addUpdateDownload', download)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Match media scanned folders with books from server
|
||||
if (this.isSocketConnected) {
|
||||
await this.$store.dispatch('downloads/linkOrphanDownloads')
|
||||
}
|
||||
},
|
||||
async initMediaStore() {
|
||||
// Request and setup listeners for media files on native
|
||||
|
@ -253,7 +260,8 @@ export default {
|
|||
this.onDownloadProgress(data)
|
||||
})
|
||||
|
||||
var downloads = (await this.$sqlStore.getAllDownloads()) || []
|
||||
// var downloads = (await this.$sqlStore.getAllDownloads()) || []
|
||||
var downloads = await this.$store.dispatch('downloads/loadFromStorage')
|
||||
var downloadFolder = await this.$localStore.getDownloadFolder()
|
||||
|
||||
if (downloadFolder) {
|
||||
|
|
87
mixins/bookshelfCardsHelpers.js
Normal file
87
mixins/bookshelfCardsHelpers.js
Normal file
|
@ -0,0 +1,87 @@
|
|||
import Vue from 'vue'
|
||||
import LazyBookCard from '@/components/cards/LazyBookCard'
|
||||
import LazySeriesCard from '@/components/cards/LazySeriesCard'
|
||||
import LazyCollectionCard from '@/components/cards/LazyCollectionCard'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
cardsHelpers: {
|
||||
mountEntityCard: this.mountEntityCard
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getComponentClass() {
|
||||
if (this.entityName === 'series') return Vue.extend(LazySeriesCard)
|
||||
if (this.entityName === 'collections') return Vue.extend(LazyCollectionCard)
|
||||
return Vue.extend(LazyBookCard)
|
||||
},
|
||||
async mountEntityCard(index) {
|
||||
var shelf = Math.floor(index / this.entitiesPerShelf)
|
||||
var shelfEl = document.getElementById(`shelf-${shelf}`)
|
||||
if (!shelfEl) {
|
||||
console.error('invalid shelf', shelf, 'book index', index)
|
||||
return
|
||||
}
|
||||
this.entityIndexesMounted.push(index)
|
||||
if (this.entityComponentRefs[index]) {
|
||||
var bookComponent = this.entityComponentRefs[index]
|
||||
shelfEl.appendChild(bookComponent.$el)
|
||||
if (this.isSelectionMode) {
|
||||
bookComponent.setSelectionMode(true)
|
||||
if (this.selectedAudiobooks.includes(bookComponent.audiobookId) || this.isSelectAll) {
|
||||
bookComponent.selected = true
|
||||
} else {
|
||||
bookComponent.selected = false
|
||||
}
|
||||
} else {
|
||||
bookComponent.setSelectionMode(false)
|
||||
}
|
||||
bookComponent.isHovering = false
|
||||
return
|
||||
}
|
||||
var shelfOffsetY = this.isBookEntity ? 24 : 16
|
||||
var row = index % this.entitiesPerShelf
|
||||
var shelfOffsetX = row * this.totalEntityCardWidth + this.bookshelfMarginLeft
|
||||
|
||||
var ComponentClass = this.getComponentClass()
|
||||
var props = {
|
||||
index,
|
||||
width: this.entityWidth,
|
||||
height: this.entityHeight,
|
||||
bookCoverAspectRatio: this.bookCoverAspectRatio
|
||||
}
|
||||
if (this.entityName === 'series-books') props.showVolumeNumber = true
|
||||
|
||||
var _this = this
|
||||
var instance = new ComponentClass({
|
||||
propsData: props,
|
||||
created() {
|
||||
// this.$on('edit', (entity) => {
|
||||
// if (_this.editEntity) _this.editEntity(entity)
|
||||
// })
|
||||
// this.$on('select', (entity) => {
|
||||
// if (_this.selectEntity) _this.selectEntity(entity)
|
||||
// })
|
||||
}
|
||||
})
|
||||
this.entityComponentRefs[index] = instance
|
||||
|
||||
instance.$mount()
|
||||
instance.$el.style.transform = `translate3d(${shelfOffsetX}px, ${shelfOffsetY}px, 0px)`
|
||||
instance.$el.classList.add('absolute', 'top-0', 'left-0', 'mx-3')
|
||||
shelfEl.appendChild(instance.$el)
|
||||
|
||||
if (this.entities[index]) {
|
||||
instance.setEntity(this.entities[index])
|
||||
}
|
||||
if (this.isSelectionMode) {
|
||||
instance.setSelectionMode(true)
|
||||
if (instance.audiobookId && this.selectedAudiobooks.includes(instance.audiobookId) || this.isSelectAll) {
|
||||
instance.selected = true
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
|
@ -43,7 +43,8 @@ export default {
|
|||
'@/plugins/my-native-audio.js',
|
||||
'@/plugins/audio-downloader.js',
|
||||
'@/plugins/storage-manager.js',
|
||||
'@/plugins/toast.js'
|
||||
'@/plugins/toast.js',
|
||||
'@/plugins/constants.js'
|
||||
],
|
||||
|
||||
components: true,
|
||||
|
|
65
objects/TouchEvent.js
Normal file
65
objects/TouchEvent.js
Normal file
|
@ -0,0 +1,65 @@
|
|||
// SOURCE: https://stackoverflow.com/a/69617795/7431543
|
||||
export default class TouchEvent {
|
||||
static SWPIE_THRESHOLD = 50 // Minumum difference in pixels at which a swipe gesture is detected
|
||||
|
||||
static SWIPE_LEFT = 1
|
||||
static SWIPE_RIGHT = 2
|
||||
static SWIPE_UP = 3
|
||||
static SWIPE_DOWN = 4
|
||||
|
||||
constructor(startEvent, endEvent) {
|
||||
this.startEvent = startEvent
|
||||
this.endEvent = endEvent || null
|
||||
}
|
||||
|
||||
isSwipeLeft() {
|
||||
return this.getSwipeDirection() == TouchEvent.SWIPE_LEFT
|
||||
}
|
||||
|
||||
isSwipeRight() {
|
||||
return this.getSwipeDirection() == TouchEvent.SWIPE_RIGHT
|
||||
}
|
||||
|
||||
isSwipeUp() {
|
||||
return this.getSwipeDirection() == TouchEvent.SWIPE_UP
|
||||
}
|
||||
|
||||
isSwipeDown() {
|
||||
return this.getSwipeDirection() == TouchEvent.SWIPE_DOWN
|
||||
}
|
||||
|
||||
getSwipeDirection() {
|
||||
let start = this.startEvent.changedTouches[0]
|
||||
let end = this.endEvent.changedTouches[0]
|
||||
|
||||
if (!start || !end) {
|
||||
return null
|
||||
}
|
||||
|
||||
let horizontalDifference = start.screenX - end.screenX
|
||||
let verticalDifference = start.screenY - end.screenY
|
||||
|
||||
// Horizontal difference dominates
|
||||
if (Math.abs(horizontalDifference) > Math.abs(verticalDifference)) {
|
||||
if (horizontalDifference >= TouchEvent.SWPIE_THRESHOLD) {
|
||||
return TouchEvent.SWIPE_LEFT
|
||||
} else if (horizontalDifference <= -TouchEvent.SWPIE_THRESHOLD) {
|
||||
return TouchEvent.SWIPE_RIGHT
|
||||
}
|
||||
|
||||
// Verical or no difference dominates
|
||||
} else {
|
||||
if (verticalDifference >= TouchEvent.SWPIE_THRESHOLD) {
|
||||
return TouchEvent.SWIPE_UP
|
||||
} else if (verticalDifference <= -TouchEvent.SWPIE_THRESHOLD) {
|
||||
return TouchEvent.SWIPE_DOWN
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
setEndEvent(endEvent) {
|
||||
this.endEvent = endEvent
|
||||
}
|
||||
}
|
|
@ -2,31 +2,12 @@
|
|||
<div class="w-full h-full">
|
||||
<home-bookshelf-nav-bar />
|
||||
<home-bookshelf-toolbar v-show="!isHome" />
|
||||
<div class="main-content overflow-y-auto overflow-x-hidden relative" :class="isHome ? 'home-page' : ''">
|
||||
<div id="bookshelf-wrapper" class="main-content overflow-y-auto overflow-x-hidden relative" :class="isHome ? 'home-page' : ''">
|
||||
<nuxt-child />
|
||||
|
||||
<div v-if="isLoading" class="absolute top-0 left-0 w-full h-full flex items-center justify-center">
|
||||
<!-- <div v-if="isLoading" class="absolute top-0 left-0 w-full h-full flex items-center justify-center">
|
||||
<ui-loading-indicator />
|
||||
</div>
|
||||
<div v-else-if="!audiobooks.length" class="absolute top-0 left-0 w-full h-full flex items-center justify-center">
|
||||
<div>
|
||||
<p class="mb-4 text-center text-xl">
|
||||
Bookshelf empty<span v-show="isSocketConnected">
|
||||
for library <strong>{{ currentLibraryName }}</strong></span
|
||||
>
|
||||
</p>
|
||||
<div class="w-full" v-if="!isSocketConnected">
|
||||
<div class="flex justify-center items-center mb-3">
|
||||
<span class="material-icons text-error text-lg">cloud_off</span>
|
||||
<p class="pl-2 text-error text-sm">Audiobookshelf server not connected.</p>
|
||||
</div>
|
||||
<p class="px-4 text-center text-error absolute bottom-12 left-0 right-0 mx-auto"><strong>Important!</strong> This app requires that you are running <u>your own server</u> and does not provide any content.</p>
|
||||
</div>
|
||||
<div class="flex justify-center">
|
||||
<ui-btn v-if="!isSocketConnected" small @click="$router.push('/connect')" class="w-32"> Connect </ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -40,12 +21,6 @@ export default {
|
|||
isHome() {
|
||||
return this.$route.name === 'bookshelf'
|
||||
},
|
||||
isLoading() {
|
||||
return this.$store.state.audiobooks.isLoading
|
||||
},
|
||||
audiobooks() {
|
||||
return this.$store.state.audiobooks.audiobooks
|
||||
},
|
||||
currentLibrary() {
|
||||
return this.$store.getters['libraries/getCurrentLibrary']
|
||||
},
|
||||
|
@ -57,35 +32,23 @@ export default {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
async loadAudiobooks() {
|
||||
var currentLibrary = await this.$localStore.getCurrentLibrary()
|
||||
if (currentLibrary) {
|
||||
this.$store.commit('libraries/setCurrentLibrary', currentLibrary.id)
|
||||
}
|
||||
this.$store.dispatch('audiobooks/load')
|
||||
},
|
||||
async loadCollections() {
|
||||
this.$store.dispatch('user/loadUserCollections')
|
||||
},
|
||||
socketConnected(isConnected) {
|
||||
if (isConnected) {
|
||||
console.log('Connected - Load from server')
|
||||
this.loadAudiobooks()
|
||||
if (this.$route.name === 'bookshelf-collections') this.loadCollections()
|
||||
} else {
|
||||
console.log('Disconnected - Reset to local storage')
|
||||
this.$store.commit('audiobooks/reset')
|
||||
this.$store.dispatch('audiobooks/useDownloaded')
|
||||
}
|
||||
// if (isConnected) {
|
||||
// console.log('Connected - Load from server')
|
||||
// this.loadAudiobooks()
|
||||
// if (this.$route.name === 'bookshelf-collections') this.loadCollections()
|
||||
// } else {
|
||||
// console.log('Disconnected - Reset to local storage')
|
||||
// this.$store.commit('audiobooks/reset')
|
||||
// this.$store.dispatch('audiobooks/useDownloaded')
|
||||
// }
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.$server.on('connected', this.socketConnected)
|
||||
if (this.$server.connected) {
|
||||
this.loadAudiobooks()
|
||||
} else {
|
||||
console.log('Bookshelf - Server not connected using downloaded')
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.$server.off('connected', this.socketConnected)
|
||||
|
@ -97,6 +60,7 @@ export default {
|
|||
.main-content {
|
||||
max-height: calc(100% - 72px);
|
||||
min-height: calc(100% - 72px);
|
||||
max-width: 100vw;
|
||||
}
|
||||
.main-content.home-page {
|
||||
max-height: calc(100% - 36px);
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
<template>
|
||||
<div class="w-full h-full">
|
||||
<bookshelf-lazy-bookshelf page="collections" />
|
||||
<!-- <div class="w-full h-full">
|
||||
<template v-for="(shelf, index) in shelves">
|
||||
<bookshelf-group-shelf :key="shelf.id" group-type="collection" :groups="shelf.groups" :style="{ zIndex: shelves.length - index }" />
|
||||
</template>
|
||||
</div>
|
||||
</div> -->
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
|
|
@ -1,8 +1,28 @@
|
|||
<template>
|
||||
<div class="w-full h-full">
|
||||
<template v-for="(shelf, index) in shelves">
|
||||
<bookshelf-shelf :key="shelf.id" :label="shelf.label" :books="shelf.books" :style="{ zIndex: shelves.length - index }" />
|
||||
<bookshelf-shelf :key="shelf.id" :label="shelf.label" :entities="shelf.entities" :type="shelf.type" :style="{ zIndex: shelves.length - index }" />
|
||||
</template>
|
||||
|
||||
<div v-if="!shelves.length" class="absolute top-0 left-0 w-full h-full flex items-center justify-center">
|
||||
<div>
|
||||
<p class="mb-4 text-center text-xl">
|
||||
Bookshelf empty<span v-show="isSocketConnected">
|
||||
for library <strong>{{ currentLibraryName }}</strong></span
|
||||
>
|
||||
</p>
|
||||
<div class="w-full" v-if="!isSocketConnected">
|
||||
<div class="flex justify-center items-center mb-3">
|
||||
<span class="material-icons text-error text-lg">cloud_off</span>
|
||||
<p class="pl-2 text-error text-sm">Audiobookshelf server not connected.</p>
|
||||
</div>
|
||||
<p class="px-4 text-center text-error absolute bottom-12 left-0 right-0 mx-auto"><strong>Important!</strong> This app requires that you are running <u>your own server</u> and does not provide any content.</p>
|
||||
</div>
|
||||
<div class="flex justify-center">
|
||||
<ui-btn v-if="!isSocketConnected" small @click="$router.push('/connect')" class="w-32"> Connect </ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -10,13 +30,19 @@
|
|||
export default {
|
||||
data() {
|
||||
return {
|
||||
settings: {}
|
||||
shelves: [],
|
||||
loading: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
books() {
|
||||
// return this.$store.getters['audiobooks/getFilteredAndSorted']()
|
||||
return this.$store.state.audiobooks.audiobooks
|
||||
return this.$store.getters['downloads/getAudiobooks']
|
||||
},
|
||||
isSocketConnected() {
|
||||
return this.$store.state.socketConnected
|
||||
},
|
||||
currentLibraryName() {
|
||||
return this.$store.getters['libraries/getCurrentLibraryName']
|
||||
},
|
||||
booksWithUserAbData() {
|
||||
var books = this.books.map((b) => {
|
||||
|
@ -50,14 +76,15 @@ export default {
|
|||
})
|
||||
return books.slice(0, 10)
|
||||
},
|
||||
shelves() {
|
||||
downloadOnlyShelves() {
|
||||
var shelves = []
|
||||
|
||||
if (this.booksCurrentlyReading.length) {
|
||||
shelves.push({
|
||||
id: 'recent',
|
||||
label: 'Continue Reading',
|
||||
books: this.booksCurrentlyReading
|
||||
type: 'books',
|
||||
entities: this.booksCurrentlyReading
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -65,7 +92,8 @@ export default {
|
|||
shelves.push({
|
||||
id: 'added',
|
||||
label: 'Recently Added',
|
||||
books: this.booksRecentlyAdded
|
||||
type: 'books',
|
||||
entities: this.booksRecentlyAdded
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -73,22 +101,58 @@ export default {
|
|||
shelves.push({
|
||||
id: 'read',
|
||||
label: 'Read Again',
|
||||
books: this.booksRead
|
||||
type: 'books',
|
||||
entities: this.booksRead
|
||||
})
|
||||
}
|
||||
return shelves
|
||||
},
|
||||
currentLibraryId() {
|
||||
return this.$store.state.libraries.currentLibraryId
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async init() {
|
||||
this.settings = { ...this.$store.state.user.settings }
|
||||
|
||||
// var bookshelfView = await this.$localStore.getBookshelfView()
|
||||
// this.isListView = bookshelfView === 'list'
|
||||
// this.bookshelfReady = true
|
||||
// console.log('Bookshelf view', bookshelfView)
|
||||
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 socketInit(isConnected) {
|
||||
if (isConnected) {
|
||||
console.log('Connected - Load from server')
|
||||
await this.fetchCategories()
|
||||
} else {
|
||||
console.log('Disconnected - Reset to local storage')
|
||||
this.shelves = this.downloadOnlyShelves
|
||||
}
|
||||
this.loading = false
|
||||
},
|
||||
async libraryChanged(libid) {
|
||||
console.log('Library changed', libid)
|
||||
if (this.isSocketConnected) {
|
||||
await this.fetchCategories()
|
||||
} else {
|
||||
this.shelves = this.downloadOnlyShelves
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {}
|
||||
mounted() {
|
||||
this.$server.on('initialized', this.socketInit)
|
||||
this.$eventBus.$on('library-changed', this.libraryChanged)
|
||||
if (this.$server.initialized) {
|
||||
this.fetchCategories()
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.$server.off('initialized', this.socketInit)
|
||||
this.$eventBus.$off('library-changed', this.libraryChanged)
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,78 +1,14 @@
|
|||
<template>
|
||||
<div class="w-full h-full">
|
||||
<div class="w-full h-full" v-show="!isListView">
|
||||
<template v-for="(shelf, index) in shelves">
|
||||
<bookshelf-library-shelf :key="shelf.id" :books="shelf.books" :style="{ zIndex: shelves.length - index }" />
|
||||
</template>
|
||||
</div>
|
||||
<div class="w-full h-full px-4 py-2" v-show="isListView">
|
||||
<template v-for="book in books">
|
||||
<app-bookshelf-list-row :key="book.id" :audiobook="book" :page-width="pageWidth" class="my-2" />
|
||||
</template>
|
||||
</div>
|
||||
<div v-show="!books.length && hasFilters" class="w-full py-16 text-center text-xl">
|
||||
<div class="py-4">No Books</div>
|
||||
<ui-btn @click="clearFilter">Clear Filter</ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
<bookshelf-lazy-bookshelf page="books" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
booksPerRow: 3,
|
||||
pageWidth: 0
|
||||
asyncData({ store, params, query }) {
|
||||
// Set filter by
|
||||
if (query.filter) {
|
||||
store.dispatch('user/updateUserSettings', { mobileFilterBy: query.filter })
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
bookshelfView() {
|
||||
return this.$store.state.bookshelfView
|
||||
},
|
||||
hasFilters() {
|
||||
return this.$store.getters['user/getUserSetting']('mobileFilterBy') !== 'all'
|
||||
},
|
||||
isListView() {
|
||||
return this.bookshelfView === 'list'
|
||||
},
|
||||
books() {
|
||||
return this.$store.getters['audiobooks/getFilteredAndSorted']()
|
||||
},
|
||||
shelves() {
|
||||
var shelves = []
|
||||
var shelf = {
|
||||
id: 0,
|
||||
books: []
|
||||
}
|
||||
for (let i = 0; i < this.books.length; i++) {
|
||||
var shelfNum = Math.floor((i + 1) / this.booksPerRow)
|
||||
shelf.id = shelfNum
|
||||
shelf.books.push(this.books[i])
|
||||
|
||||
if ((i + 1) % this.booksPerRow === 0) {
|
||||
shelves.push(shelf)
|
||||
shelf = {
|
||||
id: 0,
|
||||
books: []
|
||||
}
|
||||
}
|
||||
}
|
||||
if (shelf.books.length) {
|
||||
shelf.id++
|
||||
shelves.push(shelf)
|
||||
}
|
||||
return shelves
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
clearFilter() {
|
||||
this.$store.dispatch('user/updateUserSettings', {
|
||||
mobileFilterBy: 'all'
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.pageWidth = window.innerWidth
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
19
pages/bookshelf/series/_id.vue
Normal file
19
pages/bookshelf/series/_id.vue
Normal file
|
@ -0,0 +1,19 @@
|
|||
<template>
|
||||
<bookshelf-lazy-bookshelf page="series-books" :series-id="seriesId" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
asyncData({ params }) {
|
||||
return {
|
||||
seriesId: params.id
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {},
|
||||
methods: {},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
|
@ -1,10 +1,11 @@
|
|||
<template>
|
||||
<div class="w-full h-full">
|
||||
<bookshelf-lazy-bookshelf page="series" />
|
||||
<!-- <div class="w-full h-full">
|
||||
<template v-for="(shelf, index) in shelves">
|
||||
<bookshelf-group-shelf v-if="!selectedSeriesName" :key="shelf.id" group-type="series" :groups="shelf.groups" :style="{ zIndex: shelves.length - index }" />
|
||||
<bookshelf-library-shelf v-else :key="shelf.id" :books="shelf.books" :style="{ zIndex: shelves.length - index }" />
|
||||
</template>
|
||||
</div>
|
||||
</div> -->
|
||||
</template>
|
||||
|
||||
<script>
|
|
@ -105,6 +105,9 @@ export default {
|
|||
}
|
||||
},
|
||||
computed: {
|
||||
isSocketConnected() {
|
||||
return this.$store.state.socketConnected
|
||||
},
|
||||
hasStoragePermission() {
|
||||
return this.$store.state.hasStoragePermission
|
||||
},
|
||||
|
@ -158,9 +161,10 @@ export default {
|
|||
|
||||
await this.searchFolder()
|
||||
|
||||
var audiobooks = this.$store.state.audiobooks.audiobooks || []
|
||||
if (audiobooks.length) {
|
||||
this.$store.dispatch('downloads/linkOrphanDownloads', audiobooks)
|
||||
// var audiobooks = this.$store.state.audiobooks.audiobooks || []
|
||||
// if (audiobooks.length) {
|
||||
if (this.isSocketConnected) {
|
||||
this.$store.dispatch('downloads/linkOrphanDownloads')
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -1,28 +1,5 @@
|
|||
<template>
|
||||
<div class="w-full h-full">
|
||||
<div class="w-full h-12 relative z-20">
|
||||
<div id="toolbar" class="asolute top-0 left-0 w-full h-full bg-bg flex items-center px-2">
|
||||
<span class="material-icons px-2" @click="showSearchModal = true">search</span>
|
||||
<p class="font-book">{{ numAudiobooks }} Audiobooks</p>
|
||||
|
||||
<div class="flex-grow" />
|
||||
<span class="material-icons px-2" @click="changeView">{{ viewIcon }}</span>
|
||||
<div class="relative flex items-center px-2">
|
||||
<span class="material-icons" @click="showFilterModal = true">filter_alt</span>
|
||||
<div v-show="hasFilters" class="absolute top-0 right-2 w-2 h-2 rounded-full bg-success border border-green-300 shadow-sm z-10 pointer-events-none" />
|
||||
</div>
|
||||
<span class="material-icons px-2" @click="showSortModal = true">sort</span>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="bookshelfReady">
|
||||
<app-bookshelf v-if="!isListView" />
|
||||
<app-bookshelf-list v-else />
|
||||
</template>
|
||||
|
||||
<modals-order-modal v-model="showSortModal" :order-by.sync="settings.mobileOrderBy" :descending.sync="settings.mobileOrderDesc" @change="updateOrder" />
|
||||
<modals-filter-modal v-model="showFilterModal" :filter-by.sync="settings.mobileFilterBy" @change="updateFilter" />
|
||||
<modals-search-modal v-model="showSearchModal" />
|
||||
</div>
|
||||
<div class="w-full h-full"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
@ -31,69 +8,9 @@ export default {
|
|||
return redirect('/bookshelf')
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showSortModal: false,
|
||||
showFilterModal: false,
|
||||
showSearchModal: false,
|
||||
settings: {},
|
||||
isListView: false,
|
||||
bookshelfReady: false
|
||||
}
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
hasFilters() {
|
||||
return this.$store.getters['user/getUserSetting']('filterBy') !== 'all'
|
||||
},
|
||||
numAudiobooks() {
|
||||
return this.$store.getters['audiobooks/getFiltered']().length
|
||||
},
|
||||
viewIcon() {
|
||||
return this.isListView ? 'grid_view' : 'view_stream'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
changeView() {
|
||||
this.isListView = !this.isListView
|
||||
|
||||
var bookshelfView = this.isListView ? 'list' : 'grid'
|
||||
this.$localStore.setBookshelfView(bookshelfView)
|
||||
},
|
||||
updateOrder() {
|
||||
this.saveSettings()
|
||||
},
|
||||
updateFilter() {
|
||||
this.saveSettings()
|
||||
},
|
||||
saveSettings() {
|
||||
this.$store.commit('user/setSettings', this.settings) // Immediate update
|
||||
this.$store.dispatch('user/updateUserSettings', this.settings)
|
||||
},
|
||||
async init() {
|
||||
this.settings = { ...this.$store.state.user.settings }
|
||||
|
||||
var bookshelfView = await this.$localStore.getBookshelfView()
|
||||
this.isListView = bookshelfView === 'list'
|
||||
this.bookshelfReady = true
|
||||
console.log('Bookshelf view', bookshelfView)
|
||||
},
|
||||
settingsUpdated(settings) {
|
||||
for (const key in settings) {
|
||||
this.settings[key] = settings[key]
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.init()
|
||||
this.$store.commit('user/addSettingsListener', { id: 'bookshelftoolbar', meth: this.settingsUpdated })
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.$store.commit('user/removeSettingsListener', 'bookshelftoolbar')
|
||||
}
|
||||
computed: {},
|
||||
methods: {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#toolbar {
|
||||
box-shadow: 0px 5px 5px #11111155;
|
||||
}
|
||||
</style>
|
|
@ -7,18 +7,33 @@
|
|||
<div v-show="isFetching" class="w-full py-8 flex justify-center">
|
||||
<p class="text-lg text-gray-400">Fetching...</p>
|
||||
</div>
|
||||
<div v-if="!isFetching && lastSearch && !items.length" class="w-full py-8 flex justify-center">
|
||||
<div v-if="!isFetching && lastSearch && !totalResults" class="w-full py-8 flex justify-center">
|
||||
<p class="text-lg text-gray-400">Nothing found</p>
|
||||
</div>
|
||||
<template v-for="item in items">
|
||||
<div class="py-2 border-b border-bg flex" :key="item.id" @click="clickItem(item)">
|
||||
<cards-book-cover :audiobook="item.data" :width="50" />
|
||||
<div class="flex-grow px-4 h-full">
|
||||
<div class="w-full h-full">
|
||||
<p class="text-base truncate">{{ item.data.book.title }}</p>
|
||||
<p class="text-sm text-gray-400 truncate">{{ item.data.book.author }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="bookResults.length" class="font-semibold text-sm mb-1">Books</p>
|
||||
<template v-for="bookResult in bookResults">
|
||||
<div :key="bookResult.audiobook.id" class="w-full h-16 py-1">
|
||||
<nuxt-link :to="`/audiobook/${bookResult.audiobook.id}`">
|
||||
<cards-book-search-card :audiobook="bookResult.audiobook" :search="lastSearch" :match-key="bookResult.matchKey" :match-text="bookResult.matchText" />
|
||||
</nuxt-link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<p v-if="seriesResults.length" class="font-semibold text-sm mb-1 mt-2">Series</p>
|
||||
<template v-for="seriesResult in seriesResults">
|
||||
<div :key="seriesResult.series" class="w-full h-16 py-1">
|
||||
<nuxt-link :to="`/bookshelf/series/${$encode(seriesResult.series)}`">
|
||||
<cards-series-search-card :series="seriesResult.series" :book-items="seriesResult.audiobooks" />
|
||||
</nuxt-link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<p v-if="authorResults.length" class="font-semibold text-sm mb-1 mt-2">Authors</p>
|
||||
<template v-for="authorResult in authorResults">
|
||||
<div :key="authorResult.author" class="w-full h-14 py-1">
|
||||
<nuxt-link :to="`/bookshelf/library?filter=authors.${$encode(authorResult.author)}`">
|
||||
<cards-author-search-card :key="authorResult.author" :author="authorResult.author" />
|
||||
</nuxt-link>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
@ -33,34 +48,43 @@ export default {
|
|||
searchTimeout: null,
|
||||
lastSearch: null,
|
||||
isFetching: false,
|
||||
items: []
|
||||
bookResults: [],
|
||||
seriesResults: [],
|
||||
authorResults: []
|
||||
}
|
||||
},
|
||||
computed: {},
|
||||
methods: {
|
||||
clickItem(item) {
|
||||
this.show = false
|
||||
this.$router.push(`/audiobook/${item.id}`)
|
||||
computed: {
|
||||
currentLibraryId() {
|
||||
return this.$store.state.libraries.currentLibraryId
|
||||
},
|
||||
bookCoverAspectRatio() {
|
||||
return this.$store.getters['getBookCoverAspectRatio']
|
||||
},
|
||||
totalResults() {
|
||||
return this.bookResults.length + this.seriesResults.length + this.authorResults.length
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async runSearch(value) {
|
||||
this.lastSearch = value
|
||||
if (!this.lastSearch) {
|
||||
this.items = []
|
||||
this.bookResults = []
|
||||
this.seriesResults = []
|
||||
this.authorResults = []
|
||||
return
|
||||
}
|
||||
this.isFetching = true
|
||||
var results = await this.$axios.$get(`/api/books?q=${value}`).catch((error) => {
|
||||
var results = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/search?q=${value}&limit=5`).catch((error) => {
|
||||
console.error('Search error', error)
|
||||
return []
|
||||
})
|
||||
console.log('RESULTS', results)
|
||||
|
||||
this.isFetching = false
|
||||
this.items = results.map((res) => {
|
||||
return {
|
||||
id: res.id,
|
||||
data: res,
|
||||
type: 'audiobook'
|
||||
}
|
||||
})
|
||||
|
||||
this.bookResults = results ? results.audiobooks || [] : []
|
||||
this.seriesResults = results ? results.series || [] : []
|
||||
this.authorResults = results ? results.authors || [] : []
|
||||
},
|
||||
updateSearch(val) {
|
||||
clearTimeout(this.searchTimeout)
|
||||
|
|
26
plugins/constants.js
Normal file
26
plugins/constants.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
const DownloadStatus = {
|
||||
PENDING: 0,
|
||||
READY: 1,
|
||||
EXPIRED: 2,
|
||||
FAILED: 3
|
||||
}
|
||||
|
||||
const CoverDestination = {
|
||||
METADATA: 0,
|
||||
AUDIOBOOK: 1
|
||||
}
|
||||
|
||||
const BookCoverAspectRatio = {
|
||||
STANDARD: 0,
|
||||
SQUARE: 1
|
||||
}
|
||||
|
||||
const Constants = {
|
||||
DownloadStatus,
|
||||
CoverDestination,
|
||||
BookCoverAspectRatio
|
||||
}
|
||||
|
||||
export default ({ app }, inject) => {
|
||||
inject('constants', Constants)
|
||||
}
|
|
@ -1,6 +1,8 @@
|
|||
import Vue from 'vue'
|
||||
import { formatDistance, format } from 'date-fns'
|
||||
|
||||
Vue.prototype.$eventBus = new Vue()
|
||||
|
||||
Vue.prototype.$isDev = process.env.NODE_ENV !== 'production'
|
||||
|
||||
Vue.prototype.$dateDistanceFromNow = (unixms) => {
|
||||
|
|
|
@ -1,14 +1,8 @@
|
|||
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']
|
||||
|
||||
export const state = () => ({
|
||||
audiobooks: [],
|
||||
listeners: [],
|
||||
genres: [...STANDARD_GENRES],
|
||||
tags: [],
|
||||
series: [],
|
||||
loadedLibraryId: 'main',
|
||||
lastLoad: 0,
|
||||
isLoading: false
|
||||
|
@ -18,97 +12,6 @@ export const getters = {
|
|||
getAudiobook: state => id => {
|
||||
return state.audiobooks.find(ab => ab.id === id)
|
||||
},
|
||||
getFiltered: (state, getters, rootState, rootGetters) => () => {
|
||||
var filtered = state.audiobooks
|
||||
var settings = rootState.user.settings || {}
|
||||
var filterBy = settings.mobileFilterBy || ''
|
||||
|
||||
var searchGroups = ['genres', 'tags', 'series', 'authors', 'progress']
|
||||
var group = searchGroups.find(_group => filterBy.startsWith(_group + '.'))
|
||||
if (group) {
|
||||
var filter = decode(filterBy.replace(`${group}.`, ''))
|
||||
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 === 'series') filtered = filtered.filter(ab => ab.book && ab.book.series === filter)
|
||||
else if (group === 'authors') filtered = filtered.filter(ab => ab.book && ab.book.author === filter)
|
||||
else if (group === 'progress') {
|
||||
filtered = filtered.filter(ab => {
|
||||
var userAudiobook = rootGetters['user/getUserAudiobookData'](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
|
||||
})
|
||||
}
|
||||
}
|
||||
return filtered
|
||||
},
|
||||
getFilteredAndSorted: (state, getters, rootState, rootGetters) => () => {
|
||||
var settings = rootState.user.settings
|
||||
var direction = settings.mobileOrderDesc ? 'desc' : 'asc'
|
||||
|
||||
var filtered = getters.getFiltered()
|
||||
|
||||
if (settings.mobileOrderBy === 'recent') {
|
||||
return sort(filtered)[direction]((ab) => {
|
||||
var abprogress = rootGetters['user/getUserAudiobookData'](ab.id)
|
||||
if (!abprogress) return 0
|
||||
return abprogress.lastUpdate
|
||||
})
|
||||
} else {
|
||||
var orderByNumber = settings.mobileOrderBy === 'book.volumeNumber'
|
||||
return sort(filtered)[direction]((ab) => {
|
||||
// Supports dot notation strings i.e. "book.title"
|
||||
var value = settings.mobileOrderBy.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 _authors = state.audiobooks.filter(ab => !!(ab.book && ab.book.author)).map(ab => ab.book.author)
|
||||
return [...new Set(_authors)]
|
||||
},
|
||||
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') => {
|
||||
var book = bookItem.book
|
||||
if (!book || !book.cover || book.cover === placeholder) return placeholder
|
||||
|
@ -141,43 +44,6 @@ export const getters = {
|
|||
}
|
||||
|
||||
export const actions = {
|
||||
load({ state, commit, dispatch, 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
|
||||
}
|
||||
} else {
|
||||
commit('reset')
|
||||
commit('setLoading', true)
|
||||
}
|
||||
commit('setLoadedLibrary', currentLibraryId)
|
||||
|
||||
this.$axios
|
||||
.$get(`/api/library/${currentLibraryId}/audiobooks`)
|
||||
.then((data) => {
|
||||
commit('set', data)
|
||||
commit('setLastLoad')
|
||||
commit('setLoading', false)
|
||||
|
||||
dispatch('downloads/linkOrphanDownloads', data, { root: true })
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed', error)
|
||||
commit('set', [])
|
||||
commit('setLoading', false)
|
||||
})
|
||||
return true
|
||||
},
|
||||
useDownloaded({ commit, rootGetters }) {
|
||||
commit('set', rootGetters['downloads/getAudiobooks'])
|
||||
}
|
||||
|
@ -199,46 +65,6 @@ export const mutations = {
|
|||
state.tags = []
|
||||
state.series = []
|
||||
},
|
||||
set(state, audiobooks) {
|
||||
// GENRES
|
||||
var genres = [...state.genres]
|
||||
audiobooks.forEach((ab) => {
|
||||
if (!ab.book) return
|
||||
genres = genres.concat(ab.book.genres)
|
||||
})
|
||||
state.genres = [...new Set(genres)] // Remove Duplicates
|
||||
state.genres.sort((a, b) => a.toLowerCase() < b.toLowerCase() ? -1 : 1)
|
||||
|
||||
// TAGS
|
||||
var tags = []
|
||||
audiobooks.forEach((ab) => {
|
||||
tags = tags.concat(ab.tags)
|
||||
})
|
||||
state.tags = [...new Set(tags)] // Remove Duplicates
|
||||
state.tags.sort((a, b) => a.toLowerCase() < b.toLowerCase() ? -1 : 1)
|
||||
|
||||
// SERIES
|
||||
var series = []
|
||||
audiobooks.forEach((ab) => {
|
||||
if (!ab.book || !ab.book.series || series.includes(ab.book.series)) return
|
||||
series.push(ab.book.series)
|
||||
})
|
||||
state.series = series
|
||||
state.series.sort((a, b) => a.toLowerCase() < b.toLowerCase() ? -1 : 1)
|
||||
|
||||
audiobooks.forEach((ab) => {
|
||||
var indexOf = state.audiobooks.findIndex(_ab => _ab.id === ab.id)
|
||||
if (indexOf >= 0) {
|
||||
state.audiobooks.splice(indexOf, 1, ab)
|
||||
} else {
|
||||
state.audiobooks.push(ab)
|
||||
}
|
||||
})
|
||||
// state.audiobooks = audiobooks
|
||||
state.listeners.forEach((listener) => {
|
||||
listener.meth()
|
||||
})
|
||||
},
|
||||
addUpdate(state, audiobook) {
|
||||
var index = state.audiobooks.findIndex(a => a.id === audiobook.id)
|
||||
var origAudiobook = null
|
||||
|
|
|
@ -20,7 +20,7 @@ export const getters = {
|
|||
}
|
||||
|
||||
export const actions = {
|
||||
async loadFromStorage({ commit }) {
|
||||
async loadFromStorage({ commit, state }) {
|
||||
var downloads = await this.$sqlStore.getAllDownloads()
|
||||
|
||||
downloads.forEach(ab => {
|
||||
|
@ -31,20 +31,32 @@ export const actions = {
|
|||
ab.isPreparing = false
|
||||
commit('setDownload', ab)
|
||||
})
|
||||
return state.downloads
|
||||
},
|
||||
linkOrphanDownloads({ state, commit }, audiobooks) {
|
||||
async linkOrphanDownloads({ state, commit, rootState }) {
|
||||
if (!state.mediaScanResults || !state.mediaScanResults.folders) {
|
||||
return
|
||||
}
|
||||
console.log('Link orphan downloads', JSON.stringify(state.mediaScanResults.folders))
|
||||
state.mediaScanResults.folders.forEach((folder) => {
|
||||
// state.mediaScanResults.folders.forEach((folder) => {
|
||||
for (let i = 0; i < state.mediaScanResults.folders.length; i++) {
|
||||
var folder = state.mediaScanResults.folders[i]
|
||||
if (!folder.files || !folder.files.length) return
|
||||
|
||||
console.log('Link orphan downloads check folder', folder.name)
|
||||
|
||||
var download = state.downloads.find(dl => dl.folderName === folder.name)
|
||||
if (!download) {
|
||||
var matchingAb = audiobooks.find(ab => ab.book.title === folder.name)
|
||||
// var matchingAb = audiobooks.find(ab => ab.book.title === folder.name)
|
||||
var results = await this.$axios.$get(`/libraries/${rootState.libraries.currentLibraryId}/search?q=${folder.name}`)
|
||||
var matchingAb = null
|
||||
if (results && results.audiobooks) {
|
||||
console.log('has ab results', JSON.stringify(results.audiobooks))
|
||||
matchingAb = results.audiobooks.find(ab => ab.audiobook.book.title === folder.name)
|
||||
if (matchingAb) console.log('Found matching ab for ' + folder.name, matchingAb)
|
||||
else console.warn('did not find mathcing ab for ' + folder.name)
|
||||
} else {
|
||||
console.error('Invalid results payload', JSON.stringify(results))
|
||||
}
|
||||
if (matchingAb) {
|
||||
// Found matching download for ab
|
||||
var audioFile = folder.files.find(f => f.isAudio)
|
||||
|
@ -72,7 +84,7 @@ export const actions = {
|
|||
commit('addUpdateDownload', downloadObj)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -19,7 +19,8 @@ export const state = () => ({
|
|||
|
||||
showSideDrawer: false,
|
||||
bookshelfView: 'grid',
|
||||
isNetworkListenerInit: false
|
||||
isNetworkListenerInit: false,
|
||||
serverSettings: null
|
||||
})
|
||||
|
||||
export const getters = {
|
||||
|
@ -34,7 +35,15 @@ export const getters = {
|
|||
},
|
||||
getAudiobookIdStreaming: state => {
|
||||
return state.streamAudiobook ? state.streamAudiobook.id : null
|
||||
}
|
||||
},
|
||||
getServerSetting: state => key => {
|
||||
if (!state.serverSettings) return null
|
||||
return state.serverSettings[key]
|
||||
},
|
||||
getBookCoverAspectRatio: state => {
|
||||
if (!state.serverSettings || !state.serverSettings.coverAspectRatio) return 1.6
|
||||
return state.serverSettings.coverAspectRatio === 0 ? 1.6 : 1
|
||||
},
|
||||
}
|
||||
|
||||
export const actions = {
|
||||
|
@ -123,5 +132,8 @@ export const mutations = {
|
|||
},
|
||||
setBookshelfView(state, val) {
|
||||
state.bookshelfView = val
|
||||
},
|
||||
setServerSettings(state, val) {
|
||||
state.serverSettings = val
|
||||
}
|
||||
}
|
|
@ -5,12 +5,18 @@ export const state = () => ({
|
|||
currentLibraryId: 'main',
|
||||
showModal: false,
|
||||
folders: [],
|
||||
folderLastUpdate: 0
|
||||
folderLastUpdate: 0,
|
||||
issues: 0,
|
||||
filterData: null
|
||||
})
|
||||
|
||||
export const getters = {
|
||||
getCurrentLibrary: state => {
|
||||
return state.libraries.find(lib => lib.id === state.currentLibraryId)
|
||||
},
|
||||
getCurrentLibraryName: (state, getters) => {
|
||||
var currLib = getters.getCurrentLibrary
|
||||
return currLib ? currLib.name : null
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -21,16 +27,22 @@ export const actions = {
|
|||
return false
|
||||
}
|
||||
|
||||
var library = state.libraries.find(lib => lib.id === libraryId)
|
||||
if (library) {
|
||||
commit('setCurrentLibrary', libraryId)
|
||||
return library
|
||||
}
|
||||
// var library = state.libraries.find(lib => lib.id === libraryId)
|
||||
// if (library) {
|
||||
// commit('setCurrentLibrary', libraryId)
|
||||
// return library
|
||||
// }
|
||||
|
||||
return this.$axios
|
||||
.$get(`/api/libraries/${libraryId}`)
|
||||
.$get(`/api/libraries/${libraryId}?include=filterdata`)
|
||||
.then((data) => {
|
||||
commit('addUpdate', data)
|
||||
var library = data.library
|
||||
var filterData = data.filterdata
|
||||
var issues = data.issues || 0
|
||||
|
||||
commit('addUpdate', library)
|
||||
commit('setLibraryIssues', issues)
|
||||
commit('setLibraryFilterData', filterData)
|
||||
commit('setCurrentLibrary', libraryId)
|
||||
return data
|
||||
})
|
||||
|
@ -117,5 +129,11 @@ export const mutations = {
|
|||
},
|
||||
removeListener(state, listenerId) {
|
||||
state.listeners = state.listeners.filter(l => l.id !== listenerId)
|
||||
}
|
||||
},
|
||||
setLibraryIssues(state, val) {
|
||||
state.issues = val
|
||||
},
|
||||
setLibraryFilterData(state, filterData) {
|
||||
state.filterData = filterData
|
||||
},
|
||||
}
|
|
@ -2,7 +2,7 @@ export const state = () => ({
|
|||
user: null,
|
||||
userAudiobookData: [],
|
||||
settings: {
|
||||
mobileOrderBy: 'recent',
|
||||
mobileOrderBy: 'addedAt',
|
||||
mobileOrderDesc: true,
|
||||
mobileFilterBy: 'all',
|
||||
orderBy: 'book.title',
|
||||
|
@ -23,6 +23,9 @@ export const getters = {
|
|||
return state.user ? state.user.token : null
|
||||
},
|
||||
getUserAudiobookData: (state, getters) => (audiobookId) => {
|
||||
return getters.getUserAudiobook(audiobookId)
|
||||
},
|
||||
getUserAudiobook: (state, getters) => (audiobookId) => {
|
||||
return state.userAudiobookData.find(uabd => uabd.audiobookId === audiobookId)
|
||||
},
|
||||
getUserSetting: (state) => (key) => {
|
||||
|
@ -135,6 +138,9 @@ export const mutations = {
|
|||
var hasChanges = false
|
||||
for (const key in settings) {
|
||||
if (state.settings[key] !== settings[key]) {
|
||||
if (key === 'mobileOrderBy' && settings[key] === 'recent') {
|
||||
settings[key] = 'addedAt'
|
||||
}
|
||||
hasChanges = true
|
||||
state.settings[key] = settings[key]
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue