mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2025-06-30 00:54:39 +02:00
Add download UI indicator, download progress, update bookshelf item to show local items and items matches with local item, remove item before downloading if already exists in file system
This commit is contained in:
parent
ee942c6704
commit
119bfd6c98
18 changed files with 520 additions and 80 deletions
|
@ -15,11 +15,11 @@ class DbManager {
|
||||||
Paper.book("device").write("data", deviceData)
|
Paper.book("device").write("data", deviceData)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getLocalLibraryItems():MutableList<LocalLibraryItem> {
|
fun getLocalLibraryItems(mediaType:String? = null):MutableList<LocalLibraryItem> {
|
||||||
var localLibraryItems:MutableList<LocalLibraryItem> = mutableListOf()
|
var localLibraryItems:MutableList<LocalLibraryItem> = mutableListOf()
|
||||||
Paper.book("localLibraryItems").allKeys.forEach {
|
Paper.book("localLibraryItems").allKeys.forEach {
|
||||||
var localLibraryItem:LocalLibraryItem? = Paper.book("localLibraryItems").read(it)
|
var localLibraryItem:LocalLibraryItem? = Paper.book("localLibraryItems").read(it)
|
||||||
if (localLibraryItem != null) {
|
if (localLibraryItem != null && (mediaType.isNullOrEmpty() || mediaType == localLibraryItem?.mediaType)) {
|
||||||
// TODO: Check to make sure all file paths exist
|
// TODO: Check to make sure all file paths exist
|
||||||
// if (localMediaItem.coverContentUrl != null) {
|
// if (localMediaItem.coverContentUrl != null) {
|
||||||
// var file = DocumentFile.fromSingleUri(ctx)
|
// var file = DocumentFile.fromSingleUri(ctx)
|
||||||
|
@ -44,6 +44,10 @@ class DbManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getLocalLibraryItemByLLId(libraryItemId:String):LocalLibraryItem? {
|
||||||
|
return getLocalLibraryItems().find { it.libraryItemId == libraryItemId }
|
||||||
|
}
|
||||||
|
|
||||||
fun getLocalLibraryItem(localLibraryItemId:String):LocalLibraryItem? {
|
fun getLocalLibraryItem(localLibraryItemId:String):LocalLibraryItem? {
|
||||||
return Paper.book("localLibraryItems").read(localLibraryItemId)
|
return Paper.book("localLibraryItems").read(localLibraryItemId)
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,6 +50,7 @@ class AbsDatabase : Plugin() {
|
||||||
@PluginMethod
|
@PluginMethod
|
||||||
fun getLocalLibraryItem(call:PluginCall) {
|
fun getLocalLibraryItem(call:PluginCall) {
|
||||||
var id = call.getString("id", "").toString()
|
var id = call.getString("id", "").toString()
|
||||||
|
|
||||||
GlobalScope.launch(Dispatchers.IO) {
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
var localLibraryItem = DeviceManager.dbManager.getLocalLibraryItem(id)
|
var localLibraryItem = DeviceManager.dbManager.getLocalLibraryItem(id)
|
||||||
if (localLibraryItem == null) {
|
if (localLibraryItem == null) {
|
||||||
|
@ -61,9 +62,24 @@ class AbsDatabase : Plugin() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@PluginMethod
|
@PluginMethod
|
||||||
fun getLocalLibraryItems(call:PluginCall) {
|
fun getLocalLibraryItemByLLId(call:PluginCall) {
|
||||||
|
var libraryItemId = call.getString("libraryItemId", "").toString()
|
||||||
GlobalScope.launch(Dispatchers.IO) {
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
var localLibraryItems = DeviceManager.dbManager.getLocalLibraryItems()
|
var localLibraryItem = DeviceManager.dbManager.getLocalLibraryItemByLLId(libraryItemId)
|
||||||
|
if (localLibraryItem == null) {
|
||||||
|
call.resolve()
|
||||||
|
} else {
|
||||||
|
call.resolve(JSObject(jacksonObjectMapper().writeValueAsString(localLibraryItem)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PluginMethod
|
||||||
|
fun getLocalLibraryItems(call:PluginCall) {
|
||||||
|
var mediaType = call.getString("mediaType", "").toString()
|
||||||
|
|
||||||
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
|
var localLibraryItems = DeviceManager.dbManager.getLocalLibraryItems(mediaType)
|
||||||
var jsobj = JSObject()
|
var jsobj = JSObject()
|
||||||
jsobj.put("localLibraryItems", jacksonObjectMapper().writeValueAsString(localLibraryItems))
|
jsobj.put("localLibraryItems", jacksonObjectMapper().writeValueAsString(localLibraryItems))
|
||||||
call.resolve(jsobj)
|
call.resolve(jsobj)
|
||||||
|
|
|
@ -144,13 +144,18 @@ class AbsDownloader : Plugin() {
|
||||||
var itemFolderPath = localFolder.absolutePath + "/" + bookTitle
|
var itemFolderPath = localFolder.absolutePath + "/" + bookTitle
|
||||||
var downloadItem = DownloadItem(libraryItem.id, libraryItem.mediaType, itemFolderPath, localFolder, bookTitle, libraryItem.media, mutableListOf())
|
var downloadItem = DownloadItem(libraryItem.id, libraryItem.mediaType, itemFolderPath, localFolder, bookTitle, libraryItem.media, mutableListOf())
|
||||||
|
|
||||||
|
|
||||||
// Create download item part for each audio track
|
// Create download item part for each audio track
|
||||||
tracks.forEach { audioTrack ->
|
tracks.forEach { audioTrack ->
|
||||||
var serverPath = "/s/item/${libraryItem.id}/${cleanRelPath(audioTrack.relPath)}"
|
var serverPath = "/s/item/${libraryItem.id}/${cleanRelPath(audioTrack.relPath)}"
|
||||||
var destinationFilename = getFilenameFromRelPath(audioTrack.relPath)
|
var destinationFilename = getFilenameFromRelPath(audioTrack.relPath)
|
||||||
Log.d(tag, "Audio File Server Path $serverPath | AF RelPath ${audioTrack.relPath} | LocalFolder Path ${localFolder.absolutePath} | DestName ${destinationFilename}")
|
Log.d(tag, "Audio File Server Path $serverPath | AF RelPath ${audioTrack.relPath} | LocalFolder Path ${localFolder.absolutePath} | DestName ${destinationFilename}")
|
||||||
var destinationFile = File("$itemFolderPath/$destinationFilename")
|
var destinationFile = File("$itemFolderPath/$destinationFilename")
|
||||||
|
|
||||||
|
if (destinationFile.exists()) {
|
||||||
|
Log.d(tag, "Audio file already exists, removing it from ${destinationFile.absolutePath}")
|
||||||
|
destinationFile.delete()
|
||||||
|
}
|
||||||
|
|
||||||
var destinationUri = Uri.fromFile(destinationFile)
|
var destinationUri = Uri.fromFile(destinationFile)
|
||||||
var downloadUri = Uri.parse("${DeviceManager.serverAddress}${serverPath}?token=${DeviceManager.token}")
|
var downloadUri = Uri.parse("${DeviceManager.serverAddress}${serverPath}?token=${DeviceManager.token}")
|
||||||
Log.d(tag, "Audio File Destination Uri $destinationUri | Download URI $downloadUri")
|
Log.d(tag, "Audio File Destination Uri $destinationUri | Download URI $downloadUri")
|
||||||
|
@ -169,6 +174,12 @@ class AbsDownloader : Plugin() {
|
||||||
var serverPath = "/api/items/${libraryItem.id}/cover?format=jpeg"
|
var serverPath = "/api/items/${libraryItem.id}/cover?format=jpeg"
|
||||||
var destinationFilename = "cover.jpg"
|
var destinationFilename = "cover.jpg"
|
||||||
var destinationFile = File("$itemFolderPath/$destinationFilename")
|
var destinationFile = File("$itemFolderPath/$destinationFilename")
|
||||||
|
|
||||||
|
if (destinationFile.exists()) {
|
||||||
|
Log.d(tag, "Cover already exists, removing it from ${destinationFile.absolutePath}")
|
||||||
|
destinationFile.delete()
|
||||||
|
}
|
||||||
|
|
||||||
var destinationUri = Uri.fromFile(destinationFile)
|
var destinationUri = Uri.fromFile(destinationFile)
|
||||||
var downloadUri = Uri.parse("${DeviceManager.serverAddress}${serverPath}&token=${DeviceManager.token}")
|
var downloadUri = Uri.parse("${DeviceManager.serverAddress}${serverPath}&token=${DeviceManager.token}")
|
||||||
var downloadItemPart = DownloadItemPart(DeviceManager.getBase64Id(destinationFile.absolutePath), destinationFilename, destinationFile.absolutePath, bookTitle, serverPath, localFolder.name, localFolder.id, null, false, downloadUri, destinationUri, null, 0)
|
var downloadItemPart = DownloadItemPart(DeviceManager.getBase64Id(destinationFile.absolutePath), destinationFilename, destinationFile.absolutePath, bookTitle, serverPath, localFolder.name, localFolder.id, null, false, downloadUri, destinationUri, null, 0)
|
||||||
|
@ -237,20 +248,20 @@ class AbsDownloader : Plugin() {
|
||||||
val totalBytes = it.getInt(it.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES))
|
val totalBytes = it.getInt(it.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES))
|
||||||
val downloadStatus = it.getInt(it.getColumnIndex(DownloadManager.COLUMN_STATUS))
|
val downloadStatus = it.getInt(it.getColumnIndex(DownloadManager.COLUMN_STATUS))
|
||||||
val bytesDownloadedSoFar = it.getInt(it.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR))
|
val bytesDownloadedSoFar = it.getInt(it.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR))
|
||||||
Log.d(tag, "Download ${downloadItemPart.filename} bytes $totalBytes | bytes dled $bytesDownloadedSoFar | downloadStatus $downloadStatus")
|
Log.d(tag, "checkDownloads Download ${downloadItemPart.filename} bytes $totalBytes | bytes dled $bytesDownloadedSoFar | downloadStatus $downloadStatus")
|
||||||
|
|
||||||
if (downloadStatus == DownloadManager.STATUS_SUCCESSFUL) {
|
if (downloadStatus == DownloadManager.STATUS_SUCCESSFUL) {
|
||||||
Log.d(tag, "Download ${downloadItemPart.filename} Done")
|
Log.d(tag, "checkDownloads Download ${downloadItemPart.filename} Done")
|
||||||
// downloadItem.downloadItemParts.remove(downloadItemPart)
|
// downloadItem.downloadItemParts.remove(downloadItemPart)
|
||||||
downloadItemPart.completed = true
|
downloadItemPart.completed = true
|
||||||
} else if (downloadStatus == DownloadManager.STATUS_FAILED) {
|
} else if (downloadStatus == DownloadManager.STATUS_FAILED) {
|
||||||
Log.d(tag, "Download ${downloadItemPart.filename} Failed")
|
Log.d(tag, "checkDownloads Download ${downloadItemPart.filename} Failed")
|
||||||
downloadItem.downloadItemParts.remove(downloadItemPart)
|
downloadItem.downloadItemParts.remove(downloadItemPart)
|
||||||
// downloadItemPart.completed = true
|
// downloadItemPart.completed = true
|
||||||
} else {
|
} else {
|
||||||
//update progress
|
//update progress
|
||||||
val percentProgress = if (totalBytes > 0) ((bytesDownloadedSoFar * 100L) / totalBytes) else 0
|
val percentProgress = if (totalBytes > 0) ((bytesDownloadedSoFar * 100L) / totalBytes) else 0
|
||||||
Log.d(tag, "${downloadItemPart.filename} Progress = $percentProgress%")
|
Log.d(tag, "checkDownloads Download ${downloadItemPart.filename} Progress = $percentProgress%")
|
||||||
downloadItemPart.progress = percentProgress
|
downloadItemPart.progress = percentProgress
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
<a v-if="showBack" @click="back" class="rounded-full h-10 w-10 flex items-center justify-center hover:bg-white hover:bg-opacity-10 mr-2 cursor-pointer">
|
<a v-if="showBack" @click="back" class="rounded-full h-10 w-10 flex items-center justify-center hover:bg-white hover:bg-opacity-10 mr-2 cursor-pointer">
|
||||||
<span class="material-icons text-3xl text-white">arrow_back</span>
|
<span class="material-icons text-3xl text-white">arrow_back</span>
|
||||||
</a>
|
</a>
|
||||||
<div v-if="socketConnected">
|
<div v-if="user">
|
||||||
<div class="px-4 py-2 bg-bg bg-opacity-30 rounded-md flex items-center" @click="clickShowLibraryModal">
|
<div class="px-4 py-2 bg-bg bg-opacity-30 rounded-md flex items-center" @click="clickShowLibraryModal">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" />
|
||||||
|
@ -17,6 +17,8 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
|
|
||||||
|
<widgets-download-progress-indicator />
|
||||||
|
|
||||||
<nuxt-link class="h-7 mx-2" to="/search">
|
<nuxt-link class="h-7 mx-2" to="/search">
|
||||||
<span class="material-icons" style="font-size: 1.75rem">search</span>
|
<span class="material-icons" style="font-size: 1.75rem">search</span>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
|
@ -42,7 +42,8 @@ export default {
|
||||||
entityIndexesMounted: [],
|
entityIndexesMounted: [],
|
||||||
pagesLoaded: {},
|
pagesLoaded: {},
|
||||||
isFirstInit: false,
|
isFirstInit: false,
|
||||||
pendingReset: false
|
pendingReset: false,
|
||||||
|
localLibraryItems: []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -101,6 +102,9 @@ export default {
|
||||||
currentLibraryId() {
|
currentLibraryId() {
|
||||||
return this.$store.state.libraries.currentLibraryId
|
return this.$store.state.libraries.currentLibraryId
|
||||||
},
|
},
|
||||||
|
currentLibraryMediaType() {
|
||||||
|
return this.$store.getters['libraries/getCurrentLibraryMediaType']
|
||||||
|
},
|
||||||
shelfHeight() {
|
shelfHeight() {
|
||||||
return this.entityHeight + 40
|
return this.entityHeight + 40
|
||||||
},
|
},
|
||||||
|
@ -164,6 +168,13 @@ export default {
|
||||||
this.entities[index] = payload.results[i]
|
this.entities[index] = payload.results[i]
|
||||||
if (this.entityComponentRefs[index]) {
|
if (this.entityComponentRefs[index]) {
|
||||||
this.entityComponentRefs[index].setEntity(this.entities[index])
|
this.entityComponentRefs[index].setEntity(this.entities[index])
|
||||||
|
|
||||||
|
if (this.isBookEntity) {
|
||||||
|
var localLibraryItem = this.localLibraryItems.find((lli) => lli.libraryItemId == this.entities[index].id)
|
||||||
|
if (localLibraryItem) {
|
||||||
|
this.entityComponentRefs[index].setLocalLibraryItem(localLibraryItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -204,6 +215,7 @@ export default {
|
||||||
this.loadPage(lastBookPage)
|
this.loadPage(lastBookPage)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove entities out of view
|
||||||
this.entityIndexesMounted = this.entityIndexesMounted.filter((_index) => {
|
this.entityIndexesMounted = this.entityIndexesMounted.filter((_index) => {
|
||||||
if (_index < firstBookIndex || _index >= lastBookIndex) {
|
if (_index < firstBookIndex || _index >= lastBookIndex) {
|
||||||
var el = document.getElementById(`book-card-${_index}`)
|
var el = document.getElementById(`book-card-${_index}`)
|
||||||
|
@ -295,6 +307,10 @@ export default {
|
||||||
},
|
},
|
||||||
async init() {
|
async init() {
|
||||||
if (this.isFirstInit) return
|
if (this.isFirstInit) return
|
||||||
|
|
||||||
|
this.localLibraryItems = await this.$db.getLocalLibraryItems(this.currentLibraryMediaType)
|
||||||
|
console.log('Local library items loaded for lazy bookshelf', this.localLibraryItems.length)
|
||||||
|
|
||||||
this.isFirstInit = true
|
this.isFirstInit = true
|
||||||
this.initSizeData()
|
this.initSizeData()
|
||||||
await this.loadPage(0)
|
await this.loadPage(0)
|
||||||
|
@ -360,6 +376,13 @@ export default {
|
||||||
this.entities[indexOf] = libraryItem
|
this.entities[indexOf] = libraryItem
|
||||||
if (this.entityComponentRefs[indexOf]) {
|
if (this.entityComponentRefs[indexOf]) {
|
||||||
this.entityComponentRefs[indexOf].setEntity(libraryItem)
|
this.entityComponentRefs[indexOf].setEntity(libraryItem)
|
||||||
|
|
||||||
|
if (this.isBookEntity) {
|
||||||
|
var localLibraryItem = this.localLibraryItems.find((lli) => lli.libraryItemId == libraryItem.id)
|
||||||
|
if (localLibraryItem) {
|
||||||
|
this.entityComponentRefs[indexOf].setLocalLibraryItem(localLibraryItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,6 +37,10 @@
|
||||||
<!-- No progress shown for collapsed series in library -->
|
<!-- No progress shown for collapsed series in library -->
|
||||||
<div v-if="!booksInSeries" 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="!booksInSeries" 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>
|
||||||
|
|
||||||
<!-- Error widget -->
|
<!-- Error widget -->
|
||||||
<ui-tooltip v-if="showError" :text="errorText" class="absolute bottom-4 left-0 z-10">
|
<ui-tooltip v-if="showError" :text="errorText" class="absolute bottom-4 left-0 z-10">
|
||||||
<div :style="{ height: 1.5 * sizeMultiplier + 'rem', width: 2.5 * sizeMultiplier + 'rem' }" class="bg-error rounded-r-full shadow-md flex items-center justify-end border-r border-b border-red-300">
|
<div :style="{ height: 1.5 * sizeMultiplier + 'rem', width: 2.5 * sizeMultiplier + 'rem' }" class="bg-error rounded-r-full shadow-md flex items-center justify-end border-r border-b border-red-300">
|
||||||
|
@ -85,7 +89,8 @@ export default {
|
||||||
rescanning: false,
|
rescanning: false,
|
||||||
selected: false,
|
selected: false,
|
||||||
isSelectionMode: false,
|
isSelectionMode: false,
|
||||||
showCoverBg: false
|
showCoverBg: false,
|
||||||
|
localLibraryItem: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
@ -105,9 +110,12 @@ export default {
|
||||||
return this.libraryItem || {}
|
return this.libraryItem || {}
|
||||||
},
|
},
|
||||||
isLocal() {
|
isLocal() {
|
||||||
// Is local library item
|
|
||||||
return !!this._libraryItem.isLocal
|
return !!this._libraryItem.isLocal
|
||||||
},
|
},
|
||||||
|
isLocalOnly() {
|
||||||
|
// Local item with no server match
|
||||||
|
return this.isLocal && !this._libraryItem.libraryItemId
|
||||||
|
},
|
||||||
media() {
|
media() {
|
||||||
return this._libraryItem.media || {}
|
return this._libraryItem.media || {}
|
||||||
},
|
},
|
||||||
|
@ -119,7 +127,7 @@ export default {
|
||||||
},
|
},
|
||||||
bookCoverSrc() {
|
bookCoverSrc() {
|
||||||
if (this.isLocal) {
|
if (this.isLocal) {
|
||||||
if (this.media.coverPath) return Capacitor.convertFileSrc(this.media.coverPath)
|
if (this.libraryItem.coverContentUrl) return Capacitor.convertFileSrc(this.libraryItem.coverContentUrl)
|
||||||
return this.placeholderUrl
|
return this.placeholderUrl
|
||||||
}
|
}
|
||||||
return this.store.getters['globals/getLibraryItemCoverSrc'](this._libraryItem, this.placeholderUrl)
|
return this.store.getters['globals/getLibraryItemCoverSrc'](this._libraryItem, this.placeholderUrl)
|
||||||
|
@ -319,6 +327,10 @@ export default {
|
||||||
setEntity(libraryItem) {
|
setEntity(libraryItem) {
|
||||||
this.libraryItem = libraryItem
|
this.libraryItem = libraryItem
|
||||||
},
|
},
|
||||||
|
setLocalLibraryItem(localLibraryItem) {
|
||||||
|
// Server books may have a local library item
|
||||||
|
this.localLibraryItem = localLibraryItem
|
||||||
|
},
|
||||||
clickCard(e) {
|
clickCard(e) {
|
||||||
if (this.isSelectionMode) {
|
if (this.isSelectionMode) {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
|
@ -442,6 +454,10 @@ export default {
|
||||||
mounted() {
|
mounted() {
|
||||||
if (this.bookMount) {
|
if (this.bookMount) {
|
||||||
this.setEntity(this.bookMount)
|
this.setEntity(this.bookMount)
|
||||||
|
|
||||||
|
if (this.bookMount.localLibraryItem) {
|
||||||
|
this.setLocalLibraryItem(this.bookMount.localLibraryItem)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -66,6 +66,9 @@ export default {
|
||||||
if (!this.libraryItem) return false
|
if (!this.libraryItem) return false
|
||||||
return this.libraryItem.isLocal
|
return this.libraryItem.isLocal
|
||||||
},
|
},
|
||||||
|
localCover() {
|
||||||
|
return this.libraryItem ? this.libraryItem.coverContentUrl : null
|
||||||
|
},
|
||||||
squareAspectRatio() {
|
squareAspectRatio() {
|
||||||
return this.bookCoverAspectRatio === 1
|
return this.bookCoverAspectRatio === 1
|
||||||
},
|
},
|
||||||
|
@ -105,7 +108,7 @@ export default {
|
||||||
},
|
},
|
||||||
fullCoverUrl() {
|
fullCoverUrl() {
|
||||||
if (this.isLocal) {
|
if (this.isLocal) {
|
||||||
if (this.hasCover) return Capacitor.convertFileSrc(this.cover)
|
if (this.localCover) return Capacitor.convertFileSrc(this.localCover)
|
||||||
return this.placeholderUrl
|
return this.placeholderUrl
|
||||||
}
|
}
|
||||||
if (this.downloadCover) return this.downloadCover
|
if (this.downloadCover) return this.downloadCover
|
||||||
|
|
143
components/widgets/CircleProgress.vue
Normal file
143
components/widgets/CircleProgress.vue
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
<template>
|
||||||
|
<div ref="progressbar" class="progressbar">
|
||||||
|
<svg class="progressbar__svg">
|
||||||
|
<circle cx="20" cy="20" r="17.5" ref="circle" class="progressbar__svg-circle circle-anim"></circle>
|
||||||
|
<circle cx="20" cy="20" r="17.5" class="progressbar__svg-circlebg"></circle>
|
||||||
|
</svg>
|
||||||
|
<p class="progressbar__text text-sm text-warning">{{ count }}</p>
|
||||||
|
<!-- <span class="material-icons progressbar__text text-xl">arrow_downward</span> -->
|
||||||
|
<!-- <div class="w-4 h-4 rounded-full bg-red-600 absolute bottom-1 right-1 flex items-center justify-center transform rotate-90">
|
||||||
|
<p class="text-xs text-white">4</p>
|
||||||
|
</div> -->
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
value: Number,
|
||||||
|
count: Number
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
lastProgress: 0,
|
||||||
|
updateTimeout: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
value: {
|
||||||
|
handler(newVal, oldVal) {
|
||||||
|
this.updateProgress()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {},
|
||||||
|
methods: {
|
||||||
|
updateProgress() {
|
||||||
|
var progbar = this.$refs.progressbar
|
||||||
|
var circle = this.$refs.circle
|
||||||
|
if (!progbar || !circle) return
|
||||||
|
|
||||||
|
clearTimeout(this.updateTimeout)
|
||||||
|
var progress = Math.min(this.value || 0, 1)
|
||||||
|
|
||||||
|
progbar.style.setProperty('--progress-percent-before', this.lastProgress)
|
||||||
|
progbar.style.setProperty('--progress-percent', progress)
|
||||||
|
|
||||||
|
this.lastProgress = progress
|
||||||
|
circle.classList.remove('circle-static')
|
||||||
|
circle.classList.add('circle-anim')
|
||||||
|
this.updateTimeout = setTimeout(() => {
|
||||||
|
circle.classList.remove('circle-anim')
|
||||||
|
circle.classList.add('circle-static')
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* https://codepen.io/alvarotrigo/pen/VwMvydQ */
|
||||||
|
.progressbar {
|
||||||
|
position: relative;
|
||||||
|
width: 42.5px;
|
||||||
|
height: 42.5px;
|
||||||
|
margin: 0.25em;
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
box-sizing: border-box;
|
||||||
|
--progress-percent-before: 0;
|
||||||
|
--progress-percent: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progressbar__svg {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progressbar__svg-circlebg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
fill: none;
|
||||||
|
stroke-width: 4;
|
||||||
|
/* stroke-dasharray: 110;
|
||||||
|
stroke-dashoffset: 110; */
|
||||||
|
stroke: #fb8c0022;
|
||||||
|
stroke-linecap: round;
|
||||||
|
transform: translate(2px, 2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.progressbar__svg-circle {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
fill: none;
|
||||||
|
stroke-width: 4;
|
||||||
|
stroke-dasharray: 110;
|
||||||
|
stroke-dashoffset: 110;
|
||||||
|
/* stroke: hsl(0, 0%, 100%); */
|
||||||
|
stroke: #fb8c00;
|
||||||
|
stroke-linecap: round;
|
||||||
|
transform: translate(2px, 2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.circle-anim {
|
||||||
|
animation: anim_circle 0.5s ease-in-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.circle-static {
|
||||||
|
stroke-dashoffset: calc(110px - (110px * var(--progress-percent)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes anim_circle {
|
||||||
|
from {
|
||||||
|
stroke-dashoffset: calc(110px - (110px * var(--progress-percent-before)));
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
stroke-dashoffset: calc(110px - (110px * var(--progress-percent)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.progressbar__text {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
margin-top: 1px;
|
||||||
|
transform: translate(-50%, -50%) rotate(90deg);
|
||||||
|
animation: bounce 0.75s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bounce {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: translate(-35%, -50%) rotate(90deg);
|
||||||
|
-webkit-animation-timing-function: cubic-bezier(0.8, 0, 1, 1);
|
||||||
|
animation-timing-function: cubic-bezier(0.8, 0, 1, 1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translate(-50%, -50%) rotate(90deg);
|
||||||
|
-webkit-animation-timing-function: cubic-bezier(0, 0, 0.2, 1);
|
||||||
|
animation-timing-function: cubic-bezier(0, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
108
components/widgets/DownloadProgressIndicator.vue
Normal file
108
components/widgets/DownloadProgressIndicator.vue
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
<template>
|
||||||
|
<div v-if="numPartsRemaining > 0">
|
||||||
|
<widgets-circle-progress :value="progress" :count="numPartsRemaining" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { AbsDownloader } from '@/plugins/capacitor'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
updateListener: null,
|
||||||
|
completeListener: null,
|
||||||
|
itemDownloadingMap: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
numItemPartsComplete() {
|
||||||
|
var total = 0
|
||||||
|
Object.values(this.itemDownloadingMap).map((item) => (total += item.partsCompleted))
|
||||||
|
return total
|
||||||
|
},
|
||||||
|
numPartsRemaining() {
|
||||||
|
return this.numTotalParts - this.numItemPartsComplete
|
||||||
|
},
|
||||||
|
numTotalParts() {
|
||||||
|
var total = 0
|
||||||
|
Object.values(this.itemDownloadingMap).map((item) => (total += item.totalParts))
|
||||||
|
return total
|
||||||
|
},
|
||||||
|
progress() {
|
||||||
|
var numItems = Object.keys(this.itemDownloadingMap).length
|
||||||
|
if (!numItems) return 0
|
||||||
|
var totalProg = 0
|
||||||
|
Object.values(this.itemDownloadingMap).map((item) => (totalProg += item.itemProgress))
|
||||||
|
return totalProg / numItems
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onItemDownloadUpdate(data) {
|
||||||
|
console.log('DownloadProgressIndicator onItemDownloadUpdate', JSON.stringify(data))
|
||||||
|
if (!data || !data.downloadItemParts) {
|
||||||
|
console.error('Invalid item update payload')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var downloadItemParts = data.downloadItemParts
|
||||||
|
var partsCompleted = 0
|
||||||
|
var totalPartsProgress = 0
|
||||||
|
var partsRemaining = 0
|
||||||
|
downloadItemParts.forEach((dip) => {
|
||||||
|
if (dip.completed) {
|
||||||
|
totalPartsProgress += 1
|
||||||
|
partsCompleted++
|
||||||
|
} else {
|
||||||
|
var progPercent = dip.progress / 100
|
||||||
|
totalPartsProgress += progPercent
|
||||||
|
partsRemaining++
|
||||||
|
}
|
||||||
|
})
|
||||||
|
var itemProgress = totalPartsProgress / downloadItemParts.length
|
||||||
|
|
||||||
|
var update = {
|
||||||
|
id: data.id,
|
||||||
|
partsRemaining,
|
||||||
|
partsCompleted,
|
||||||
|
totalParts: downloadItemParts.length,
|
||||||
|
itemProgress
|
||||||
|
}
|
||||||
|
data.itemProgress = itemProgress
|
||||||
|
|
||||||
|
console.log('Saving item update download payload', JSON.stringify(update))
|
||||||
|
this.$set(this.itemDownloadingMap, update.id, update)
|
||||||
|
|
||||||
|
this.$store.commit('globals/addUpdateItemDownload', data)
|
||||||
|
},
|
||||||
|
onItemDownloadComplete(data) {
|
||||||
|
console.log('DownloadProgressIndicator onItemDownloadComplete', JSON.stringify(data))
|
||||||
|
if (!data || !data.libraryItemId) {
|
||||||
|
console.error('Invalid item downlaod complete payload')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.itemDownloadingMap[data.libraryItemId]) {
|
||||||
|
delete this.itemDownloadingMap[data.libraryItemId]
|
||||||
|
} else {
|
||||||
|
console.warn('Item download complete but not found in item downloading map', data.libraryItemId)
|
||||||
|
}
|
||||||
|
if (!data.localLibraryItem) {
|
||||||
|
this.$toast.error('Item download complete but failed to create library item')
|
||||||
|
} else {
|
||||||
|
this.$toast.success(`Item "${data.localLibraryItem.media.metadata.title}" download finished`)
|
||||||
|
this.$eventBus.$emit('new-local-library-item', data.localLibraryItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$store.commit('globals/removeItemDownload', data.libraryItemId)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.updateListener = AbsDownloader.addListener('onItemDownloadUpdate', (data) => this.onItemDownloadUpdate(data))
|
||||||
|
this.completeListener = AbsDownloader.addListener('onItemDownloadComplete', (data) => this.onItemDownloadComplete(data))
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
if (this.updateListener) this.updateListener.remove()
|
||||||
|
if (this.completeListener) this.completeListener.remove()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -13,17 +13,25 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { AppUpdate } from '@robingenz/capacitor-app-update'
|
import { AppUpdate } from '@robingenz/capacitor-app-update'
|
||||||
import { AbsFileSystem, AbsDownloader } from '@/plugins/capacitor'
|
import { AbsFileSystem } from '@/plugins/capacitor'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
return {}
|
return {
|
||||||
|
attemptingConnection: false,
|
||||||
|
inittingLibraries: false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
networkConnected: {
|
networkConnected: {
|
||||||
handler(newVal) {
|
handler(newVal, oldVal) {
|
||||||
if (newVal) {
|
if (newVal) {
|
||||||
|
console.log(`[default] network connected changed ${oldVal} -> ${newVal}`)
|
||||||
|
if (!this.user) {
|
||||||
this.attemptConnection()
|
this.attemptConnection()
|
||||||
|
} else if (!this.currentLibraryId) {
|
||||||
|
this.initLibraries()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -136,20 +144,14 @@ export default {
|
||||||
// }
|
// }
|
||||||
// })
|
// })
|
||||||
// },
|
// },
|
||||||
onItemDownloadUpdate(data) {
|
|
||||||
console.log('ON ITEM DOWNLOAD UPDATE', JSON.stringify(data))
|
|
||||||
},
|
|
||||||
onItemDownloadComplete(data) {
|
|
||||||
console.log('ON ITEM DOWNLOAD COMPLETE', JSON.stringify(data))
|
|
||||||
},
|
|
||||||
async initMediaStore() {
|
async initMediaStore() {
|
||||||
// Request and setup listeners for media files on native
|
// Request and setup listeners for media files on native
|
||||||
AbsDownloader.addListener('onItemDownloadUpdate', (data) => {
|
// AbsDownloader.addListener('onItemDownloadUpdate', (data) => {
|
||||||
this.onItemDownloadUpdate(data)
|
// this.onItemDownloadUpdate(data)
|
||||||
})
|
// })
|
||||||
AbsDownloader.addListener('onItemDownloadComplete', (data) => {
|
// AbsDownloader.addListener('onItemDownloadComplete', (data) => {
|
||||||
this.onItemDownloadComplete(data)
|
// this.onItemDownloadComplete(data)
|
||||||
})
|
// })
|
||||||
},
|
},
|
||||||
async loadSavedSettings() {
|
async loadSavedSettings() {
|
||||||
var userSavedServerSettings = await this.$localStore.getServerSettings()
|
var userSavedServerSettings = await this.$localStore.getServerSettings()
|
||||||
|
@ -167,6 +169,10 @@ export default {
|
||||||
console.warn('No network connection')
|
console.warn('No network connection')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (this.attemptingConnection) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.attemptingConnection = true
|
||||||
|
|
||||||
var deviceData = await this.$db.getDeviceData()
|
var deviceData = await this.$db.getDeviceData()
|
||||||
var serverConfig = null
|
var serverConfig = null
|
||||||
|
@ -175,16 +181,22 @@ export default {
|
||||||
}
|
}
|
||||||
if (!serverConfig) {
|
if (!serverConfig) {
|
||||||
// No last server config set
|
// No last server config set
|
||||||
|
this.attemptingConnection = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(`[default] Got server config, attempt authorize ${serverConfig.address}`)
|
||||||
|
|
||||||
var authRes = await this.$axios.$post(`${serverConfig.address}/api/authorize`, null, { headers: { Authorization: `Bearer ${serverConfig.token}` } }).catch((error) => {
|
var authRes = await this.$axios.$post(`${serverConfig.address}/api/authorize`, null, { headers: { Authorization: `Bearer ${serverConfig.token}` } }).catch((error) => {
|
||||||
console.error('[Server] Server auth failed', error)
|
console.error('[Server] Server auth failed', error)
|
||||||
var errorMsg = error.response ? error.response.data || 'Unknown Error' : 'Unknown Error'
|
var errorMsg = error.response ? error.response.data || 'Unknown Error' : 'Unknown Error'
|
||||||
this.error = errorMsg
|
this.error = errorMsg
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
if (!authRes) return
|
if (!authRes) {
|
||||||
|
this.attemptingConnection = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const { user, userDefaultLibraryId } = authRes
|
const { user, userDefaultLibraryId } = authRes
|
||||||
if (userDefaultLibraryId) {
|
if (userDefaultLibraryId) {
|
||||||
|
@ -199,6 +211,7 @@ export default {
|
||||||
|
|
||||||
console.log('Successful connection on last saved connection config', JSON.stringify(serverConnectionConfig))
|
console.log('Successful connection on last saved connection config', JSON.stringify(serverConnectionConfig))
|
||||||
await this.initLibraries()
|
await this.initLibraries()
|
||||||
|
this.attemptingConnection = false
|
||||||
},
|
},
|
||||||
itemRemoved(libraryItem) {
|
itemRemoved(libraryItem) {
|
||||||
if (this.$route.name.startsWith('item')) {
|
if (this.$route.name.startsWith('item')) {
|
||||||
|
@ -219,9 +232,15 @@ export default {
|
||||||
},
|
},
|
||||||
socketInit(data) {},
|
socketInit(data) {},
|
||||||
async initLibraries() {
|
async initLibraries() {
|
||||||
|
if (this.inittingLibraries) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.inittingLibraries = true
|
||||||
await this.$store.dispatch('libraries/load')
|
await this.$store.dispatch('libraries/load')
|
||||||
|
console.log(`[default] initLibraries loaded`)
|
||||||
this.$eventBus.$emit('library-changed')
|
this.$eventBus.$emit('library-changed')
|
||||||
this.$store.dispatch('libraries/fetch', this.currentLibraryId)
|
this.$store.dispatch('libraries/fetch', this.currentLibraryId)
|
||||||
|
this.inittingLibraries = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
|
@ -240,8 +259,10 @@ export default {
|
||||||
await this.$store.dispatch('setupNetworkListener')
|
await this.$store.dispatch('setupNetworkListener')
|
||||||
|
|
||||||
if (this.$store.state.user.serverConnectionConfig) {
|
if (this.$store.state.user.serverConnectionConfig) {
|
||||||
|
console.log(`[default] server connection config set - call init libraries`)
|
||||||
await this.initLibraries()
|
await this.initLibraries()
|
||||||
} else {
|
} else {
|
||||||
|
console.log(`[default] no server connection config - call attempt connection`)
|
||||||
await this.attemptConnection()
|
await this.attemptConnection()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -67,7 +67,15 @@ export default {
|
||||||
shelfEl.appendChild(instance.$el)
|
shelfEl.appendChild(instance.$el)
|
||||||
|
|
||||||
if (this.entities[index]) {
|
if (this.entities[index]) {
|
||||||
instance.setEntity(this.entities[index])
|
var entity = this.entities[index]
|
||||||
|
instance.setEntity(entity)
|
||||||
|
|
||||||
|
if (this.isBookEntity && !entity.isLocal) {
|
||||||
|
var localLibraryItem = this.localLibraryItems.find(lli => lli.libraryItemId == entity.id)
|
||||||
|
if (localLibraryItem) {
|
||||||
|
instance.setLocalLibraryItem(localLibraryItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,7 +35,8 @@ export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
shelves: [],
|
shelves: [],
|
||||||
loading: false
|
loading: false,
|
||||||
|
localLibraryItems: []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -55,8 +56,9 @@ export default {
|
||||||
methods: {
|
methods: {
|
||||||
async getLocalMediaItemCategories() {
|
async getLocalMediaItemCategories() {
|
||||||
var localMedia = await this.$db.getLocalLibraryItems()
|
var localMedia = await this.$db.getLocalLibraryItems()
|
||||||
|
console.log('Got local library items', localMedia ? localMedia.length : 'N/A')
|
||||||
if (!localMedia || !localMedia.length) return []
|
if (!localMedia || !localMedia.length) return []
|
||||||
console.log('Got local library items', localMedia.length)
|
|
||||||
var categories = []
|
var categories = []
|
||||||
var books = []
|
var books = []
|
||||||
var podcasts = []
|
var podcasts = []
|
||||||
|
@ -84,9 +86,11 @@ export default {
|
||||||
entities: podcasts
|
entities: podcasts
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return categories
|
return categories
|
||||||
},
|
},
|
||||||
async fetchCategories() {
|
async fetchCategories(from = null) {
|
||||||
|
console.log('[4breadcrumbs] fetchCategories', from)
|
||||||
if (this.loading) {
|
if (this.loading) {
|
||||||
console.log('Already loading categories')
|
console.log('Already loading categories')
|
||||||
return
|
return
|
||||||
|
@ -94,36 +98,39 @@ export default {
|
||||||
this.loading = true
|
this.loading = true
|
||||||
this.shelves = []
|
this.shelves = []
|
||||||
|
|
||||||
|
this.localLibraryItems = await this.$db.getLocalLibraryItems()
|
||||||
|
|
||||||
var localCategories = await this.getLocalMediaItemCategories()
|
var localCategories = await this.getLocalMediaItemCategories()
|
||||||
this.shelves = this.shelves.concat(localCategories)
|
this.shelves = this.shelves.concat(localCategories)
|
||||||
|
|
||||||
if (this.user && this.currentLibraryId) {
|
if (this.user && this.currentLibraryId) {
|
||||||
var categories = await this.$axios
|
var categories = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/personalized?minified=1`).catch((error) => {
|
||||||
.$get(`/api/libraries/${this.currentLibraryId}/personalized?minified=1`)
|
|
||||||
.then((data) => {
|
|
||||||
return data
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error('Failed to fetch categories', error)
|
console.error('Failed to fetch categories', error)
|
||||||
return []
|
return []
|
||||||
})
|
})
|
||||||
|
categories = categories.map((cat) => {
|
||||||
|
console.log('[breadcrumb] Personalized category from server', cat.type)
|
||||||
|
if (cat.type == 'book' || cat.type == 'podcast') {
|
||||||
|
// Map localLibraryItem to entities
|
||||||
|
cat.entities = cat.entities.map((entity) => {
|
||||||
|
var localLibraryItem = this.localLibraryItems.find((lli) => {
|
||||||
|
return lli.libraryItemId == entity.id
|
||||||
|
})
|
||||||
|
if (localLibraryItem) {
|
||||||
|
entity.localLibraryItem = localLibraryItem
|
||||||
|
}
|
||||||
|
return entity
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return cat
|
||||||
|
})
|
||||||
this.shelves = this.shelves.concat(categories)
|
this.shelves = this.shelves.concat(categories)
|
||||||
}
|
}
|
||||||
this.loading = false
|
this.loading = false
|
||||||
},
|
},
|
||||||
// async socketInit(isConnected) {
|
|
||||||
// if (isConnected && this.currentLibraryId) {
|
|
||||||
// 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) {
|
async libraryChanged(libid) {
|
||||||
if (this.isSocketConnected && this.currentLibraryId) {
|
if (this.currentLibraryId) {
|
||||||
await this.fetchCategories()
|
await this.fetchCategories('libraryChanged')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
audiobookAdded(audiobook) {
|
audiobookAdded(audiobook) {
|
||||||
|
@ -181,7 +188,7 @@ export default {
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.initListeners()
|
this.initListeners()
|
||||||
this.fetchCategories()
|
this.fetchCategories('mounted')
|
||||||
// if (this.$server.initialized && this.currentLibraryId) {
|
// if (this.$server.initialized && this.currentLibraryId) {
|
||||||
// this.fetchCategories()
|
// this.fetchCategories()
|
||||||
// } else {
|
// } else {
|
||||||
|
|
|
@ -36,27 +36,27 @@
|
||||||
<span class="material-icons">auto_stories</span>
|
<span class="material-icons">auto_stories</span>
|
||||||
<span v-if="!showPlay" class="px-2 text-base">Read {{ ebookFormat }}</span>
|
<span v-if="!showPlay" class="px-2 text-base">Read {{ ebookFormat }}</span>
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
<ui-btn v-if="isConnected && showPlay && !isIos" color="primary" class="flex items-center justify-center" :padding-x="2" @click="downloadClick">
|
|
||||||
<span class="material-icons">download</span>
|
|
||||||
<!-- <span class="material-icons" :class="downloadObj ? 'animate-pulse' : ''">{{ downloadObj ? (isDownloading || isDownloadPreparing ? 'downloading' : 'download_done') : 'download' }}</span> -->
|
|
||||||
</ui-btn>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="(isConnected && (showPlay || showRead)) || isDownloadPlayable" class="flex mt-4 -mr-2">
|
<div v-else-if="(user && (showPlay || showRead)) || hasLocal" class="flex mt-4 -mr-2">
|
||||||
<ui-btn v-if="showPlay" color="success" :disabled="isPlaying" class="flex items-center justify-center flex-grow mr-2" :padding-x="4" @click="playClick">
|
<ui-btn v-if="showPlay" color="success" :disabled="isPlaying" class="flex items-center justify-center flex-grow mr-2" :padding-x="4" @click="playClick">
|
||||||
<span v-show="!isPlaying" class="material-icons">play_arrow</span>
|
<span v-show="!isPlaying" class="material-icons">play_arrow</span>
|
||||||
<span class="px-1 text-sm">{{ isPlaying ? (isStreaming ? 'Streaming' : 'Playing') : isDownloadPlayable ? 'Play local' : 'Play stream' }}</span>
|
<span class="px-1 text-sm">{{ isPlaying ? (isStreaming ? 'Streaming' : 'Playing') : hasLocal ? 'Play Local' : 'Play Stream' }}</span>
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
<ui-btn v-if="showRead && isConnected" color="info" class="flex items-center justify-center mr-2" :class="showPlay ? '' : 'flex-grow'" :padding-x="2" @click="readBook">
|
<ui-btn v-if="showRead && user" color="info" class="flex items-center justify-center mr-2" :class="showPlay ? '' : 'flex-grow'" :padding-x="2" @click="readBook">
|
||||||
<span class="material-icons">auto_stories</span>
|
<span class="material-icons">auto_stories</span>
|
||||||
<span v-if="!showPlay" class="px-2 text-base">Read {{ ebookFormat }}</span>
|
<span v-if="!showPlay" class="px-2 text-base">Read {{ ebookFormat }}</span>
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
<ui-btn v-if="isConnected && showPlay && !isIos" color="primary" class="flex items-center justify-center" :padding-x="2" @click="downloadClick">
|
<ui-btn v-if="user && showPlay && !isIos && !hasLocal" :color="downloadItem ? 'warning' : 'primary'" class="flex items-center justify-center" :padding-x="2" @click="downloadClick">
|
||||||
<span class="material-icons">download</span>
|
<span class="material-icons" :class="downloadItem ? 'animate-pulse' : ''">{{ downloadItem ? 'downloading' : 'download' }}</span>
|
||||||
<!-- <span class="material-icons" :class="downloadObj ? 'animate-pulse' : ''">{{ downloadObj ? (isDownloading || isDownloadPreparing ? 'downloading' : 'download_done') : 'download' }}</span> -->
|
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="downloadItem" class="py-3">
|
||||||
|
<p class="text-center text-lg">Downloading! ({{ Math.round(downloadItem.itemProgress * 100) }}%)</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="w-full py-4">
|
<div class="w-full py-4">
|
||||||
<p>{{ description }}</p>
|
<p>{{ description }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -81,6 +81,14 @@ export default {
|
||||||
console.error('Failed', error)
|
console.error('Failed', error)
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
// Check if
|
||||||
|
if (libraryItem) {
|
||||||
|
var localLibraryItem = await app.$db.getLocalLibraryItemByLLId(libraryItemId)
|
||||||
|
if (localLibraryItem) {
|
||||||
|
console.log('Library item has local library item also', localLibraryItem.id)
|
||||||
|
libraryItem.localLibraryItem = localLibraryItem
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!libraryItem) {
|
if (!libraryItem) {
|
||||||
|
@ -104,6 +112,14 @@ export default {
|
||||||
isLocal() {
|
isLocal() {
|
||||||
return this.libraryItem.isLocal
|
return this.libraryItem.isLocal
|
||||||
},
|
},
|
||||||
|
hasLocal() {
|
||||||
|
// Server library item has matching local library item
|
||||||
|
return this.isLocal || this.libraryItem.localLibraryItem
|
||||||
|
},
|
||||||
|
localLibraryItem() {
|
||||||
|
if (this.isLocal) return this.libraryItem
|
||||||
|
return this.libraryItem.localLibraryItem || null
|
||||||
|
},
|
||||||
isConnected() {
|
isConnected() {
|
||||||
return this.$store.state.socketConnected
|
return this.$store.state.socketConnected
|
||||||
},
|
},
|
||||||
|
@ -140,6 +156,9 @@ export default {
|
||||||
size() {
|
size() {
|
||||||
return this.media.size
|
return this.media.size
|
||||||
},
|
},
|
||||||
|
user() {
|
||||||
|
return this.$store.state.user.user
|
||||||
|
},
|
||||||
userToken() {
|
userToken() {
|
||||||
return this.$store.getters['user/getToken']
|
return this.$store.getters['user/getToken']
|
||||||
},
|
},
|
||||||
|
@ -179,9 +198,6 @@ export default {
|
||||||
isIncomplete() {
|
isIncomplete() {
|
||||||
return this.libraryItem.isIncomplete
|
return this.libraryItem.isIncomplete
|
||||||
},
|
},
|
||||||
isDownloading() {
|
|
||||||
return this.downloadObj ? this.downloadObj.isDownloading : false
|
|
||||||
},
|
|
||||||
showPlay() {
|
showPlay() {
|
||||||
return !this.isMissing && !this.isIncomplete && this.numTracks
|
return !this.isMissing && !this.isIncomplete && this.numTracks
|
||||||
},
|
},
|
||||||
|
@ -195,12 +211,14 @@ export default {
|
||||||
if (!this.ebookFile) return null
|
if (!this.ebookFile) return null
|
||||||
return this.ebookFile.ebookFormat
|
return this.ebookFile.ebookFormat
|
||||||
},
|
},
|
||||||
isDownloadPlayable() {
|
|
||||||
return false
|
|
||||||
// return this.downloadObj && !this.isDownloading && !this.isDownloadPreparing
|
|
||||||
},
|
|
||||||
hasStoragePermission() {
|
hasStoragePermission() {
|
||||||
return this.$store.state.hasStoragePermission
|
return this.$store.state.hasStoragePermission
|
||||||
|
},
|
||||||
|
downloadItem() {
|
||||||
|
return this.$store.getters['globals/getDownloadItem'](this.libraryItemId)
|
||||||
|
},
|
||||||
|
downloadItems() {
|
||||||
|
return this.$store.state.globals.downloadItems || []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -208,6 +226,8 @@ export default {
|
||||||
this.$store.commit('openReader', this.libraryItem)
|
this.$store.commit('openReader', this.libraryItem)
|
||||||
},
|
},
|
||||||
playClick() {
|
playClick() {
|
||||||
|
// Todo: Allow playing local or streaming
|
||||||
|
if (this.hasLocal) return this.$eventBus.$emit('play-item', this.localLibraryItem.id)
|
||||||
this.$eventBus.$emit('play-item', this.libraryItem.id)
|
this.$eventBus.$emit('play-item', this.libraryItem.id)
|
||||||
},
|
},
|
||||||
async clearProgressClick() {
|
async clearProgressClick() {
|
||||||
|
@ -249,6 +269,9 @@ export default {
|
||||||
this.download(localFolder)
|
this.download(localFolder)
|
||||||
},
|
},
|
||||||
downloadClick() {
|
downloadClick() {
|
||||||
|
if (this.downloadItem) {
|
||||||
|
return
|
||||||
|
}
|
||||||
this.download()
|
this.download()
|
||||||
},
|
},
|
||||||
async download(selectedLocalFolder = null) {
|
async download(selectedLocalFolder = null) {
|
||||||
|
@ -296,11 +319,17 @@ export default {
|
||||||
async startDownload(localFolder) {
|
async startDownload(localFolder) {
|
||||||
console.log('Starting download to local folder', localFolder.name)
|
console.log('Starting download to local folder', localFolder.name)
|
||||||
var downloadRes = await AbsDownloader.downloadLibraryItem({ libraryItemId: this.libraryItemId, localFolderId: localFolder.id })
|
var downloadRes = await AbsDownloader.downloadLibraryItem({ libraryItemId: this.libraryItemId, localFolderId: localFolder.id })
|
||||||
if (downloadRes.error) {
|
if (downloadRes && downloadRes.error) {
|
||||||
var errorMsg = downloadRes.error || 'Unknown error'
|
var errorMsg = downloadRes.error || 'Unknown error'
|
||||||
console.error('Download error', errorMsg)
|
console.error('Download error', errorMsg)
|
||||||
this.$toast.error(errorMsg)
|
this.$toast.error(errorMsg)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
newLocalLibraryItem(item) {
|
||||||
|
if (item.libraryItemId == this.libraryItemId) {
|
||||||
|
console.log('New local library item', item.id)
|
||||||
|
this.$set(this.libraryItem, 'localLibraryItem', item)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// async prepareDownload() {
|
// async prepareDownload() {
|
||||||
// var audiobook = this.libraryItem
|
// var audiobook = this.libraryItem
|
||||||
|
@ -429,6 +458,7 @@ export default {
|
||||||
// }
|
// }
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
this.$eventBus.$on('new-local-library-item', this.newLocalLibraryItem)
|
||||||
// if (!this.$server.socket) {
|
// if (!this.$server.socket) {
|
||||||
// console.warn('Library Item Page mounted: Server socket not set')
|
// console.warn('Library Item Page mounted: Server socket not set')
|
||||||
// } else {
|
// } else {
|
||||||
|
@ -439,6 +469,7 @@ export default {
|
||||||
// }
|
// }
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
|
this.$eventBus.$off('new-local-library-item', this.newLocalLibraryItem)
|
||||||
// if (!this.$server.socket) {
|
// if (!this.$server.socket) {
|
||||||
// console.warn('Library Item Page beforeDestroy: Server socket not set')
|
// console.warn('Library Item Page beforeDestroy: Server socket not set')
|
||||||
// } else {
|
// } else {
|
||||||
|
|
|
@ -131,10 +131,29 @@ export default {
|
||||||
if (this.shouldScan) {
|
if (this.shouldScan) {
|
||||||
this.scanFolder()
|
this.scanFolder()
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
newLocalLibraryItem(item) {
|
||||||
|
if (item.folderId == this.folderId) {
|
||||||
|
console.log('New local library item', item.id)
|
||||||
|
if (this.localLibraryItems.find((li) => li.id == item.id)) {
|
||||||
|
console.warn('Item already added', item.id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var _item = {
|
||||||
|
...item,
|
||||||
|
coverPathSrc: item.coverContentUrl ? Capacitor.convertFileSrc(item.coverContentUrl) : null
|
||||||
|
}
|
||||||
|
this.localLibraryItems.push(_item)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
this.$eventBus.$on('new-local-library-item', this.newLocalLibraryItem)
|
||||||
this.init()
|
this.init()
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.$eventBus.$off('new-local-library-item', this.newLocalLibraryItem)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -12,6 +12,9 @@
|
||||||
</div>
|
</div>
|
||||||
<p class="text-lg mb-0.5 text-white text-opacity-75">Folder: {{ folderName }}</p>
|
<p class="text-lg mb-0.5 text-white text-opacity-75">Folder: {{ folderName }}</p>
|
||||||
<p class="mb-4 text-xl">{{ mediaMetadata.title }}</p>
|
<p class="mb-4 text-xl">{{ mediaMetadata.title }}</p>
|
||||||
|
|
||||||
|
<p class="mb-4 text-xs text-gray-400">{{ libraryItemId || 'Not linked to server library item' }}</p>
|
||||||
|
|
||||||
<div v-if="isScanning" class="w-full text-center p-4">
|
<div v-if="isScanning" class="w-full text-center p-4">
|
||||||
<p>Scanning...</p>
|
<p>Scanning...</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -88,6 +91,9 @@ export default {
|
||||||
},
|
},
|
||||||
mediaType() {
|
mediaType() {
|
||||||
return this.localLibraryItem ? this.localLibraryItem.mediaType : null
|
return this.localLibraryItem ? this.localLibraryItem.mediaType : null
|
||||||
|
},
|
||||||
|
libraryItemId() {
|
||||||
|
return this.localLibraryItem ? this.localLibraryItem.libraryItemId : null
|
||||||
},
|
},
|
||||||
media() {
|
media() {
|
||||||
return this.localLibraryItem ? this.localLibraryItem.media : null
|
return this.localLibraryItem ? this.localLibraryItem.media : null
|
||||||
|
|
|
@ -84,9 +84,9 @@ class DbService {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
getLocalLibraryItems() {
|
getLocalLibraryItems(mediaType = null) {
|
||||||
if (isWeb) return []
|
if (isWeb) return []
|
||||||
return AbsDatabase.getLocalLibraryItems().then((data) => {
|
return AbsDatabase.getLocalLibraryItems(mediaType).then((data) => {
|
||||||
console.log('Loaded all local media items', JSON.stringify(data))
|
console.log('Loaded all local media items', JSON.stringify(data))
|
||||||
if (data.localLibraryItems && typeof data.localLibraryItems == 'string') {
|
if (data.localLibraryItems && typeof data.localLibraryItems == 'string') {
|
||||||
return JSON.parse(data.localLibraryItems)
|
return JSON.parse(data.localLibraryItems)
|
||||||
|
@ -99,6 +99,11 @@ class DbService {
|
||||||
if (isWeb) return null
|
if (isWeb) return null
|
||||||
return AbsDatabase.getLocalLibraryItem({ id })
|
return AbsDatabase.getLocalLibraryItem({ id })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getLocalLibraryItemByLLId(libraryItemId) {
|
||||||
|
if (isWeb) return null
|
||||||
|
return AbsDatabase.getLocalLibraryItemByLLId({ libraryItemId })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ({ app, store }, inject) => {
|
export default ({ app, store }, inject) => {
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
export const state = () => ({
|
export const state = () => ({
|
||||||
|
itemDownloads: []
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getters = {
|
export const getters = {
|
||||||
|
getDownloadItem: state => libraryItemId => {
|
||||||
|
return state.itemDownloads.find(i => i.id == libraryItemId)
|
||||||
|
},
|
||||||
getLibraryItemCoverSrc: (state, getters, rootState, rootGetters) => (libraryItem, placeholder = '/book_placeholder.jpg') => {
|
getLibraryItemCoverSrc: (state, getters, rootState, rootGetters) => (libraryItem, placeholder = '/book_placeholder.jpg') => {
|
||||||
if (!libraryItem) return placeholder
|
if (!libraryItem) return placeholder
|
||||||
var media = libraryItem.media
|
var media = libraryItem.media
|
||||||
|
@ -28,5 +31,15 @@ export const actions = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const mutations = {
|
export const mutations = {
|
||||||
|
addUpdateItemDownload(state, downloadItem) {
|
||||||
|
var index = state.itemDownloads.findIndex(i => i.id == downloadItem.id)
|
||||||
|
if (index >= 0) {
|
||||||
|
state.itemDownloads.splice(index, 1, downloadItem)
|
||||||
|
} else {
|
||||||
|
state.itemDownloads.push(downloadItem)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
removeItemDownload(state, id) {
|
||||||
|
state.itemDownloads = state.itemDownloads.filter(i => i.id != id)
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -17,6 +17,10 @@ export const getters = {
|
||||||
getCurrentLibraryName: (state, getters) => {
|
getCurrentLibraryName: (state, getters) => {
|
||||||
var currLib = getters.getCurrentLibrary
|
var currLib = getters.getCurrentLibrary
|
||||||
return currLib ? currLib.name : null
|
return currLib ? currLib.name : null
|
||||||
|
},
|
||||||
|
getCurrentLibraryMediaType: (state, getters) => {
|
||||||
|
var currLib = getters.getCurrentLibrary
|
||||||
|
return currLib ? currLib.mediaType : null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue