Finish lazy bookshelf,Fix:Async sql db,Fix:Load user progress

This commit is contained in:
advplyr
2021-12-05 18:31:47 -06:00
parent 37d3021302
commit 3232b519d3
42 changed files with 613 additions and 1878 deletions
+1 -40
View File
@@ -14,14 +14,9 @@
</svg>
<p class="text-lg font-book leading-4 ml-2">{{ currentLibraryName }}</p>
</div>
<!-- <p class="text-lg font-book leading-4">AudioBookshelf</p> -->
</div>
<div class="flex-grow" />
<!-- <ui-menu :label="username" :items="menuItems" @action="menuAction" class="ml-5" /> -->
<!-- <span class="material-icons cursor-pointer mx-4" :class="hasDownloadsFolder ? '' : 'text-warning'" @click="$store.commit('downloads/setShowModal', true)">source</span> -->
<nuxt-link class="h-7 mx-2" to="/search">
<span class="material-icons" style="font-size: 1.75rem">search</span>
</nuxt-link>
@@ -36,19 +31,7 @@
<script>
export default {
data() {
return {
menuItems: [
{
value: 'account',
text: 'Account',
to: '/account'
},
{
value: 'logout',
text: 'Logout'
}
]
}
return {}
},
computed: {
socketConnected() {
@@ -68,16 +51,6 @@ export default {
},
username() {
return this.user ? this.user.username : 'err'
},
appListingUrl() {
if (this.$platform === 'android') {
return process.env.ANDROID_APP_URL
} else {
return process.env.IOS_APP_URL
}
},
hasDownloadsFolder() {
return !!this.$store.state.downloadFolder
}
},
methods: {
@@ -93,18 +66,6 @@ export default {
} else {
this.$router.push('/bookshelf')
}
},
logout() {
this.$axios.$post('/logout').catch((error) => {
console.error(error)
})
this.$server.logout()
this.$router.push('/connect')
},
menuAction(action) {
if (action === 'logout') {
this.logout()
}
}
},
mounted() {}
+20 -2
View File
@@ -12,9 +12,9 @@
</div>
</div>
<div class="cover-wrapper absolute z-30 pointer-events-auto" @click="clickContainer">
<div class="cover-wrapper absolute z-30 pointer-events-auto" :class="bookCoverAspectRatio === 1 ? 'square-cover' : ''" @click="clickContainer">
<div class="cover-container bookCoverWrapper bg-black bg-opacity-75 w-full h-full">
<cards-player-book-cover :audiobook="audiobook" :download-cover="downloadedCover" :width="showFullscreen ? 200 : 60" />
<covers-book-cover :audiobook="audiobook" :download-cover="downloadedCover" :width="bookCoverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
</div>
</div>
@@ -134,6 +134,15 @@ export default {
this.$emit('update:playing', val)
}
},
bookCoverAspectRatio() {
return this.$store.getters['getBookCoverAspectRatio']
},
bookCoverWidth() {
if (this.bookCoverAspectRatio === 1) {
return this.showFullscreen ? 260 : 60
}
return this.showFullscreen ? 200 : 60
},
book() {
return this.audiobook.book || {}
},
@@ -641,6 +650,9 @@ export default {
transition-property: left, bottom, width, height;
transform-origin: left bottom;
}
.cover-wrapper.square-cover {
height: 60px;
}
.title-author-texts {
transition: all 0.15s cubic-bezier(0.39, 0.575, 0.565, 1);
@@ -719,6 +731,12 @@ export default {
height: 320px;
width: 200px;
}
.fullscreen .cover-wrapper.square-cover {
height: 260px;
width: 260px;
left: calc(50% - 130px);
}
.fullscreen #playerControls {
width: 100%;
bottom: 100px;
+12 -3
View File
@@ -113,9 +113,6 @@ export default {
cover() {
return this.book ? this.book.cover : ''
},
downloadedCover() {
return this.download ? this.download.cover : null
},
series() {
return this.book ? this.book.series : ''
},
@@ -361,6 +358,7 @@ export default {
if (this.$refs.audioPlayer) {
this.$refs.audioPlayer.terminateStream()
}
this.download = null
}
this.lastProgressTimeUpdate = 0
@@ -450,6 +448,15 @@ export default {
this.$server.socket.on('stream_progress', this.streamProgress)
this.$server.socket.on('stream_ready', this.streamReady)
this.$server.socket.on('stream_reset', this.streamReset)
},
closeStreamOnly() {
// If user logs out or disconnects from server, close audio if streaming
if (!this.download) {
this.$store.commit('setStreamAudiobook', null)
if (this.$refs.audioPlayer) {
this.$refs.audioPlayer.terminateStream()
}
}
}
},
mounted() {
@@ -460,6 +467,7 @@ export default {
console.log(`[AudioPlayerContainer] Init Playback Speed: ${this.playbackSpeed}`)
this.setListeners()
this.$eventBus.$on('close_stream', this.closeStreamOnly)
this.$store.commit('user/addSettingsListener', { id: 'streamContainer', meth: this.settingsUpdated })
this.$store.commit('setStreamListener', this.streamUpdated)
},
@@ -475,6 +483,7 @@ export default {
this.$server.socket.off('stream_reset', this.streamReset)
}
this.$eventBus.$off('close_stream', this.closeStreamOnly)
this.$store.commit('user/removeSettingsListener', 'streamContainer')
this.$store.commit('removeStreamListener')
}
-147
View File
@@ -1,147 +0,0 @@
<template>
<div id="bookshelf" ref="wrapper" class="w-full overflow-y-auto">
<template v-for="(shelf, index) in groupedBooks">
<div :key="index" class="border-b border-opacity-10 w-full bookshelfRow py-4 flex justify-around relative">
<template v-for="audiobook in shelf">
<cards-book-card :key="audiobook.id" :audiobook="audiobook" :width="cardWidth" />
</template>
<div class="bookshelfDivider h-4 w-full absolute bottom-0 left-0 right-0 z-10" />
</div>
</template>
<div v-show="!groupedBooks.length" class="w-full py-16 text-center text-xl">
<div class="py-4">No Audiobooks</div>
<ui-btn v-if="hasFilters" @click="clearFilter">Clear Filter</ui-btn>
</div>
<div v-show="isLoading" class="absolute top-0 left-0 w-full h-full flex items-center justify-center bg-black bg-opacity-70 z-20">
<div class="py-4">Loading...</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
currFilterOrderKey: null,
groupedBooks: [],
pageWidth: 0
}
},
computed: {
isLoading() {
return this.$store.state.audiobooks.isLoading
},
cardWidth() {
return 140
},
cardHeight() {
return this.cardWidth * 2
},
filterOrderKey() {
return this.$store.getters['user/getFilterOrderKey']
},
hasFilters() {
return this.$store.getters['user/getUserSetting']('mobileFilterBy') !== 'all'
}
},
methods: {
clearFilter() {
this.$store.dispatch('user/updateUserSettings', {
mobileFilterBy: 'all'
})
},
calcShelves() {
var booksPerShelf = Math.floor(this.pageWidth / (this.cardWidth + 32))
var groupedBooks = []
var audiobooksSorted = []
this.currFilterOrderKey = this.filterOrderKey
var numGroups = Math.ceil(audiobooksSorted.length / booksPerShelf)
for (let i = 0; i < numGroups; i++) {
var group = audiobooksSorted.slice(i * booksPerShelf, i * booksPerShelf + 2)
groupedBooks.push(group)
}
this.groupedBooks = groupedBooks
},
audiobooksUpdated() {
this.calcShelves()
},
init() {
if (this.$refs.wrapper) {
this.pageWidth = this.$refs.wrapper.clientWidth
this.calcShelves()
}
},
resize() {
this.init()
},
settingsUpdated() {
if (this.currFilterOrderKey !== this.filterOrderKey) {
this.calcShelves()
}
},
async loadAudiobooks() {
var currentLibrary = await this.$localStore.getCurrentLibrary()
if (currentLibrary) {
this.$store.commit('libraries/setCurrentLibrary', currentLibrary.id)
}
},
socketConnected(isConnected) {
if (isConnected) {
console.log('Connected - Load from server')
this.loadAudiobooks()
} else {
console.log('Disconnected - Reset to local storage')
this.$store.commit('audiobooks/reset')
this.$store.dispatch('audiobooks/useDownloaded')
// this.calcShelves()
// this.$store.dispatch('downloads/loadFromStorage')
}
}
},
mounted() {
this.$store.commit('audiobooks/addListener', { id: 'bookshelf', meth: this.audiobooksUpdated })
this.$store.commit('user/addSettingsListener', { id: 'bookshelf', meth: this.settingsUpdated })
window.addEventListener('resize', this.resize)
if (!this.$server) {
console.error('Bookshelf mounted no server')
return
}
this.$server.on('connected', this.socketConnected)
if (this.$server.connected) {
this.loadAudiobooks()
} else {
console.log('Bookshelf - Server not connected using downloaded')
}
this.init()
},
beforeDestroy() {
this.$store.commit('audiobooks/removeListener', 'bookshelf')
this.$store.commit('user/removeSettingsListener', 'bookshelf')
window.removeEventListener('resize', this.resize)
if (!this.$server) {
console.error('Bookshelf beforeDestroy no server')
return
}
this.$server.off('connected', this.socketConnected)
}
}
</script>
<style>
#bookshelf {
height: calc(100% - 48px);
}
.bookshelfRow {
background-image: url(/wood_panels.jpg);
}
.bookshelfDivider {
background: rgb(149, 119, 90);
background: linear-gradient(180deg, rgba(149, 119, 90, 1) 0%, rgba(103, 70, 37, 1) 17%, rgba(103, 70, 37, 1) 88%, rgba(71, 48, 25, 1) 100%);
box-shadow: 2px 14px 8px #111111aa;
}
</style>
-135
View File
@@ -1,135 +0,0 @@
<template>
<div id="bookshelf" ref="wrapper" class="w-full overflow-y-auto">
<template v-for="(ab, index) in audiobooks">
<div :key="index" class="border-b border-opacity-10 w-full bookshelfRow py-4 px-2 flex relative">
<app-bookshelf-list-row :audiobook="ab" :card-width="cardWidth" :page-width="pageWidth" />
<div class="bookshelfDivider h-4 w-full absolute bottom-0 left-0 right-0 z-10" />
</div>
</template>
<div v-show="!audiobooks.length" class="w-full py-16 text-center text-xl">
<div class="py-4">No Audiobooks</div>
<ui-btn v-if="hasFilters" @click="clearFilter">Clear Filter</ui-btn>
</div>
<div v-show="isLoading" class="absolute top-0 left-0 w-full h-full flex items-center justify-center bg-black bg-opacity-70 z-20">
<div class="py-4">Loading...</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
currFilterOrderKey: null,
pageWidth: 0,
audiobooks: []
}
},
computed: {
isLoading() {
return this.$store.state.audiobooks.isLoading
},
cardWidth() {
return 75
},
cardHeight() {
return this.cardWidth * 2
},
contentRowWidth() {
return this.pageWidth - 16 - this.cardWidth
},
filterOrderKey() {
return this.$store.getters['user/getFilterOrderKey']
},
hasFilters() {
return this.$store.getters['user/getUserSetting']('mobileFilterBy') !== 'all'
}
},
methods: {
clearFilter() {
this.$store.dispatch('user/updateUserSettings', {
mobileFilterBy: 'all'
})
},
calcShelves() {},
audiobooksUpdated() {
this.calcShelves()
},
init() {
if (this.$refs.wrapper) {
this.pageWidth = this.$refs.wrapper.clientWidth
this.calcShelves()
}
},
resize() {
this.init()
},
settingsUpdated() {
if (this.currFilterOrderKey !== this.filterOrderKey) {
this.calcShelves()
}
},
async loadAudiobooks() {
var currentLibrary = await this.$localStore.getCurrentLibrary()
if (currentLibrary) {
this.$store.commit('libraries/setCurrentLibrary', currentLibrary.id)
}
},
socketConnected(isConnected) {
if (isConnected) {
console.log('Connected - Load from server')
this.loadAudiobooks()
} else {
console.log('Disconnected - Reset to local storage')
this.$store.commit('audiobooks/reset')
this.$store.dispatch('audiobooks/useDownloaded')
// this.calcShelves()
// this.$store.dispatch('downloads/loadFromStorage')
}
}
},
mounted() {
this.$store.commit('audiobooks/addListener', { id: 'bookshelf', meth: this.audiobooksUpdated })
this.$store.commit('user/addSettingsListener', { id: 'bookshelf', meth: this.settingsUpdated })
window.addEventListener('resize', this.resize)
if (!this.$server) {
console.error('Bookshelf mounted no server')
return
}
this.$server.on('connected', this.socketConnected)
if (this.$server.connected) {
this.loadAudiobooks()
} else {
console.log('Bookshelf - Server not connected using downloaded')
}
this.init()
},
beforeDestroy() {
this.$store.commit('audiobooks/removeListener', 'bookshelf')
this.$store.commit('user/removeSettingsListener', 'bookshelf')
window.removeEventListener('resize', this.resize)
if (!this.$server) {
console.error('Bookshelf beforeDestroy no server')
return
}
this.$server.off('connected', this.socketConnected)
}
}
</script>
<style>
#bookshelf {
height: calc(100% - 48px);
}
.bookshelfRow {
background-image: url(/wood_panels.jpg);
}
.bookshelfDivider {
background: rgb(149, 119, 90);
background: linear-gradient(180deg, rgba(149, 119, 90, 1) 0%, rgba(103, 70, 37, 1) 17%, rgba(103, 70, 37, 1) 88%, rgba(71, 48, 25, 1) 100%);
box-shadow: 2px 14px 8px #111111aa;
}
</style>
-152
View File
@@ -1,152 +0,0 @@
<template>
<div class="w-full h-full flex">
<cards-book-card :audiobook="audiobook" :width="cardWidth" class="self-end" />
<div class="relative px-2" :style="{ width: contentRowWidth + 'px' }">
<div class="flex">
<nuxt-link :to="`/audiobook/${audiobook.id}`">
<p class="leading-6" style="font-size: 1.1rem">{{ audiobook.book.title }}</p>
</nuxt-link>
<div class="flex-grow" />
<div class="flex items-center">
<!-- <button class="mx-1" @click="editAudiobook(ab)">
<span class="material-icons text-icon pb-px">edit</span>
</button> -->
<button v-if="showRead" class="mx-1 rounded-full w-6 h-6" @click="readBook">
<span class="material-icons">auto_stories</span>
</button>
<button v-if="showPlay" class="mx-1 rounded-full w-6 h-6" @click="playAudiobook">
<span class="material-icons">play_arrow</span>
</button>
</div>
</div>
<p v-if="audiobook.book.subtitle" class="text-gray-200 leading-6 truncate" style="font-size: 0.9rem">{{ audiobook.book.subtitle }}</p>
<p class="text-sm text-gray-200">by {{ audiobook.book.author }}</p>
<div v-if="numTracks" class="flex items-center py-1">
<p class="text-xs text-gray-300">{{ $elapsedPretty(audiobook.duration) }}</p>
<span class="px-3 text-xs text-gray-300"></span>
<p class="text-xs text-gray-300 font-mono">{{ $bytesPretty(audiobook.size, 0) }}</p>
<span class="px-3 text-xs text-gray-300"></span>
<p class="text-xs text-gray-300">{{ numTracks }} tracks</p>
</div>
<div class="flex">
<div v-if="userProgressPercent && !userIsRead" class="w-min my-1">
<div class="bg-primary bg-opacity-70 text-sm px-2 py-px rounded-full whitespace-nowrap">Progress: {{ Math.floor(userProgressPercent * 100) }}%</div>
</div>
<div v-if="isDownloadPlayable" class="w-min my-1 mx-1">
<div class="bg-success bg-opacity-70 text-sm px-2 py-px rounded-full whitespace-nowrap">Downloaded</div>
</div>
<div v-else-if="isDownloading" class="w-min my-1 mx-1">
<div class="bg-warning bg-opacity-70 text-sm px-2 py-px rounded-full whitespace-nowrap">Downloading...</div>
</div>
<div v-if="isPlaying" class="w-min my-1 mx-1">
<div class="bg-info bg-opacity-70 text-sm px-2 py-px rounded-full whitespace-nowrap">{{ isStreaming ? 'Streaming' : 'Playing' }}</div>
</div>
<div v-if="hasEbook" class="w-min my-1 mx-1">
<div class="bg-bg bg-opacity-70 text-sm px-2 py-px rounded-full whitespace-nowrap">{{ ebookFormat }}</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
audiobook: {
type: Object,
default: () => {}
},
cardWidth: {
type: Number,
default: 75
},
pageWidth: Number
},
data() {
return {}
},
computed: {
audiobookId() {
return this.audiobook.id
},
mostRecentUserProgress() {
return this.$store.getters['user/getUserAudiobookData'](this.audiobookId)
},
userProgressPercent() {
return this.mostRecentUserProgress ? this.mostRecentUserProgress.progress || 0 : 0
},
userIsRead() {
return this.mostRecentUserProgress ? !!this.mostRecentUserProgress.isRead : false
},
contentRowWidth() {
return this.pageWidth - 16 - this.cardWidth
},
isDownloading() {
return this.downloadObj ? this.downloadObj.isDownloading : false
},
isDownloadPreparing() {
return this.downloadObj ? this.downloadObj.isPreparing : false
},
isDownloadPlayable() {
return this.downloadObj && !this.isDownloading && !this.isDownloadPreparing
},
downloadedCover() {
return this.downloadObj ? this.downloadObj.cover : null
},
downloadObj() {
return this.$store.getters['downloads/getDownload'](this.audiobookId)
},
isStreaming() {
return this.$store.getters['isAudiobookStreaming'](this.audiobookId)
},
isPlaying() {
return this.$store.getters['isAudiobookPlaying'](this.audiobookId)
},
isMissing() {
return this.audiobook.isMissing
},
isIncomplete() {
return this.audiobook.isIncomplete
},
numTracks() {
if (this.audiobook.tracks) return this.audiobook.tracks.length
return this.audiobook.numTracks || 0
},
showPlay() {
return !this.isPlaying && !this.isMissing && !this.isIncomplete && this.numTracks
},
showRead() {
return this.hasEbook && this.ebookFormat !== '.pdf'
},
hasEbook() {
return this.audiobook.numEbooks
},
ebookFormat() {
if (!this.audiobook || !this.audiobook.ebooks || !this.audiobook.ebooks.length) return null
return this.audiobook.ebooks[0].ext.substr(1)
}
},
methods: {
readBook() {
this.$store.commit('openReader', this.audiobook)
},
playAudiobook() {
if (this.isPlaying) {
return
}
this.$store.commit('setPlayOnLoad', true)
if (!this.isDownloadPlayable) {
// Stream
console.log('[PLAYCLICK] Set Playing STREAM ' + this.audiobook.book.title)
this.$store.commit('setStreamAudiobook', this.audiobook)
this.$server.socket.emit('open_stream', this.audiobook.id)
} else {
// Local
console.log('[PLAYCLICK] Set Playing Local Download ' + this.audiobook.book.title)
this.$store.commit('setPlayingDownload', this.downloadObj)
}
}
},
mounted() {}
}
</script>
-3
View File
@@ -113,9 +113,6 @@ export default {
})
this.$server.logout()
this.$router.push('/connect')
this.$store.commit('audiobooks/reset')
this.$store.dispatch('audiobooks/useDownloaded')
},
touchstart(e) {
this.touchEvent = new TouchEvent(e)
-35
View File
@@ -1,35 +0,0 @@
<template>
<div class="w-full relative">
<div class="bookshelfRow flex items-end justify-around px-3 max-w-full" :class="shelfHeightClass">
<template v-for="group in groups">
<cards-series-card v-if="groupType === 'series'" :key="group.id" :group="group" :width="112" class="mx-2" />
<cards-collection-card v-if="groupType === 'collection'" :key="group.id" :collection="group" :width="90" class="mx-2" />
</template>
</div>
<div class="w-full h-5 z-40 bookshelfDivider"></div>
</div>
</template>
<script>
export default {
props: {
groupType: String,
groups: {
type: Array,
default: () => []
}
},
data() {
return {}
},
computed: {
shelfHeightClass() {
if (this.groupType === 'series') return 'h-48'
return 'h-44'
}
},
methods: {},
mounted() {}
}
</script>
+121 -34
View File
@@ -1,16 +1,11 @@
<template>
<div id="bookshelf" class="w-full max-w-full h-full min-h-screen">
<div id="bookshelf" class="w-full max-w-full h-full">
<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 class="bookshelfDivider w-full absolute bottom-0 left-0 z-30" style="min-height: 16px" :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>
@@ -121,8 +116,17 @@ export default {
// Includes margin
return this.entityWidth + 24
},
downloads() {
return this.$store.getters['downloads/getDownloads']
},
downloadedBooks() {
return this.$store.getters['downloads/getAudiobooks']
return this.downloads.map((dl) => {
var download = { ...dl }
var ab = { ...download.audiobook }
delete download.audiobook
ab.download = download
return ab
})
}
},
methods: {
@@ -169,6 +173,16 @@ export default {
}
for (let i = 0; i < payload.results.length; i++) {
if (this.entityName === 'books' || this.entityName === 'series-books') {
// Check if has download and append download obj
var download = this.downloads.find((dl) => dl.id === payload.results[i].id)
if (download) {
var dl = { ...download }
delete dl.audiobook
payload.results[i].download = dl
}
}
var index = i + startIndex
this.entities[index] = payload.results[i]
if (this.entityComponentRefs[index]) {
@@ -227,12 +241,17 @@ export default {
}
},
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)
if (this.entityName === 'books') {
this.entities = this.downloadedBooks
// TOOD: Sort and filter here
this.totalEntities = this.entities.length
this.totalShelves = Math.ceil(this.totalEntities / this.entitiesPerShelf)
} else {
// TODO: Support offline series and collections
this.entities = []
this.totalEntities = 0
this.totalShelves = 0
}
this.$eventBus.$emit('bookshelf-total-entities', this.totalEntities)
},
async resetEntities() {
@@ -257,9 +276,22 @@ export default {
this.mountEntites(0, lastBookIndex)
} else {
this.setDownloads()
this.mountEntites(0, this.totalEntities - 1)
}
},
remountEntities() {
// Remount when an entity is removed
for (const key in this.entityComponentRefs) {
if (this.entityComponentRefs[key]) {
this.entityComponentRefs[key].destroy()
}
}
this.entityComponentRefs = {}
this.entityIndexesMounted.forEach((i) => {
this.cardsHelpers.mountEntityCard(i)
})
},
initSizeData() {
var bookshelf = document.getElementById('bookshelf')
if (!bookshelf) {
@@ -282,15 +314,23 @@ export default {
}
return entitiesPerShelfBefore < this.entitiesPerShelf // Books per shelf has changed
},
async init(bookshelf) {
async init() {
if (this.isFirstInit) return
this.isFirstInit = true
this.initSizeData(bookshelf)
this.initSizeData()
await this.loadPage(0)
var lastBookIndex = Math.min(this.totalEntities, this.shelvesPerPage * this.entitiesPerShelf)
this.mountEntites(0, lastBookIndex)
},
initDownloads() {
this.initSizeData()
this.setDownloads()
this.$nextTick(() => {
console.log('Mounting downloads', this.totalEntities, 'total shelves', this.totalShelves)
this.mountEntites(0, this.totalEntities)
})
},
scroll(e) {
if (!e || !e.target) return
if (!this.isSocketConnected) return // Offline books are all mounted at once
@@ -345,6 +385,49 @@ export default {
this.resetEntities()
}
},
downloadsLoaded() {
if (!this.isSocketConnected) {
this.resetEntities()
}
},
audiobookAdded(audiobook) {
console.log('Audiobook added', audiobook)
// TODO: Check if audiobook would be on this shelf
this.resetEntities()
},
audiobookUpdated(audiobook) {
console.log('Audiobook updated', audiobook)
if (this.entityName === 'books' || this.entityName === 'series-books') {
var indexOf = this.entities.findIndex((ent) => ent && ent.id === audiobook.id)
if (indexOf >= 0) {
this.entities[indexOf] = audiobook
if (this.entityComponentRefs[indexOf]) {
this.entityComponentRefs[indexOf].setEntity(audiobook)
}
}
}
},
audiobookRemoved(audiobook) {
if (this.entityName === 'books' || this.entityName === 'series-books') {
var indexOf = this.entities.findIndex((ent) => ent && ent.id === audiobook.id)
if (indexOf >= 0) {
this.entities = this.entities.filter((ent) => ent.id !== audiobook.id)
this.totalEntities = this.entities.length
this.$eventBus.$emit('bookshelf-total-entities', this.totalEntities)
this.remountEntities()
}
}
},
audiobooksAdded(audiobooks) {
console.log('audiobooks added', audiobooks)
// TODO: Check if audiobook would be on this shelf
this.resetEntities()
},
audiobooksUpdated(audiobooks) {
audiobooks.forEach((ab) => {
this.audiobookUpdated(ab)
})
},
initListeners() {
var bookshelf = document.getElementById('bookshelf-wrapper')
if (bookshelf) {
@@ -354,17 +437,18 @@ export default {
// this.$eventBus.$on('bookshelf-select-all', this.selectAllEntities)
// this.$eventBus.$on('bookshelf-keyword-filter', this.updateKeywordFilter)
this.$eventBus.$on('library-changed', this.libraryChanged)
this.$eventBus.$on('downloads-loaded', this.downloadsLoaded)
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')
// }
if (this.$server.socket) {
this.$server.socket.on('audiobook_updated', this.audiobookUpdated)
this.$server.socket.on('audiobook_added', this.audiobookAdded)
this.$server.socket.on('audiobook_removed', this.audiobookRemoved)
this.$server.socket.on('audiobooks_updated', this.audiobooksUpdated)
this.$server.socket.on('audiobooks_added', this.audiobooksAdded)
} else {
console.error('Bookshelf - Socket not initialized')
}
},
removeListeners() {
var bookshelf = document.getElementById('bookshelf-wrapper')
@@ -372,22 +456,25 @@ export default {
bookshelf.removeEventListener('scroll', this.scroll)
}
this.$eventBus.$off('library-changed', this.libraryChanged)
this.$eventBus.$off('downloads-loaded', this.downloadsLoaded)
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')
// }
if (this.$server.socket) {
this.$server.socket.off('audiobook_updated', this.audiobookUpdated)
this.$server.socket.off('audiobook_added', this.audiobookAdded)
this.$server.socket.off('audiobook_removed', this.audiobookRemoved)
this.$server.socket.off('audiobooks_updated', this.audiobooksUpdated)
this.$server.socket.off('audiobooks_added', this.audiobooksAdded)
} else {
console.error('Bookshelf - Socket not initialized')
}
}
},
mounted() {
if (this.$server.initialized) {
this.init()
} else {
this.initDownloads()
}
this.$server.on('initialized', this.socketInit)
this.initListeners()
-28
View File
@@ -1,28 +0,0 @@
<template>
<div class="w-full relative">
<div class="bookshelfRow h-48 flex items-end justify-around px-3 max-w-full">
<template v-for="book in books">
<cards-book-card :key="book.id" :audiobook="book" :width="108" class="mx-2" />
</template>
</div>
<div class="w-full h-4 z-40 bookshelfDivider"></div>
</div>
</template>
<script>
export default {
props: {
books: {
type: Array,
default: () => []
}
},
data() {
return {}
},
computed: {},
methods: {},
mounted() {}
}
</script>
-1
View File
@@ -2,7 +2,6 @@
<div class="w-full relative">
<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>
-95
View File
@@ -1,95 +0,0 @@
<template>
<div class="relative">
<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' }">
<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>
</div>
<div class="absolute bottom-0 left-0 h-1.5 shadow-sm" :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', bottom: sizeMultiplier + 'rem' }" class="bg-error rounded-r-full shadow-md flex items-center justify-end border-r border-b border-red-300 absolute left-0">
<span class="material-icons text-red-100 pr-1" :style="{ fontSize: 0.875 * sizeMultiplier + 'rem' }">priority_high</span>
</div>
</div>
</nuxt-link>
</div>
</div>
</template>
<script>
export default {
props: {
audiobook: {
type: Object,
default: () => null
},
width: {
type: Number,
default: 140
},
bookCoverAspectRatio: Number
},
data() {
return {}
},
computed: {
tags() {
return this.audiobook.tags || []
},
audiobookId() {
return this.audiobook.id
},
book() {
return this.audiobook.book || {}
},
height() {
return this.width * this.bookCoverAspectRatio
},
sizeMultiplier() {
if (this.bookCoverAspectRatio === 1) return this.width / 160
return this.width / 100
},
mostRecentUserProgress() {
return this.$store.getters['user/getUserAudiobookData'](this.audiobookId)
},
userProgressPercent() {
return this.mostRecentUserProgress ? this.mostRecentUserProgress.progress || 0 : 0
},
userIsRead() {
return this.mostRecentUserProgress ? !!this.mostRecentUserProgress.isRead : false
},
showError() {
return this.hasMissingParts || this.hasInvalidParts
},
hasMissingParts() {
return this.audiobook.hasMissingParts
},
hasInvalidParts() {
return this.audiobook.hasInvalidParts
},
downloadCover() {
return this.download ? this.download.cover : null
},
download() {
return this.$store.getters['downloads/getDownloadIfReady'](this.audiobookId)
},
errorText() {
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'
}
},
methods: {},
mounted() {}
}
</script>
-167
View File
@@ -1,167 +0,0 @@
<template>
<div class="relative rounded-sm overflow-hidden" :style="{ height: width * 1.6 + 'px', width: width + 'px', maxWidth: width + 'px', minWidth: width + 'px' }">
<div class="w-full h-full relative">
<div class="bg-primary absolute top-0 left-0 w-full h-full">
<!-- Blurred background for covers that dont fill -->
<div v-if="showCoverBg" class="w-full h-full z-0" ref="coverBg" />
<!-- Image Loading indicator -->
<div v-if="!isImageLoaded" class="w-full h-full flex items-center justify-center text-white">
<svg class="animate-spin w-12 h-12" viewBox="0 0 24 24">
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
</svg>
</div>
</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-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" 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" :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>
</template>
<script>
export default {
props: {
audiobook: {
type: Object,
default: () => {}
},
downloadCover: String,
authorOverride: String,
width: {
type: Number,
default: 120
}
},
data() {
return {
imageFailed: false,
showCoverBg: false,
isImageLoaded: false
}
},
watch: {
cover() {
this.imageFailed = false
}
},
computed: {
userToken() {
return this.$store.getters['user/getToken']
},
book() {
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'
},
serverUrl() {
return this.$store.state.serverUrl
},
networkConnected() {
return this.$store.state.networkConnected
},
fullCoverUrl() {
if (this.downloadCover) return this.downloadCover
else if (!this.networkConnected) return this.placeholderUrl
return this.$store.getters['audiobooks/getBookCoverSrc'](this.audiobook)
// if (this.cover.startsWith('http')) return this.cover
// var _clean = this.cover.replace(/\\/g, '/')
// if (_clean.startsWith('/local')) {
// var _cover = process.env.NODE_ENV !== 'production' && process.env.PROD !== '1' ? _clean.replace('/local', '') : _clean
// return `${this.$store.state.serverUrl}${_cover}?token=${this.userToken}&ts=${Date.now()}`
// } else if (_clean.startsWith('/metadata')) {
// return `${this.$store.state.serverUrl}${_clean}?token=${this.userToken}&ts=${Date.now()}`
// }
// return _clean
},
cover() {
return this.book.cover || this.placeholderUrl
},
hasCover() {
if (!this.networkConnected && !this.downloadCover) return false
return !!this.book.cover
},
sizeMultiplier() {
return this.width / 120
},
titleFontSize() {
return 0.75 * this.sizeMultiplier
},
authorFontSize() {
return 0.6 * this.sizeMultiplier
},
placeholderCoverPadding() {
return 0.8 * this.sizeMultiplier
},
authorBottom() {
return 0.75 * this.sizeMultiplier
}
},
methods: {
setCoverBg() {
if (this.$refs.coverBg) {
this.$refs.coverBg.style.backgroundImage = `url("${this.fullCoverUrl}")`
this.$refs.coverBg.style.backgroundSize = 'cover'
this.$refs.coverBg.style.backgroundPosition = 'center'
this.$refs.coverBg.style.opacity = 0.25
this.$refs.coverBg.style.filter = 'blur(1px)'
}
},
hideCoverBg() {},
imageLoaded() {
if (this.$refs.cover && this.cover !== this.placeholderUrl) {
var { naturalWidth, naturalHeight } = this.$refs.cover
var aspectRatio = naturalHeight / naturalWidth
var arDiff = Math.abs(aspectRatio - 1.6)
// If image aspect ratio is <= 1.45 or >= 1.75 then use cover bg, otherwise stretch to fit
if (arDiff > 0.15) {
this.showCoverBg = true
this.$nextTick(this.setCoverBg)
} else {
this.showCoverBg = false
}
}
this.isImageLoaded = true
},
imageError(err) {
this.imageFailed = true
console.error('ImgError', err, `SET IMAGE FAILED ${this.imageFailed}`)
}
}
}
</script>
-86
View File
@@ -1,86 +0,0 @@
<template>
<div class="relative">
<div class="rounded-sm h-full relative" @click="clickCard">
<nuxt-link :to="groupTo" class="cursor-pointer">
<div class="w-full relative bg-primary" :style="{ height: coverHeight + 'px', width: coverWidth + 'px' }">
<cards-collection-cover ref="groupcover" :book-items="bookItems" :width="coverWidth" :height="coverHeight" />
</div>
</nuxt-link>
</div>
<div class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-5 h-5 rounded-md font-book text-center" :style="{ width: Math.min(160, coverWidth) + 'px' }">
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.8 * sizeMultiplier}rem` }">
<p class="truncate pt-px" :style="{ fontSize: labelFontSize + 'rem' }">{{ collectionName }}</p>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
collection: {
type: Object,
default: () => null
},
width: {
type: Number,
default: 120
},
paddingY: {
type: Number,
default: 24
}
},
data() {
return {}
},
watch: {
width(newVal) {
this.$nextTick(() => {
if (this.$refs.groupcover && this.$refs.groupcover.init) {
this.$refs.groupcover.init()
}
})
}
},
computed: {
labelFontSize() {
if (this.coverWidth < 160) return 0.7
return 0.75
},
currentLibraryId() {
return this.$store.state.libraries.currentLibraryId
},
_collection() {
return this.collection || {}
},
groupTo() {
return `/collection/${this._collection.id}`
},
coverWidth() {
return this.width * 2
},
coverHeight() {
return this.width * 1.6
},
sizeMultiplier() {
return this.width / 120
},
paddingX() {
return 16 * this.sizeMultiplier
},
bookItems() {
return this._collection.books || []
},
collectionName() {
return this._collection.name || 'No Name'
}
},
methods: {
clickCard() {
this.$emit('click', this.collection)
}
}
}
</script>
-63
View File
@@ -1,63 +0,0 @@
<template>
<div class="relative rounded-sm overflow-hidden" :style="{ width: width + 'px', height: height + 'px' }">
<!-- <div class="absolute top-0 left-0 w-full h-full rounded-sm overflow-hidden z-10">
<div class="w-full h-full border border-white border-opacity-10" />
</div> -->
<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" />
<cards-book-cover :audiobook="books[0]" :width="width / 2" />
<cards-book-cover v-if="books.length > 1" :audiobook="books[1]" :width="width / 2" />
</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
},
data() {
return {
imageFailed: false,
showCoverBg: false
}
},
computed: {
sizeMultiplier() {
return this.width / 120
},
hasOwnCover() {
return false
},
fullCoverUrl() {
return null
},
books() {
return this.bookItems || []
}
},
methods: {
imageError() {},
imageLoaded() {}
},
mounted() {}
}
</script>
+15 -1
View File
@@ -23,6 +23,12 @@
</div>
</div>
<!-- Downloaded indicator icon -->
<div v-if="hasDownload" class="absolute z-10" :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>
</div>
<!-- Progress bar -->
<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">
@@ -74,7 +80,15 @@ export default {
placeholderUrl() {
return '/book_placeholder.jpg'
},
hasDownload() {
return !!this._audiobook.download
},
downloadedCover() {
if (!this._audiobook.download) return null
return this._audiobook.download.cover
},
bookCoverSrc() {
if (this.downloadedCover) return this.downloadedCover
return this.store.getters['audiobooks/getBookCoverSrc'](this._audiobook, this.placeholderUrl)
},
audiobookId() {
@@ -96,7 +110,7 @@ export default {
return this.bookCoverAspectRatio === 1
},
sizeMultiplier() {
var baseSize = this.squareAspectRatio ? 192 : 120
var baseSize = this.squareAspectRatio ? 160 : 100
return this.width / baseSize
},
title() {
-167
View File
@@ -1,167 +0,0 @@
<template>
<div class="relative rounded-sm overflow-hidden w-full h-full">
<div class="w-full h-full relative">
<div class="bg-primary absolute top-0 left-0 w-full h-full">
<!-- Blurred background for covers that dont fill -->
<div v-if="showCoverBg" class="w-full h-full z-0" ref="coverBg" />
<!-- Image Loading indicator -->
<div v-if="!isImageLoaded" class="w-full h-full flex items-center justify-center text-white">
<svg class="animate-spin w-12 h-12" viewBox="0 0 24 24">
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
</svg>
</div>
</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-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" 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" :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>
</template>
<script>
export default {
props: {
audiobook: {
type: Object,
default: () => {}
},
downloadCover: String,
authorOverride: String,
width: {
type: Number,
default: 120
}
},
data() {
return {
imageFailed: false,
showCoverBg: false,
isImageLoaded: false
}
},
watch: {
cover() {
this.imageFailed = false
}
},
computed: {
userToken() {
return this.$store.getters['user/getToken']
},
book() {
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'
},
serverUrl() {
return this.$store.state.serverUrl
},
networkConnected() {
return this.$store.state.networkConnected
},
fullCoverUrl() {
if (this.downloadCover) return this.downloadCover
else if (!this.networkConnected) return this.placeholderUrl
return this.$store.getters['audiobooks/getBookCoverSrc'](this.audiobook)
// if (this.cover.startsWith('http')) return this.cover
// var _clean = this.cover.replace(/\\/g, '/')
// if (_clean.startsWith('/local')) {
// var _cover = process.env.NODE_ENV !== 'production' && process.env.PROD !== '1' ? _clean.replace('/local', '') : _clean
// return `${this.$store.state.serverUrl}${_cover}?token=${this.userToken}&ts=${Date.now()}`
// } else if (_clean.startsWith('/metadata')) {
// return `${this.$store.state.serverUrl}${_clean}?token=${this.userToken}&ts=${Date.now()}`
// }
// return _clean
},
cover() {
return this.book.cover || this.placeholderUrl
},
hasCover() {
if (!this.networkConnected && !this.downloadCover) return false
return !!this.book.cover
},
sizeMultiplier() {
return this.width / 120
},
titleFontSize() {
return 0.75 * this.sizeMultiplier
},
authorFontSize() {
return 0.6 * this.sizeMultiplier
},
placeholderCoverPadding() {
return 0.8 * this.sizeMultiplier
},
authorBottom() {
return 0.75 * this.sizeMultiplier
}
},
methods: {
setCoverBg() {
if (this.$refs.coverBg) {
this.$refs.coverBg.style.backgroundImage = `url("${this.fullCoverUrl}")`
this.$refs.coverBg.style.backgroundSize = 'cover'
this.$refs.coverBg.style.backgroundPosition = 'center'
this.$refs.coverBg.style.opacity = 0.25
this.$refs.coverBg.style.filter = 'blur(1px)'
}
},
hideCoverBg() {},
imageLoaded() {
if (this.$refs.cover && this.cover !== this.placeholderUrl) {
var { naturalWidth, naturalHeight } = this.$refs.cover
var aspectRatio = naturalHeight / naturalWidth
var arDiff = Math.abs(aspectRatio - 1.6)
// If image aspect ratio is <= 1.45 or >= 1.75 then use cover bg, otherwise stretch to fit
if (arDiff > 0.15) {
this.showCoverBg = true
this.$nextTick(this.setCoverBg)
} else {
this.showCoverBg = false
}
}
this.isImageLoaded = true
},
imageError(err) {
this.imageFailed = true
console.error('ImgError', err, `SET IMAGE FAILED ${this.imageFailed}`)
}
}
}
</script>
-113
View File
@@ -1,113 +0,0 @@
<template>
<div class="rounded-sm relative" @click="clickCard">
<nuxt-link :to="groupTo" class="cursor-pointer">
<div class="w-full relative bg-primary" :style="{ height: coverHeight + 'px', width: coverWidth + 'px' }">
<cards-series-cover ref="groupcover" :name="groupName" :group-to="groupTo" :type="groupType" :book-items="bookItems" :width="coverWidth" :height="coverHeight" />
<div class="absolute top-2 right-2 w-7 h-7 rounded-lg bg-black bg-opacity-90 text-gray-300 box-shadow-book flex items-center justify-center border border-white border-opacity-25 pointer-events-none z-40">
<p class="font-book text-xl">{{ bookItems.length }}</p>
</div>
<div class="absolute bottom-0 left-0 w-full h-1 flex flex-nowrap z-40">
<div v-for="userProgress in userProgressItems" :key="userProgress.audiobookId" class="h-full w-full" :class="userProgress.isRead ? 'bg-success' : userProgress.progress > 0 ? 'bg-yellow-400' : ''" />
</div>
</div>
</nuxt-link>
<div class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-5 h-5 rounded-md font-book text-center" :style="{ width: Math.min(160, coverWidth) + 'px' }">
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.8 * sizeMultiplier}rem` }">
<p class="truncate pt-px" :style="{ fontSize: labelFontSize + 'rem' }">{{ groupName }}</p>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
group: {
type: Object,
default: () => null
},
width: {
type: Number,
default: 120
}
},
data() {
return {}
},
watch: {
width(newVal) {
this.$nextTick(() => {
if (this.$refs.groupcover && this.$refs.groupcover.init) {
this.$refs.groupcover.init()
}
})
}
},
computed: {
currentLibraryId() {
return this.$store.state.libraries.currentLibraryId
},
labelFontSize() {
if (this.coverWidth < 160) return 0.7
return 0.75
},
_group() {
return this.group || {}
},
groupType() {
return this._group.type
},
groupTo() {
if (this.groupType === 'series') {
return `/bookshelf/series?series=${this.groupEncode}`
} else {
return `/bookshelf?filter=tags.${this.groupEncode}`
}
},
coverWidth() {
return this.coverHeight
},
coverHeight() {
return this.width * 1.6
},
sizeMultiplier() {
return this.width / 120
},
paddingX() {
return 16 * this.sizeMultiplier
},
bookItems() {
return this._group.books || []
},
userAudiobooks() {
return Object.values(this.$store.state.user.user ? this.$store.state.user.user.audiobooks || {} : {})
},
userProgressItems() {
return this.bookItems.map((item) => {
var userAudiobook = this.userAudiobooks.find((ab) => ab.audiobookId === item.id)
return userAudiobook || {}
})
},
groupName() {
return this._group.name || 'No Name'
},
groupEncode() {
return this.$encode(this.groupName)
},
filter() {
return `${this.groupType}.${this.$encode(this.groupName)}`
},
hasValidCovers() {
var validCovers = this.bookItems.map((bookItem) => bookItem.book.cover)
return !!validCovers.length
}
},
methods: {
clickCard() {
this.$emit('click', this.group)
}
}
}
</script>
-171
View File
@@ -1,171 +0,0 @@
<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: {
name: String,
bookItems: {
type: Array,
default: () => []
},
width: Number,
height: Number
},
data() {
return {
noValidCovers: false,
coverDiv: null,
coverWrapperEl: null,
coverImageEls: [],
coverWidth: 0,
offsetIncrement: 0,
windowWidth: 0
}
},
watch: {
bookItems: {
immediate: true,
handler(newVal) {
if (newVal) {
// ensure wrapper is initialized
this.$nextTick(this.init)
}
}
}
},
computed: {
sizeMultiplier() {
return this.width / 192
}
},
methods: {
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 - 1.6)
// 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 bg-primary'
var coverbg = document.createElement('div')
coverbg.className = 'w-full h-full'
coverbg.style.backgroundImage = `url("${src}")`
coverbg.style.backgroundSize = 'cover'
coverbg.style.backgroundPosition = 'center'
coverbg.style.opacity = 0.25
coverbg.style.filter = 'blur(1px)'
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.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 / 1.6
widthPer = (this.width - coverWidth) / (validCovers.length - 1)
}
this.coverWidth = coverWidth
this.offsetIncrement = widthPer
var outerdiv = document.createElement('div')
this.coverWrapperEl = outerdiv
outerdiv.className = 'w-full h-full relative'
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()
}
}
</script>
+1 -1
View File
@@ -9,7 +9,7 @@
<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>
<!-- <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" />
-100
View File
@@ -1,100 +0,0 @@
<template>
<modals-modal v-model="show" width="90%" height="100%">
<div class="w-full h-full overflow-hidden absolute top-0 left-0 flex items-center justify-center" @click="show = false">
<div class="w-full overflow-x-hidden overflow-y-auto bg-primary rounded-lg border border-white border-opacity-20 p-8" style="max-height: 75%" @click.stop>
<ui-text-input ref="input" v-model="search" @input="updateSearch" placeholder="Search" class="w-full text-lg" />
<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">
<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>
</div>
</template>
</div>
</div>
</modals-modal>
</template>
<script>
export default {
props: {
value: Boolean
},
data() {
return {
search: null,
searchTimeout: null,
lastSearch: null,
isFetching: false,
items: []
}
},
watch: {
value(newVal) {
if (newVal) {
this.$nextTick(this.setFocus())
}
}
},
computed: {
show: {
get() {
return this.value
},
set(val) {
this.$emit('input', val)
}
}
},
methods: {
clickItem(item) {
this.show = false
this.$router.push(`/audiobook/${item.id}`)
},
async runSearch(value) {
this.lastSearch = value
if (!this.lastSearch) {
this.items = []
return
}
this.isFetching = true
var results = await this.$axios.$get(`/api/books?q=${value}`).catch((error) => {
console.error('Search error', error)
return []
})
this.isFetching = false
this.items = results.map((res) => {
return {
id: res.id,
data: res,
type: 'audiobook'
}
})
},
updateSearch(val) {
clearTimeout(this.searchTimeout)
this.searchTimeout = setTimeout(() => {
this.runSearch(val)
}, 500)
},
setFocus() {
setTimeout(() => {
if (this.$refs.input) {
this.$refs.input.focus()
}
}, 100)
}
},
mounted() {}
}
</script>
@@ -1,8 +1,8 @@
<template>
<div class="w-full px-2 py-2 overflow-hidden relative">
<div v-if="book" class="flex h-20">
<div class="h-full relative" :style="{ width: '50px' }">
<cards-book-cover :audiobook="book" :width="50" />
<div class="h-full relative" :style="{ width: bookWidth + 'px' }">
<covers-book-cover :audiobook="book" :width="bookWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
</div>
<div class="w-80 h-full px-2 flex items-center">
<div>
@@ -38,6 +38,13 @@ export default {
}
},
computed: {
bookCoverAspectRatio() {
return this.$store.getters['getBookCoverAspectRatio']
},
bookWidth() {
if (this.bookCoverAspectRatio === 1) return 80
return 50
},
_book() {
return this.book.book || {}
},