Add bookshelf list view

This commit is contained in:
advplyr 2022-04-07 19:59:23 -05:00
parent 119bfd6c98
commit 105451ebf1
8 changed files with 471 additions and 255 deletions

View file

@ -273,220 +273,3 @@ class AbsDownloader : Plugin() {
} }
} }
} }
//
// @PluginMethod
// fun download(call: PluginCall) {
// var audiobookId = call.data.getString("audiobookId", "audiobook").toString()
// var url = call.data.getString("downloadUrl", "unknown").toString()
// var coverDownloadUrl = call.data.getString("coverDownloadUrl", "").toString()
// var title = call.data.getString("title", "Audiobook").toString()
// var filename = call.data.getString("filename", "audiobook.mp3").toString()
// var coverFilename = call.data.getString("coverFilename", "cover.png").toString()
// var downloadFolderUrl = call.data.getString("downloadFolderUrl", "").toString()
// var folder = DocumentFileCompat.fromUri(context, Uri.parse(downloadFolderUrl))!!
// Log.d(tag, "Called download: $url | Folder: ${folder.name} | $downloadFolderUrl")
//
// var dlfilename = audiobookId + "." + File(filename).extension
// var coverdlfilename = audiobookId + "." + File(coverFilename).extension
// Log.d(tag, "DL Filename $dlfilename | Cover DL Filename $coverdlfilename")
//
// var canWriteToFolder = folder.canWrite()
// if (!canWriteToFolder) {
// Log.e(tag, "Error Cannot Write to Folder ${folder.baseName}")
// val ret = JSObject()
// ret.put("error", "Cannot write to ${folder.baseName}")
// call.resolve(ret)
// return
// }
//
// var dlRequest = DownloadManager.Request(Uri.parse(url))
// dlRequest.setTitle("Ab: $title")
// dlRequest.setDescription("Downloading to ${folder.name}")
// dlRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
// dlRequest.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, dlfilename)
//
// var audiobookDownloadId = downloadManager.enqueue(dlRequest)
// var coverDownloadId:Long? = null
//
// if (coverDownloadUrl != "") {
// var coverDlRequest = DownloadManager.Request(Uri.parse(coverDownloadUrl))
// coverDlRequest.setTitle("Cover: $title")
// coverDlRequest.setDescription("Downloading to ${folder.name}")
// coverDlRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION)
// coverDlRequest.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, coverdlfilename)
// coverDownloadId = downloadManager.enqueue(coverDlRequest)
// }
//
// var progressReceiver : (id:Long, prog: Long) -> Unit = { id:Long, prog: Long ->
// if (id == audiobookDownloadId) {
// var jsobj = JSObject()
// jsobj.put("audiobookId", audiobookId)
// jsobj.put("progress", prog)
// notifyListeners("onDownloadProgress", jsobj)
// }
// }
//
// var coverDocFile:DocumentFile? = null
//
// var doneReceiver : (id:Long, success: Boolean) -> Unit = { id:Long, success: Boolean ->
// Log.d(tag, "RECEIVER DONE $id, SUCCES? $success")
// var docfile:DocumentFile? = null
//
// // Download was complete, now find downloaded file
// if (id == coverDownloadId) {
// docfile = DocumentFileCompat.fromPublicFolder(context, PublicDirectory.DOWNLOADS, coverdlfilename)
// Log.d(tag, "Move Cover File ${docfile?.name}")
//
// // For unknown reason, Android 10 test was using the title set in "setTitle" for the dl manager as the filename
// // check if this was the case
// if (docfile?.name == null) {
// docfile = DocumentFileCompat.fromPublicFolder(context, PublicDirectory.DOWNLOADS, "Cover: $title")
// Log.d(tag, "Cover File name attempt 2 ${docfile?.name}")
// }
// } else if (id == audiobookDownloadId) {
// docfile = DocumentFileCompat.fromPublicFolder(context, PublicDirectory.DOWNLOADS, dlfilename)
// Log.d(tag, "Move Audiobook File ${docfile?.name}")
//
// if (docfile?.name == null) {
// docfile = DocumentFileCompat.fromPublicFolder(context, PublicDirectory.DOWNLOADS, "Ab: $title")
// Log.d(tag, "File name attempt 2 ${docfile?.name}")
// }
// }
//
// // Callback for moving the downloaded file
// var callback = object : FileCallback() {
// override fun onPrepare() {
// Log.d(tag, "PREPARING MOVE FILE")
// }
// override fun onFailed(errorCode:ErrorCode) {
// Log.e(tag, "FAILED MOVE FILE $errorCode")
//
// docfile?.delete()
// coverDocFile?.delete()
//
// if (id == audiobookDownloadId) {
// var jsobj = JSObject()
// jsobj.put("audiobookId", audiobookId)
// jsobj.put("error", "Move failed")
// notifyListeners("onDownloadFailed", jsobj)
// }
// }
// override fun onCompleted(result:Any) {
// var resultDocFile = result as DocumentFile
// var simplePath = resultDocFile.getSimplePath(context)
// var storageId = resultDocFile.getStorageId(context)
// var size = resultDocFile.length()
// Log.d(tag, "Finished Moving File, NAME: ${resultDocFile.name} | URI:${resultDocFile.uri} | AbsolutePath:${resultDocFile.getAbsolutePath(context)} | $storageId | SimplePath: $simplePath")
//
// var abFolder = folder.findFolder(title)
// var jsobj = JSObject()
// jsobj.put("audiobookId", audiobookId)
// jsobj.put("downloadId", id)
// jsobj.put("storageId", storageId)
// jsobj.put("storageType", resultDocFile.getStorageType(context))
// jsobj.put("folderUrl", abFolder?.uri)
// jsobj.put("folderName", abFolder?.name)
// jsobj.put("downloadFolderUrl", downloadFolderUrl)
// jsobj.put("contentUrl", resultDocFile.uri)
// jsobj.put("basePath", resultDocFile.getBasePath(context))
// jsobj.put("filename", filename)
// jsobj.put("simplePath", simplePath)
// jsobj.put("size", size)
//
// if (resultDocFile.name == filename) {
// Log.d(tag, "Audiobook Finishing Moving")
// } else if (resultDocFile.name == coverFilename) {
// coverDocFile = docfile
// Log.d(tag, "Audiobook Cover Finished Moving")
// jsobj.put("isCover", true)
// }
// notifyListeners("onDownloadComplete", jsobj)
// }
// }
//
// // After file is downloaded, move the files into an audiobook directory inside the user selected folder
// if (id == coverDownloadId) {
// docfile?.moveFileTo(context, folder, FileDescription(coverFilename, title, MimeType.IMAGE), callback)
// } else if (id == audiobookDownloadId) {
// docfile?.moveFileTo(context, folder, FileDescription(filename, title, MimeType.AUDIO), callback)
// }
// }
//
// var progressUpdater = DownloadProgressUpdater(downloadManager, audiobookDownloadId, progressReceiver, doneReceiver)
// progressUpdater.run()
// if (coverDownloadId != null) {
// var coverProgressUpdater = DownloadProgressUpdater(downloadManager, coverDownloadId, progressReceiver, doneReceiver)
// coverProgressUpdater.run()
// }
//
// val ret = JSObject()
// ret.put("audiobookDownloadId", audiobookDownloadId)
// ret.put("coverDownloadId", coverDownloadId)
// call.resolve(ret)
// }
//
//
//internal class DownloadProgressUpdater(private val manager: DownloadManager, private val downloadId: Long, private var receiver: (Long, Long) -> Unit, private var doneReceiver: (Long, Boolean) -> Unit) : Thread() {
// private val query: DownloadManager.Query = DownloadManager.Query()
// private var totalBytes: Int = 0
// private var TAG = "DownloadProgressUpdater"
//
// init {
// query.setFilterById(this.downloadId)
// }
//
// override fun run() {
// Log.d(TAG, "RUN FOR ID $downloadId")
// var keepRunning = true
// var increment = 0
// while (keepRunning) {
// Thread.sleep(500)
// increment++
//
// if (increment % 4 == 0) {
// Log.d(TAG, "Loop $increment : $downloadId")
// }
//
// manager.query(query).use {
// if (it.moveToFirst()) {
// //get total bytes of the file
// if (totalBytes <= 0) {
// totalBytes = it.getInt(it.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES))
// if (totalBytes <= 0) {
// Log.e(TAG, "Download Is 0 Bytes $downloadId")
// doneReceiver(downloadId, false)
// keepRunning = false
// this.interrupt()
// return
// }
// }
//
// val downloadStatus = it.getInt(it.getColumnIndex(DownloadManager.COLUMN_STATUS))
// val bytesDownloadedSoFar = it.getInt(it.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR))
//
// if (increment % 4 == 0) {
// Log.d(TAG, "BYTES $increment : $downloadId : $bytesDownloadedSoFar : TOTAL: $totalBytes")
// }
//
// if (downloadStatus == DownloadManager.STATUS_SUCCESSFUL || downloadStatus == DownloadManager.STATUS_FAILED) {
// if (downloadStatus == DownloadManager.STATUS_SUCCESSFUL) {
// doneReceiver(downloadId, true)
// } else {
// doneReceiver(downloadId, false)
// }
// keepRunning = false
// this.interrupt()
// } else {
// //update progress
// val percentProgress = ((bytesDownloadedSoFar * 100L) / totalBytes)
// receiver(downloadId, percentProgress)
// }
// } else {
// Log.e(TAG, "NOT FOUND IN QUERY")
// keepRunning = false
// }
// }
// }
// }
// }
//}

View file

@ -1,8 +1,8 @@
<template> <template>
<div id="bookshelf" class="w-full max-w-full h-full"> <div id="bookshelf" class="w-full max-w-full h-full">
<template v-for="shelf in totalShelves"> <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 :key="shelf" class="w-full px-2 relative" :class="showBookshelfListView ? '' : 'bookshelfRow'" :id="`shelf-${shelf - 1}`" :style="{ height: shelfHeight + 'px' }">
<div class="bookshelfDivider w-full absolute bottom-0 left-0 z-30" style="min-height: 16px" :class="`h-${shelfDividerHeightIndex}`" /> <div v-if="!showBookshelfListView" class="bookshelfDivider w-full absolute bottom-0 left-0 z-30" style="min-height: 16px" :class="`h-${shelfDividerHeightIndex}`" />
</div> </div>
</template> </template>
@ -28,9 +28,8 @@ export default {
bookshelfWidth: 0, bookshelfWidth: 0,
bookshelfMarginLeft: 0, bookshelfMarginLeft: 0,
shelvesPerPage: 0, shelvesPerPage: 0,
entitiesPerShelf: 8, entitiesPerShelf: 2,
currentPage: 0, currentPage: 0,
currentBookWidth: 0,
booksPerFetch: 20, booksPerFetch: 20,
initialized: false, initialized: false,
currentSFQueryString: null, currentSFQueryString: null,
@ -46,6 +45,11 @@ export default {
localLibraryItems: [] localLibraryItems: []
} }
}, },
watch: {
showBookshelfListView(newVal) {
this.resetEntities()
}
},
computed: { computed: {
user() { user() {
return this.$store.state.user.user return this.$store.state.user.user
@ -57,6 +61,12 @@ export default {
if (this.isBookEntity) return 4 if (this.isBookEntity) return 4
return 6 return 6
}, },
bookshelfListView() {
return this.$store.state.globals.bookshelfListView
},
showBookshelfListView() {
return this.isBookEntity && this.bookshelfListView
},
entityName() { entityName() {
return this.page return this.page
}, },
@ -93,10 +103,12 @@ export default {
return this.bookWidth * 1.6 return this.bookWidth * 1.6
}, },
entityWidth() { entityWidth() {
if (this.showBookshelfListView) return this.bookshelfWidth - 16
if (this.isBookEntity) return this.bookWidth if (this.isBookEntity) return this.bookWidth
return this.bookWidth * 2 return this.bookWidth * 2
}, },
entityHeight() { entityHeight() {
if (this.showBookshelfListView) return 88
return this.bookHeight return this.bookHeight
}, },
currentLibraryId() { currentLibraryId() {
@ -106,9 +118,11 @@ export default {
return this.$store.getters['libraries/getCurrentLibraryMediaType'] return this.$store.getters['libraries/getCurrentLibraryMediaType']
}, },
shelfHeight() { shelfHeight() {
if (this.showBookshelfListView) return this.entityHeight
return this.entityHeight + 40 return this.entityHeight + 40
}, },
totalEntityCardWidth() { totalEntityCardWidth() {
if (this.showBookshelfListView) return this.entityWidth
// Includes margin // Includes margin
return this.entityWidth + 24 return this.entityWidth + 24
} }
@ -154,16 +168,6 @@ export default {
} }
for (let i = 0; i < payload.results.length; i++) { 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 var index = i + startIndex
this.entities[index] = payload.results[i] this.entities[index] = payload.results[i]
if (this.entityComponentRefs[index]) { if (this.entityComponentRefs[index]) {
@ -294,12 +298,11 @@ export default {
var { clientHeight, clientWidth } = bookshelf var { clientHeight, clientWidth } = bookshelf
this.bookshelfHeight = clientHeight this.bookshelfHeight = clientHeight
this.bookshelfWidth = clientWidth this.bookshelfWidth = clientWidth
this.entitiesPerShelf = Math.floor((this.bookshelfWidth - 16) / this.totalEntityCardWidth) this.entitiesPerShelf = this.showBookshelfListView ? 1 : Math.floor((this.bookshelfWidth - 16) / this.totalEntityCardWidth)
this.shelvesPerPage = Math.ceil(this.bookshelfHeight / this.shelfHeight) + 2 this.shelvesPerPage = Math.ceil(this.bookshelfHeight / this.shelfHeight) + 2
this.bookshelfMarginLeft = (this.bookshelfWidth - this.entitiesPerShelf * this.totalEntityCardWidth) / 2 this.bookshelfMarginLeft = (this.bookshelfWidth - this.entitiesPerShelf * this.totalEntityCardWidth) / 2
this.currentBookWidth = this.bookWidth
if (this.totalEntities) { if (this.totalEntities) {
this.totalShelves = Math.ceil(this.totalEntities / this.entitiesPerShelf) this.totalShelves = Math.ceil(this.totalEntities / this.entitiesPerShelf)
} }

View file

@ -0,0 +1,428 @@
<template>
<div ref="card" :id="`book-card-${index}`" :style="{ minWidth: width + 'px', maxWidth: width + 'px', height: height + 'px' }" class="rounded-sm z-10 cursor-pointer py-1" @click="clickCard">
<div class="h-full flex">
<div class="w-20 h-20 relative" style="min-width: 80px; max-width: 80px">
<!-- 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">
<img v-show="libraryItem" ref="cover" :src="bookCoverSrc" class="w-full h-full transition-opacity duration-300" :class="showCoverBg ? 'object-contain' : 'object-fill'" @load="imageLoaded" :style="{ opacity: imageReady ? 1 : 0 }" />
</div>
<!-- No progress shown for collapsed series in library -->
<div class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b" :class="itemIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: width * userProgressPercent + 'px' }"></div>
<div v-if="localLibraryItem || isLocal" class="absolute top-0 right-0 z-20" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }">
<span class="material-icons text-2xl text-success">{{ isLocalOnly ? 'task' : 'download_done' }}</span>
</div>
</div>
<div class="flex-grow px-2">
<p class="whitespace-normal" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">
{{ displayTitle }}
</p>
<p class="truncate text-gray-400" :style="{ fontSize: 0.7 * sizeMultiplier + 'rem' }">{{ displayAuthor }}</p>
<p v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.7 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p>
</div>
</div>
</div>
</template>
<script>
import { Capacitor } from '@capacitor/core'
export default {
props: {
index: Number,
width: {
type: Number,
default: 120
},
height: {
type: Number,
default: 192
},
bookCoverAspectRatio: Number,
showVolumeNumber: Boolean,
bookshelfView: Number,
bookMount: {
// Book can be passed as prop or set with setEntity()
type: Object,
default: () => null
},
orderBy: String,
filterBy: String,
sortingIgnorePrefix: Boolean
},
data() {
return {
isProcessingReadUpdate: false,
libraryItem: null,
imageReady: false,
rescanning: false,
selected: false,
isSelectionMode: false,
showCoverBg: false,
localLibraryItem: null
}
},
watch: {
bookMount: {
handler(newVal) {
if (newVal) {
this.libraryItem = newVal
}
}
}
},
computed: {
showExperimentalFeatures() {
return this.store.state.showExperimentalFeatures
},
_libraryItem() {
return this.libraryItem || {}
},
isLocal() {
return !!this._libraryItem.isLocal
},
isLocalOnly() {
// Local item with no server match
return this.isLocal && !this._libraryItem.libraryItemId
},
media() {
return this._libraryItem.media || {}
},
mediaMetadata() {
return this.media.metadata || {}
},
placeholderUrl() {
return '/book_placeholder.jpg'
},
bookCoverSrc() {
if (this.isLocal) {
if (this.libraryItem.coverContentUrl) return Capacitor.convertFileSrc(this.libraryItem.coverContentUrl)
return this.placeholderUrl
}
return this.store.getters['globals/getLibraryItemCoverSrc'](this._libraryItem, this.placeholderUrl)
},
libraryItemId() {
return this._libraryItem.id
},
series() {
return this.mediaMetadata.series
},
libraryId() {
return this._libraryItem.libraryId
},
hasEbook() {
return this.media.ebookFile
},
numTracks() {
return this.media.numTracks
},
processingBatch() {
return this.store.state.processingBatch
},
booksInSeries() {
// Only added to audiobook object when collapseSeries is enabled
return this._libraryItem.booksInSeries
},
hasCover() {
return !!this.media.coverPath
},
squareAspectRatio() {
return this.bookCoverAspectRatio === 1
},
sizeMultiplier() {
return this.width / 364
},
title() {
return this.mediaMetadata.title || ''
},
playIconFontSize() {
return Math.max(2, 3 * this.sizeMultiplier)
},
author() {
return this.mediaMetadata.authorName || ''
},
authorLF() {
return this.mediaMetadata.authorNameLF || ''
},
volumeNumber() {
return this.mediaMetadata.volumeNumber || null
},
displayTitle() {
if (this.orderBy === 'media.metadata.title' && this.sortingIgnorePrefix && this.title.toLowerCase().startsWith('the ')) {
return this.title.substr(4) + ', The'
}
return this.title
},
displayAuthor() {
if (this.orderBy === 'media.metadata.authorNameLF') return this.authorLF
return this.author
},
displaySortLine() {
if (this.orderBy === 'mtimeMs') return 'Modified ' + this.$formatDate(this._libraryItem.mtimeMs)
if (this.orderBy === 'birthtimeMs') return 'Born ' + this.$formatDate(this._libraryItem.birthtimeMs)
if (this.orderBy === 'addedAt') return 'Added ' + this.$formatDate(this._libraryItem.addedAt)
if (this.orderBy === 'duration') return 'Duration: ' + this.$elapsedPrettyExtended(this.media.duration, false)
if (this.orderBy === 'size') return 'Size: ' + this.$bytesPretty(this._libraryItem.size)
return null
},
userProgress() {
return this.store.getters['user/getUserLibraryItemProgress'](this.libraryItemId)
},
userProgressPercent() {
return this.userProgress ? this.userProgress.progress || 0 : 0
},
itemIsFinished() {
return this.userProgress ? !!this.userProgress.isFinished : false
},
showError() {
return this.hasMissingParts || this.hasInvalidParts || this.isMissing || this.isInvalid
},
isStreaming() {
return this.store.getters['getlibraryItemIdStreaming'] === this.libraryItemId
},
showReadButton() {
return !this.isSelectionMode && this.showExperimentalFeatures && !this.showPlayButton && this.hasEbook
},
showPlayButton() {
return !this.isSelectionMode && !this.isMissing && !this.isInvalid && this.numTracks && !this.isStreaming
},
showSmallEBookIcon() {
return !this.isSelectionMode && this.showExperimentalFeatures && this.hasEbook
},
isMissing() {
return this._libraryItem.isMissing
},
isInvalid() {
return this._libraryItem.isInvalid
},
hasMissingParts() {
return this._libraryItem.hasMissingParts
},
hasInvalidParts() {
return this._libraryItem.hasInvalidParts
},
errorText() {
if (this.isMissing) return 'Item directory is missing!'
else if (this.isInvalid) return 'Item has no media files'
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'
},
overlayWrapperClasslist() {
var classes = []
if (this.isSelectionMode) classes.push('bg-opacity-60')
else classes.push('bg-opacity-40')
if (this.selected) {
classes.push('border-2 border-yellow-400')
}
return classes
},
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.author) return ''
if (this.author.length > 30) {
return this.author.slice(0, 27) + '...'
}
return this.author
},
isAlternativeBookshelfView() {
return false
// var constants = this.$constants || this.$nuxt.$constants
// return this.bookshelfView === constants.BookshelfView.TITLES
},
titleDisplayBottomOffset() {
if (!this.isAlternativeBookshelfView) return 0
else if (!this.displaySortLine) return 3 * this.sizeMultiplier
return 4.25 * this.sizeMultiplier
}
},
methods: {
setSelectionMode(val) {
this.isSelectionMode = val
if (!val) this.selected = false
},
setEntity(libraryItem) {
this.libraryItem = libraryItem
},
setLocalLibraryItem(localLibraryItem) {
// Server books may have a local library item
this.localLibraryItem = localLibraryItem
},
clickCard(e) {
if (this.isSelectionMode) {
e.stopPropagation()
e.preventDefault()
this.selectBtnClick()
} else {
var router = this.$router || this.$nuxt.$router
if (router) {
if (this.booksInSeries) router.push(`/library/${this.libraryId}/series/${this.$encode(this.series)}`)
else router.push(`/item/${this.libraryItemId}`)
}
}
},
editClick() {
this.$emit('edit', this.libraryItem)
},
toggleFinished() {
var updatePayload = {
isFinished: !this.itemIsFinished
}
this.isProcessingReadUpdate = true
var toast = this.$toast || this.$nuxt.$toast
var axios = this.$axios || this.$nuxt.$axios
axios
.$patch(`/api/me/progress/${this.libraryItemId}`, updatePayload)
.then(() => {
this.isProcessingReadUpdate = false
toast.success(`Item marked as ${updatePayload.isFinished ? 'Finished' : 'Not Finished'}`)
})
.catch((error) => {
console.error('Failed', error)
this.isProcessingReadUpdate = false
toast.error(`Failed to mark as ${updatePayload.isFinished ? 'Finished' : 'Not Finished'}`)
})
},
rescan() {
this.rescanning = true
this.$axios
.$get(`/api/items/${this.libraryItemId}/scan`)
.then((data) => {
this.rescanning = false
var result = data.result
if (!result) {
this.$toast.error(`Re-Scan Failed for "${this.title}"`)
} else if (result === 'UPDATED') {
this.$toast.success(`Re-Scan complete item was updated`)
} else if (result === 'UPTODATE') {
this.$toast.success(`Re-Scan complete item was up to date`)
} else if (result === 'REMOVED') {
this.$toast.error(`Re-Scan complete item was removed`)
}
})
.catch((error) => {
console.error('Failed to scan library item', error)
this.$toast.error('Failed to scan library item')
this.rescanning = false
})
},
showEditModalTracks() {
// More menu func
this.store.commit('showEditModalOnTab', { libraryItem: this.libraryItem, tab: 'tracks' })
},
showEditModalMatch() {
// More menu func
this.store.commit('showEditModalOnTab', { libraryItem: this.libraryItem, tab: 'match' })
},
showEditModalDownload() {
// More menu func
this.store.commit('showEditModalOnTab', { libraryItem: this.libraryItem, tab: 'download' })
},
openCollections() {
this.store.commit('setSelectedLibraryItem', this.libraryItem)
this.store.commit('globals/setShowUserCollectionsModal', true)
},
clickReadEBook() {
this.store.commit('showEReader', this.libraryItem)
},
selectBtnClick() {
if (this.processingBatch) return
this.selected = !this.selected
this.$emit('select', this.libraryItem)
},
play() {
var eventBus = this.$eventBus || this.$nuxt.$eventBus
eventBus.$emit('play-item', this.libraryItemId)
},
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)
if (this.bookMount.localLibraryItem) {
this.setLocalLibraryItem(this.bookMount.localLibraryItem)
}
}
}
}
</script>

View file

@ -9,6 +9,7 @@
<p v-show="selectedSeriesName" class="ml-2 font-book pt-1">{{ selectedSeriesName }} ({{ totalEntities }})</p> <p v-show="selectedSeriesName" class="ml-2 font-book pt-1">{{ selectedSeriesName }} ({{ totalEntities }})</p>
<div class="flex-grow" /> <div class="flex-grow" />
<template v-if="page === 'library'"> <template v-if="page === 'library'">
<span class="material-icons px-2" @click="bookshelfListView = !bookshelfListView">{{ bookshelfListView ? 'view_list' : 'grid_view' }}</span>
<div class="relative flex items-center px-2"> <div class="relative flex items-center px-2">
<span class="material-icons" @click="showFilterModal = true">filter_alt</span> <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 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" />
@ -34,6 +35,14 @@ export default {
} }
}, },
computed: { computed: {
bookshelfListView: {
get() {
return this.$store.state.globals.bookshelfListView
},
set(val) {
this.$store.commit('globals/setBookshelfListView', val)
}
},
hasFilters() { hasFilters() {
return this.$store.getters['user/getUserSetting']('mobileFilterBy') !== 'all' return this.$store.getters['user/getUserSetting']('mobileFilterBy') !== 'all'
}, },

View file

@ -29,35 +29,23 @@ export default {
items: [ items: [
{ {
text: 'Title', text: 'Title',
value: 'book.title' value: 'media.metadata.title'
}, },
{ {
text: 'Author (First Last)', text: 'Author (First Last)',
value: 'book.authorFL' value: 'media.metadata.authorName'
}, },
{ {
text: 'Author (Last, First)', text: 'Author (Last, First)',
value: 'book.authorLF' value: 'media.metadata.authorNameLF'
}, },
{ {
text: 'Added At', text: 'Added At',
value: 'addedAt' value: 'addedAt'
}, },
{
text: 'Volume #',
value: 'book.volumeNumber'
},
{
text: 'Duration',
value: 'duration'
},
{ {
text: 'Size', text: 'Size',
value: 'size' value: 'size'
},
{
text: 'Last Read',
value: 'recent'
} }
] ]
} }

View file

@ -1,5 +1,6 @@
import Vue from 'vue' import Vue from 'vue'
import LazyBookCard from '@/components/cards/LazyBookCard' import LazyBookCard from '@/components/cards/LazyBookCard'
import LazyListBookCard from '@/components/cards/LazyListBookCard'
import LazySeriesCard from '@/components/cards/LazySeriesCard' import LazySeriesCard from '@/components/cards/LazySeriesCard'
import LazyCollectionCard from '@/components/cards/LazyCollectionCard' import LazyCollectionCard from '@/components/cards/LazyCollectionCard'
@ -15,6 +16,7 @@ export default {
getComponentClass() { getComponentClass() {
if (this.entityName === 'series') return Vue.extend(LazySeriesCard) if (this.entityName === 'series') return Vue.extend(LazySeriesCard)
if (this.entityName === 'collections') return Vue.extend(LazyCollectionCard) if (this.entityName === 'collections') return Vue.extend(LazyCollectionCard)
if (this.showBookshelfListView) return Vue.extend(LazyListBookCard)
return Vue.extend(LazyBookCard) return Vue.extend(LazyBookCard)
}, },
async mountEntityCard(index) { async mountEntityCard(index) {
@ -32,10 +34,10 @@ export default {
bookComponent.isHovering = false bookComponent.isHovering = false
return return
} }
var shelfOffsetY = this.isBookEntity ? 24 : 16 var shelfOffsetY = this.showBookshelfListView ? 8 : this.isBookEntity ? 24 : 16
var row = index % this.entitiesPerShelf var row = index % this.entitiesPerShelf
var marginShiftLeft = 12 var marginShiftLeft = this.showBookshelfListView ? 0 : 12
var shelfOffsetX = row * this.totalEntityCardWidth + this.bookshelfMarginLeft + marginShiftLeft var shelfOffsetX = row * this.totalEntityCardWidth + this.bookshelfMarginLeft + marginShiftLeft
var ComponentClass = this.getComponentClass() var ComponentClass = this.getComponentClass()

View file

@ -89,8 +89,7 @@ export default {
return categories return categories
}, },
async fetchCategories(from = null) { async fetchCategories() {
console.log('[4breadcrumbs] fetchCategories', from)
if (this.loading) { if (this.loading) {
console.log('Already loading categories') console.log('Already loading categories')
return return
@ -130,7 +129,7 @@ export default {
}, },
async libraryChanged(libid) { async libraryChanged(libid) {
if (this.currentLibraryId) { if (this.currentLibraryId) {
await this.fetchCategories('libraryChanged') await this.fetchCategories()
} }
}, },
audiobookAdded(audiobook) { audiobookAdded(audiobook) {
@ -188,7 +187,7 @@ export default {
}, },
mounted() { mounted() {
this.initListeners() this.initListeners()
this.fetchCategories('mounted') this.fetchCategories()
// if (this.$server.initialized && this.currentLibraryId) { // if (this.$server.initialized && this.currentLibraryId) {
// this.fetchCategories() // this.fetchCategories()
// } else { // } else {

View file

@ -1,5 +1,6 @@
export const state = () => ({ export const state = () => ({
itemDownloads: [] itemDownloads: [],
bookshelfListView: false
}) })
export const getters = { export const getters = {
@ -41,5 +42,8 @@ export const mutations = {
}, },
removeItemDownload(state, id) { removeItemDownload(state, id) {
state.itemDownloads = state.itemDownloads.filter(i => i.id != id) state.itemDownloads = state.itemDownloads.filter(i => i.id != id)
},
setBookshelfListView(state, val) {
state.bookshelfListView = val
} }
} }