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
+3 -1
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>
+24 -1
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)
}
}
}
}
}
+19 -3
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)
}
}
}
}
+4 -1
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
+143
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>
@@ -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>