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:
advplyr 2022-04-07 18:46:58 -05:00
parent ee942c6704
commit 119bfd6c98
18 changed files with 520 additions and 80 deletions

View file

@ -15,11 +15,11 @@ class DbManager {
Paper.book("device").write("data", deviceData)
}
fun getLocalLibraryItems():MutableList<LocalLibraryItem> {
fun getLocalLibraryItems(mediaType:String? = null):MutableList<LocalLibraryItem> {
var localLibraryItems:MutableList<LocalLibraryItem> = mutableListOf()
Paper.book("localLibraryItems").allKeys.forEach {
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
// if (localMediaItem.coverContentUrl != null) {
// 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? {
return Paper.book("localLibraryItems").read(localLibraryItemId)
}

View file

@ -50,6 +50,7 @@ class AbsDatabase : Plugin() {
@PluginMethod
fun getLocalLibraryItem(call:PluginCall) {
var id = call.getString("id", "").toString()
GlobalScope.launch(Dispatchers.IO) {
var localLibraryItem = DeviceManager.dbManager.getLocalLibraryItem(id)
if (localLibraryItem == null) {
@ -61,9 +62,24 @@ class AbsDatabase : Plugin() {
}
@PluginMethod
fun getLocalLibraryItems(call:PluginCall) {
fun getLocalLibraryItemByLLId(call:PluginCall) {
var libraryItemId = call.getString("libraryItemId", "").toString()
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()
jsobj.put("localLibraryItems", jacksonObjectMapper().writeValueAsString(localLibraryItems))
call.resolve(jsobj)

View file

@ -144,13 +144,18 @@ class AbsDownloader : Plugin() {
var itemFolderPath = localFolder.absolutePath + "/" + bookTitle
var downloadItem = DownloadItem(libraryItem.id, libraryItem.mediaType, itemFolderPath, localFolder, bookTitle, libraryItem.media, mutableListOf())
// Create download item part for each audio track
tracks.forEach { audioTrack ->
var serverPath = "/s/item/${libraryItem.id}/${cleanRelPath(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}")
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 downloadUri = Uri.parse("${DeviceManager.serverAddress}${serverPath}?token=${DeviceManager.token}")
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 destinationFilename = "cover.jpg"
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 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)
@ -237,20 +248,20 @@ class AbsDownloader : Plugin() {
val totalBytes = it.getInt(it.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES))
val downloadStatus = it.getInt(it.getColumnIndex(DownloadManager.COLUMN_STATUS))
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) {
Log.d(tag, "Download ${downloadItemPart.filename} Done")
Log.d(tag, "checkDownloads Download ${downloadItemPart.filename} Done")
// downloadItem.downloadItemParts.remove(downloadItemPart)
downloadItemPart.completed = true
} 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)
// downloadItemPart.completed = true
} else {
//update progress
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
}
} else {

View file

@ -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">
<span class="material-icons text-3xl text-white">arrow_back</span>
</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">
<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" />
@ -17,6 +17,8 @@
</div>
<div class="flex-grow" />
<widgets-download-progress-indicator />
<nuxt-link class="h-7 mx-2" to="/search">
<span class="material-icons" style="font-size: 1.75rem">search</span>
</nuxt-link>

View file

@ -42,7 +42,8 @@ export default {
entityIndexesMounted: [],
pagesLoaded: {},
isFirstInit: false,
pendingReset: false
pendingReset: false,
localLibraryItems: []
}
},
computed: {
@ -101,6 +102,9 @@ export default {
currentLibraryId() {
return this.$store.state.libraries.currentLibraryId
},
currentLibraryMediaType() {
return this.$store.getters['libraries/getCurrentLibraryMediaType']
},
shelfHeight() {
return this.entityHeight + 40
},
@ -164,6 +168,13 @@ export default {
this.entities[index] = payload.results[i]
if (this.entityComponentRefs[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)
}
// Remove entities out of view
this.entityIndexesMounted = this.entityIndexesMounted.filter((_index) => {
if (_index < firstBookIndex || _index >= lastBookIndex) {
var el = document.getElementById(`book-card-${_index}`)
@ -295,6 +307,10 @@ export default {
},
async init() {
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.initSizeData()
await this.loadPage(0)
@ -360,6 +376,13 @@ export default {
this.entities[indexOf] = libraryItem
if (this.entityComponentRefs[indexOf]) {
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)
}
}
}
}
}

View file

@ -37,6 +37,10 @@
<!-- 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="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 -->
<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">
@ -85,7 +89,8 @@ export default {
rescanning: false,
selected: false,
isSelectionMode: false,
showCoverBg: false
showCoverBg: false,
localLibraryItem: null
}
},
watch: {
@ -105,9 +110,12 @@ export default {
return this.libraryItem || {}
},
isLocal() {
// Is local library item
return !!this._libraryItem.isLocal
},
isLocalOnly() {
// Local item with no server match
return this.isLocal && !this._libraryItem.libraryItemId
},
media() {
return this._libraryItem.media || {}
},
@ -119,7 +127,7 @@ export default {
},
bookCoverSrc() {
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.store.getters['globals/getLibraryItemCoverSrc'](this._libraryItem, this.placeholderUrl)
@ -319,6 +327,10 @@ export default {
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()
@ -442,6 +454,10 @@ export default {
mounted() {
if (this.bookMount) {
this.setEntity(this.bookMount)
if (this.bookMount.localLibraryItem) {
this.setLocalLibraryItem(this.bookMount.localLibraryItem)
}
}
}
}

View file

@ -66,6 +66,9 @@ export default {
if (!this.libraryItem) return false
return this.libraryItem.isLocal
},
localCover() {
return this.libraryItem ? this.libraryItem.coverContentUrl : null
},
squareAspectRatio() {
return this.bookCoverAspectRatio === 1
},
@ -105,7 +108,7 @@ export default {
},
fullCoverUrl() {
if (this.isLocal) {
if (this.hasCover) return Capacitor.convertFileSrc(this.cover)
if (this.localCover) return Capacitor.convertFileSrc(this.localCover)
return this.placeholderUrl
}
if (this.downloadCover) return this.downloadCover

View 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>

View 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>

View file

@ -13,17 +13,25 @@
<script>
import { AppUpdate } from '@robingenz/capacitor-app-update'
import { AbsFileSystem, AbsDownloader } from '@/plugins/capacitor'
import { AbsFileSystem } from '@/plugins/capacitor'
export default {
data() {
return {}
return {
attemptingConnection: false,
inittingLibraries: false
}
},
watch: {
networkConnected: {
handler(newVal) {
handler(newVal, oldVal) {
if (newVal) {
console.log(`[default] network connected changed ${oldVal} -> ${newVal}`)
if (!this.user) {
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() {
// Request and setup listeners for media files on native
AbsDownloader.addListener('onItemDownloadUpdate', (data) => {
this.onItemDownloadUpdate(data)
})
AbsDownloader.addListener('onItemDownloadComplete', (data) => {
this.onItemDownloadComplete(data)
})
// AbsDownloader.addListener('onItemDownloadUpdate', (data) => {
// this.onItemDownloadUpdate(data)
// })
// AbsDownloader.addListener('onItemDownloadComplete', (data) => {
// this.onItemDownloadComplete(data)
// })
},
async loadSavedSettings() {
var userSavedServerSettings = await this.$localStore.getServerSettings()
@ -167,6 +169,10 @@ export default {
console.warn('No network connection')
return
}
if (this.attemptingConnection) {
return
}
this.attemptingConnection = true
var deviceData = await this.$db.getDeviceData()
var serverConfig = null
@ -175,16 +181,22 @@ export default {
}
if (!serverConfig) {
// No last server config set
this.attemptingConnection = false
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) => {
console.error('[Server] Server auth failed', error)
var errorMsg = error.response ? error.response.data || 'Unknown Error' : 'Unknown Error'
this.error = errorMsg
return false
})
if (!authRes) return
if (!authRes) {
this.attemptingConnection = false
return
}
const { user, userDefaultLibraryId } = authRes
if (userDefaultLibraryId) {
@ -199,6 +211,7 @@ export default {
console.log('Successful connection on last saved connection config', JSON.stringify(serverConnectionConfig))
await this.initLibraries()
this.attemptingConnection = false
},
itemRemoved(libraryItem) {
if (this.$route.name.startsWith('item')) {
@ -219,9 +232,15 @@ export default {
},
socketInit(data) {},
async initLibraries() {
if (this.inittingLibraries) {
return
}
this.inittingLibraries = true
await this.$store.dispatch('libraries/load')
console.log(`[default] initLibraries loaded`)
this.$eventBus.$emit('library-changed')
this.$store.dispatch('libraries/fetch', this.currentLibraryId)
this.inittingLibraries = false
}
},
async mounted() {
@ -240,8 +259,10 @@ export default {
await this.$store.dispatch('setupNetworkListener')
if (this.$store.state.user.serverConnectionConfig) {
console.log(`[default] server connection config set - call init libraries`)
await this.initLibraries()
} else {
console.log(`[default] no server connection config - call attempt connection`)
await this.attemptConnection()
}

View file

@ -67,7 +67,15 @@ export default {
shelfEl.appendChild(instance.$el)
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)
}
}
}
},
}

View file

@ -35,7 +35,8 @@ export default {
data() {
return {
shelves: [],
loading: false
loading: false,
localLibraryItems: []
}
},
computed: {
@ -55,8 +56,9 @@ export default {
methods: {
async getLocalMediaItemCategories() {
var localMedia = await this.$db.getLocalLibraryItems()
console.log('Got local library items', localMedia ? localMedia.length : 'N/A')
if (!localMedia || !localMedia.length) return []
console.log('Got local library items', localMedia.length)
var categories = []
var books = []
var podcasts = []
@ -84,9 +86,11 @@ export default {
entities: podcasts
})
}
return categories
},
async fetchCategories() {
async fetchCategories(from = null) {
console.log('[4breadcrumbs] fetchCategories', from)
if (this.loading) {
console.log('Already loading categories')
return
@ -94,36 +98,39 @@ export default {
this.loading = true
this.shelves = []
this.localLibraryItems = await this.$db.getLocalLibraryItems()
var localCategories = await this.getLocalMediaItemCategories()
this.shelves = this.shelves.concat(localCategories)
if (this.user && this.currentLibraryId) {
var categories = await this.$axios
.$get(`/api/libraries/${this.currentLibraryId}/personalized?minified=1`)
.then((data) => {
return data
})
.catch((error) => {
var categories = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/personalized?minified=1`).catch((error) => {
console.error('Failed to fetch categories', error)
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.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) {
if (this.isSocketConnected && this.currentLibraryId) {
await this.fetchCategories()
if (this.currentLibraryId) {
await this.fetchCategories('libraryChanged')
}
},
audiobookAdded(audiobook) {
@ -181,7 +188,7 @@ export default {
},
mounted() {
this.initListeners()
this.fetchCategories()
this.fetchCategories('mounted')
// if (this.$server.initialized && this.currentLibraryId) {
// this.fetchCategories()
// } else {

View file

@ -36,27 +36,27 @@
<span class="material-icons">auto_stories</span>
<span v-if="!showPlay" class="px-2 text-base">Read {{ ebookFormat }}</span>
</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 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">
<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 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 v-if="!showPlay" class="px-2 text-base">Read {{ ebookFormat }}</span>
</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 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" :class="downloadItem ? 'animate-pulse' : ''">{{ downloadItem ? 'downloading' : 'download' }}</span>
</ui-btn>
</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">
<p>{{ description }}</p>
</div>
@ -81,6 +81,14 @@ export default {
console.error('Failed', error)
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) {
@ -104,6 +112,14 @@ export default {
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() {
return this.$store.state.socketConnected
},
@ -140,6 +156,9 @@ export default {
size() {
return this.media.size
},
user() {
return this.$store.state.user.user
},
userToken() {
return this.$store.getters['user/getToken']
},
@ -179,9 +198,6 @@ export default {
isIncomplete() {
return this.libraryItem.isIncomplete
},
isDownloading() {
return this.downloadObj ? this.downloadObj.isDownloading : false
},
showPlay() {
return !this.isMissing && !this.isIncomplete && this.numTracks
},
@ -195,12 +211,14 @@ export default {
if (!this.ebookFile) return null
return this.ebookFile.ebookFormat
},
isDownloadPlayable() {
return false
// return this.downloadObj && !this.isDownloading && !this.isDownloadPreparing
},
hasStoragePermission() {
return this.$store.state.hasStoragePermission
},
downloadItem() {
return this.$store.getters['globals/getDownloadItem'](this.libraryItemId)
},
downloadItems() {
return this.$store.state.globals.downloadItems || []
}
},
methods: {
@ -208,6 +226,8 @@ export default {
this.$store.commit('openReader', this.libraryItem)
},
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)
},
async clearProgressClick() {
@ -249,6 +269,9 @@ export default {
this.download(localFolder)
},
downloadClick() {
if (this.downloadItem) {
return
}
this.download()
},
async download(selectedLocalFolder = null) {
@ -296,11 +319,17 @@ export default {
async startDownload(localFolder) {
console.log('Starting download to local folder', localFolder.name)
var downloadRes = await AbsDownloader.downloadLibraryItem({ libraryItemId: this.libraryItemId, localFolderId: localFolder.id })
if (downloadRes.error) {
if (downloadRes && downloadRes.error) {
var errorMsg = downloadRes.error || 'Unknown error'
console.error('Download 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() {
// var audiobook = this.libraryItem
@ -429,6 +458,7 @@ export default {
// }
},
mounted() {
this.$eventBus.$on('new-local-library-item', this.newLocalLibraryItem)
// if (!this.$server.socket) {
// console.warn('Library Item Page mounted: Server socket not set')
// } else {
@ -439,6 +469,7 @@ export default {
// }
},
beforeDestroy() {
this.$eventBus.$off('new-local-library-item', this.newLocalLibraryItem)
// if (!this.$server.socket) {
// console.warn('Library Item Page beforeDestroy: Server socket not set')
// } else {

View file

@ -131,10 +131,29 @@ export default {
if (this.shouldScan) {
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() {
this.$eventBus.$on('new-local-library-item', this.newLocalLibraryItem)
this.init()
},
beforeDestroy() {
this.$eventBus.$off('new-local-library-item', this.newLocalLibraryItem)
}
}
</script>

View file

@ -12,6 +12,9 @@
</div>
<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-xs text-gray-400">{{ libraryItemId || 'Not linked to server library item' }}</p>
<div v-if="isScanning" class="w-full text-center p-4">
<p>Scanning...</p>
</div>
@ -88,6 +91,9 @@ export default {
},
mediaType() {
return this.localLibraryItem ? this.localLibraryItem.mediaType : null
},
libraryItemId() {
return this.localLibraryItem ? this.localLibraryItem.libraryItemId : null
},
media() {
return this.localLibraryItem ? this.localLibraryItem.media : null

View file

@ -84,9 +84,9 @@ class DbService {
})
}
getLocalLibraryItems() {
getLocalLibraryItems(mediaType = null) {
if (isWeb) return []
return AbsDatabase.getLocalLibraryItems().then((data) => {
return AbsDatabase.getLocalLibraryItems(mediaType).then((data) => {
console.log('Loaded all local media items', JSON.stringify(data))
if (data.localLibraryItems && typeof data.localLibraryItems == 'string') {
return JSON.parse(data.localLibraryItems)
@ -99,6 +99,11 @@ class DbService {
if (isWeb) return null
return AbsDatabase.getLocalLibraryItem({ id })
}
getLocalLibraryItemByLLId(libraryItemId) {
if (isWeb) return null
return AbsDatabase.getLocalLibraryItemByLLId({ libraryItemId })
}
}
export default ({ app, store }, inject) => {

View file

@ -1,8 +1,11 @@
export const state = () => ({
itemDownloads: []
})
export const getters = {
getDownloadItem: state => libraryItemId => {
return state.itemDownloads.find(i => i.id == libraryItemId)
},
getLibraryItemCoverSrc: (state, getters, rootState, rootGetters) => (libraryItem, placeholder = '/book_placeholder.jpg') => {
if (!libraryItem) return placeholder
var media = libraryItem.media
@ -28,5 +31,15 @@ export const actions = {
}
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)
}
}

View file

@ -17,6 +17,10 @@ export const getters = {
getCurrentLibraryName: (state, getters) => {
var currLib = getters.getCurrentLibrary
return currLib ? currLib.name : null
},
getCurrentLibraryMediaType: (state, getters) => {
var currLib = getters.getCurrentLibrary
return currLib ? currLib.mediaType : null
}
}