Update user audiobook progress model, add mark as read/not read, download individual tracks

This commit is contained in:
advplyr 2021-09-06 14:13:01 -05:00
parent 4f094df0b2
commit 11d7330c5d
21 changed files with 496 additions and 100 deletions

View file

@ -26,7 +26,7 @@
<span class="material-icons" :class="selected ? 'text-yellow-400' : ''" :style="{ fontSize: 1.25 * sizeMultiplier + 'rem' }">{{ selected ? 'radio_button_checked' : 'radio_button_unchecked' }}</span>
</div>
</div>
<div v-show="!isSelectionMode" class="absolute bottom-0 left-0 h-1 bg-yellow-400 shadow-sm" :style="{ width: width * userProgressPercent + 'px' }"></div>
<div v-show="!isSelectionMode" class="absolute bottom-0 left-0 h-1 shadow-sm" :class="userIsRead ? 'bg-success' : 'bg-yellow-400'" :style="{ width: width * userProgressPercent + 'px' }"></div>
<ui-tooltip v-if="showError" :text="errorText" class="absolute bottom-4 left-0">
<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">
@ -125,6 +125,9 @@ export default {
userProgressPercent() {
return this.userProgress ? this.userProgress.progress || 0 : 0
},
userIsRead() {
return this.userProgress ? !!this.userProgress.isRead : false
},
showError() {
return this.hasMissingParts || this.hasInvalidParts
},

View file

@ -10,7 +10,7 @@
<div :key="tab.id" class="w-28 rounded-t-lg flex items-center justify-center mr-1 cursor-pointer hover:bg-bg font-book border-t border-l border-r border-black-300 tab" :class="selectedTab === tab.id ? 'tab-selected bg-bg pb-px' : 'bg-primary text-gray-400'" @click="selectTab(tab.id)">{{ tab.title }}</div>
</template>
</div>
<div class="px-4 w-full h-full text-sm py-6 rounded-b-lg rounded-tr-lg bg-bg shadow-lg border border-black-300">
<div class="w-full h-full text-sm rounded-b-lg rounded-tr-lg bg-bg shadow-lg border border-black-300">
<keep-alive>
<component v-if="audiobook" :is="tabName" :audiobook="audiobook" :processing.sync="processing" @close="show = false" />
</keep-alive>
@ -22,7 +22,6 @@
export default {
data() {
return {
selectedTab: 'details',
processing: false,
audiobook: null,
fetchOnShow: false,
@ -79,6 +78,14 @@ export default {
this.$store.commit('setShowEditModal', val)
}
},
selectedTab: {
get() {
return this.$store.state.editModalTab
},
set(val) {
this.$store.commit('setEditModalTab', val)
}
},
height() {
var maxHeightAllowed = window.innerHeight - 150
return Math.min(maxHeightAllowed, 650)

View file

@ -1,5 +1,5 @@
<template>
<div class="w-full h-full overflow-hidden overflow-y-auto px-1">
<div class="w-full h-full overflow-hidden overflow-y-auto px-4 py-6">
<div class="flex">
<div class="relative">
<cards-book-cover :audiobook="audiobook" />

View file

@ -1,60 +1,64 @@
<template>
<div class="w-full h-full overflow-hidden overflow-y-auto px-1">
<div v-if="userProgress" class="bg-success bg-opacity-40 rounded-md w-full px-4 py-1 mb-4 border border-success border-opacity-50">
<div class="w-full flex items-center">
<p>
Your progress: <span class="font-mono text-lg">{{ (userProgress * 100).toFixed(0) }}%</span>
</p>
<div class="flex-grow" />
<ui-btn v-if="!resettingProgress" small :padding-x="2" class="-mr-3" @click="resetProgress">Reset</ui-btn>
</div>
<div class="w-full h-full relative">
<div ref="formWrapper" class="px-4 py-6 details-form-wrapper w-full overflow-hidden overflow-y-auto">
<!-- <div v-if="userProgress" class="bg-success bg-opacity-40 rounded-md w-full px-4 py-1 mb-4 border border-success border-opacity-50">
<div class="w-full flex items-center">
<p>
Your progress: <span class="font-mono text-lg">{{ (userProgress * 100).toFixed(0) }}%</span>
</p>
<div class="flex-grow" />
<ui-btn v-if="!resettingProgress" small :padding-x="2" class="-mr-3" @click="resetProgress">Reset</ui-btn>
</div>
</div> -->
<form @submit.prevent="submitForm">
<ui-text-input-with-label v-model="details.title" label="Title" />
<ui-text-input-with-label v-model="details.subtitle" label="Subtitle" class="mt-2" />
<div class="flex mt-2 -mx-1">
<div class="w-3/4 px-1">
<ui-text-input-with-label v-model="details.author" label="Author" />
</div>
<div class="flex-grow px-1">
<ui-text-input-with-label v-model="details.publishYear" type="number" label="Publish Year" />
</div>
</div>
<div class="flex mt-2 -mx-1">
<div class="w-3/4 px-1">
<ui-input-dropdown v-model="details.series" label="Series" :items="series" />
</div>
<div class="flex-grow px-1">
<ui-text-input-with-label v-model="details.volumeNumber" label="Volume #" />
</div>
</div>
<ui-textarea-with-label v-model="details.description" :rows="3" label="Description" class="mt-2" />
<div class="flex mt-2 -mx-1">
<div class="w-1/2 px-1">
<ui-multi-select v-model="details.genres" label="Genres" :items="genres" />
</div>
<div class="flex-grow px-1">
<ui-multi-select v-model="newTags" label="Tags" :items="tags" />
</div>
</div>
<div class="flex mt-2 -mx-1">
<div class="w-1/2 px-1">
<ui-text-input-with-label v-model="details.narrarator" label="Narrarator" />
</div>
</div>
</form>
</div>
<form @submit.prevent="submitForm">
<ui-text-input-with-label v-model="details.title" label="Title" />
<ui-text-input-with-label v-model="details.subtitle" label="Subtitle" class="mt-2" />
<div class="flex mt-2 -mx-1">
<div class="w-3/4 px-1">
<ui-text-input-with-label v-model="details.author" label="Author" />
</div>
<div class="flex-grow px-1">
<ui-text-input-with-label v-model="details.publishYear" type="number" label="Publish Year" />
</div>
</div>
<div class="flex mt-2 -mx-1">
<div class="w-3/4 px-1">
<ui-input-dropdown v-model="details.series" label="Series" :items="series" />
</div>
<div class="flex-grow px-1">
<ui-text-input-with-label v-model="details.volumeNumber" label="Volume #" />
</div>
</div>
<ui-textarea-with-label v-model="details.description" :rows="3" label="Description" class="mt-2" />
<div class="flex mt-2 -mx-1">
<div class="w-1/2 px-1">
<ui-multi-select v-model="details.genres" label="Genres" :items="genres" />
</div>
<div class="flex-grow px-1">
<ui-multi-select v-model="newTags" label="Tags" :items="tags" />
</div>
</div>
<div class="flex mt-2 -mx-1">
<div class="w-1/2 px-1">
<ui-text-input-with-label v-model="details.narrarator" label="Narrarator" />
</div>
</div>
<div class="flex py-4 mt-2">
<div class="absolute bottom-0 left-0 w-full py-4 bg-bg" :class="isScrollable ? 'box-shadow-md-up' : 'border-t border-primary border-opacity-50'">
<div class="flex px-4">
<ui-btn color="error" type="button" small @click.stop.prevent="deleteAudiobook">Remove</ui-btn>
<div class="flex-grow" />
<ui-btn type="submit">Submit</ui-btn>
</div>
</form>
</div>
</div>
</template>
@ -81,7 +85,8 @@ export default {
genres: []
},
newTags: [],
resettingProgress: false
resettingProgress: false,
isScrollable: false
}
},
watch: {
@ -107,12 +112,12 @@ export default {
book() {
return this.audiobook ? this.audiobook.book || {} : {}
},
userAudiobook() {
return this.$store.getters['user/getUserAudiobook'](this.audiobookId)
},
userProgress() {
return this.userAudiobook ? this.userAudiobook.progress : 0
},
// userAudiobook() {
// return this.$store.getters['user/getUserAudiobook'](this.audiobookId)
// },
// userProgress() {
// return this.userAudiobook ? this.userAudiobook.progress : 0
// },
genres() {
return this.$store.state.audiobooks.genres
},
@ -189,7 +194,40 @@ export default {
this.isProcessing = false
})
}
},
checkIsScrollable() {
this.$nextTick(() => {
if (this.$refs.formWrapper) {
if (this.$refs.formWrapper.scrollHeight > this.$refs.formWrapper.clientHeight) {
this.isScrollable = true
} else {
this.isScrollable = false
}
}
})
},
setResizeObserver() {
try {
this.$nextTick(() => {
const resizeObserver = new ResizeObserver(() => {
this.checkIsScrollable()
})
resizeObserver.observe(this.$refs.formWrapper)
})
} catch (error) {
console.error('Failed to set resize observer')
}
}
},
mounted() {
// this.init()
this.setResizeObserver()
}
}
</script>
</script>
<style scoped>
.details-form-wrapper {
height: calc(100% - 70px);
}
</style>

View file

@ -1,5 +1,5 @@
<template>
<div class="w-full h-full overflow-hidden overflow-y-auto px-1">
<div class="w-full h-full overflow-hidden overflow-y-auto px-4 py-6">
<div class="w-full border border-black-200 p-4 my-4">
<p class="text-center text-lg mb-4 pb-8 border-b border-black-200">
<span class="text-error">Experimental Feature!</span> If your audiobook is made up of multiple audio files, this will concatenate them into a single file. The file type will be the same as the first track. Preparing downloads can take anywhere from a few seconds to several minutes and will be stored in
@ -13,8 +13,8 @@
<p v-if="singleAudioDownloadFailed" class="text-error mb-2">Download Failed</p>
<p v-if="singleAudioDownloadReady" class="text-success mb-2">Download Ready!</p>
<p v-if="singleAudioDownloadExpired" class="text-error mb-2">Download Expired</p>
<ui-btn v-if="!singleAudioDownloadReady" :loading="singleAudioDownloadPending" :disabled="tempDisable" @click="startSingleAudioDownload">Start Download</ui-btn>
<a v-if="isSingleTrack" :href="`/local/${singleTrackPath}`" class="btn outline-none rounded-md shadow-md relative border border-gray-600 px-4 py-2 bg-primary">Download Track</a>
<ui-btn v-else-if="!singleAudioDownloadReady" :loading="singleAudioDownloadPending" :disabled="tempDisable" @click="startSingleAudioDownload">Start Download</ui-btn>
<ui-btn v-else @click="downloadWithProgress">Download</ui-btn>
</div>
</div>
@ -81,6 +81,14 @@ export default {
},
zipBundleDownload() {
return this.downloads.find((d) => d.type === 'zipBundle')
},
isSingleTrack() {
if (!this.audiobook.tracks) return false
return this.audiobook.tracks.length === 1
},
singleTrackPath() {
if (!this.isSingleTrack) return null
return this.audiobook.tracks[0].path
}
},
methods: {

View file

@ -1,5 +1,5 @@
<template>
<div class="w-full h-full overflow-hidden">
<div class="w-full h-full overflow-hidden px-4 py-6">
<form @submit.prevent="submitSearch">
<div class="flex items-center justify-start -mx-1 h-20">
<div class="w-72 px-1">

View file

@ -1,5 +1,5 @@
<template>
<div class="w-full h-full overflow-y-auto overflow-x-hidden">
<div class="w-full h-full overflow-y-auto overflow-x-hidden px-4 py-6">
<div class="flex mb-4">
<nuxt-link :to="`/audiobook/${audiobook.id}/edit`">
<ui-btn color="primary">Edit Track Order</ui-btn>
@ -11,6 +11,7 @@
<th class="text-left">Filename</th>
<th class="text-left">Size</th>
<th class="text-left">Duration</th>
<th class="text-center">Download</th>
</tr>
<template v-for="track in tracks">
<tr :key="track.index">
@ -26,6 +27,9 @@
<td class="font-mono">
{{ $secondsToTimestamp(track.duration) }}
</td>
<td class="font-mono text-center">
<a :href="`/local/${track.path}`" download><span class="material-icons icon-text">download</span></a>
</td>
</tr>
</template>
</table>

View file

@ -19,6 +19,7 @@
<th class="text-left">Filename</th>
<th class="text-left">Size</th>
<th class="text-left">Duration</th>
<th class="text-center">Download</th>
</tr>
<template v-for="track in tracks">
<tr :key="track.index">
@ -34,6 +35,9 @@
<td class="font-mono">
{{ $secondsToTimestamp(track.duration) }}
</td>
<td class="text-center">
<a :href="`/local/${track.path}`" download><span class="material-icons icon-text">download</span></a>
</td>
</tr>
</template>
</table>

View file

@ -0,0 +1,49 @@
<template>
<button class="icon-btn rounded-md bg-primary border border-gray-600 flex items-center justify-center h-9 w-9 relative" @click="clickBtn">
<span class="material-icons icon-text">{{ icon }}</span>
</button>
</template>
<script>
export default {
props: {
icon: String,
disabled: Boolean
},
data() {
return {}
},
computed: {},
methods: {
clickBtn(e) {
if (this.disabled) {
e.preventDefault()
return
}
this.$emit('click')
e.stopPropagation()
}
},
mounted() {}
}
</script>
<style>
button.icon-btn::before {
content: '';
position: absolute;
border-radius: 6px;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0);
transition: all 0.1s ease-in-out;
}
button.icon-btn:hover:not(:disabled)::before {
background-color: rgba(255, 255, 255, 0.1);
}
button.icon-btn:disabled::before {
background-color: rgba(0, 0, 0, 0.2);
}
</style>

View file

@ -0,0 +1,82 @@
<template>
<button class="icon-btn rounded-md bg-primary border border-gray-600 flex items-center justify-center h-9 w-9 relative" @click="clickBtn">
<div class="w-5 h-5 text-white relative">
<svg v-if="isRead" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-9 15l-5-5 1.41-1.41L10 13.17l7.59-7.59L19 7l-9 9z" />
</svg>
<svg v-else xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-7 19.6l-7-4.66V3h14v12.93l-7 4.67zm-2.01-7.42l-2.58-2.59L6 12l4 4 8-8-1.42-1.42z" />
</svg>
<!-- <svg v-if="!isRead" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 482.204 482.204" xml:space="preserve" fill="currentColor">
<path
d="M83.127,344.477c54.602,1.063,101.919,9.228,136.837,23.613c0.596,0.244,1.227,0.366,1.852,0.366
c0.95,0,1.895-0.279,2.706-0.822c1.349-0.902,2.158-2.418,2.158-4.041l0.019-261.017c0-1.992-1.215-3.783-3.066-4.519
L85.019,42.899c-1.496-0.596-3.193-0.411-4.527,0.494c-1.334,0.906-2.133,2.413-2.133,4.025v292.197
C78.359,342.264,80.479,344.425,83.127,344.477z"
/>
<path
d="M480.244,89.256c-1.231-0.917-2.824-1.198-4.297-0.759l-49.025,14.657
c-2.06,0.616-3.471,2.51-3.471,4.659v252.151c0,0,0.218,3.978-3.97,3.978c-4.796,0-7.946,0-7.946,0
c-39.549,0-113.045,4.105-160.93,31.6l-9.504,5.442l-9.503-5.442c-47.886-27.494-121.381-31.6-160.93-31.6c0,0-8.099,0-10.142,0
c-1.891,0-1.775-2.272-1.775-2.271V107.813c0-2.149-1.411-4.043-3.47-4.659L6.256,88.497c-1.473-0.439-3.066-0.158-4.298,0.759
S0,91.619,0,93.155v305.069c0,1.372,0.581,2.681,1.597,3.604c1.017,0.921,2.375,1.372,3.741,1.236
c14.571-1.429,37.351-3.131,63.124-3.131c56.606,0,102.097,8.266,131.576,23.913c4.331,2.272,29.441,15.803,41.065,15.803
c11.624,0,36.733-13.53,41.063-15.803c29.48-15.647,74.971-23.913,131.577-23.913c25.771,0,48.553,1.702,63.123,3.131
c1.367,0.136,2.725-0.315,3.742-1.236c1.016-0.923,1.596-2.231,1.596-3.604V93.155C482.203,91.619,481.476,90.173,480.244,89.256z
"
/>
<path
d="M257.679,367.634c0.812,0.543,1.757,0.822,2.706,0.822c0.626,0,1.256-0.122,1.853-0.366
c34.917-14.386,82.235-22.551,136.837-23.613c2.648-0.052,4.769-2.213,4.769-4.861V47.418c0-1.613-0.799-3.12-2.133-4.025
c-1.334-0.904-3.031-1.09-4.528-0.494L258.569,98.057c-1.851,0.736-3.065,2.527-3.065,4.519l0.019,261.017
C255.521,365.216,256.331,366.732,257.679,367.634z"
/>
</svg>
<svg v-else viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M19 2H6c-1.206 0-3 .799-3 3v14c0 2.201 1.794 3 3 3h15v-2H6.012C5.55 19.988 5 19.806 5 19c0-.101.009-.191.024-.273.112-.576.584-.717.988-.727H21V4a2 2 0 0 0-2-2zm0 9-2-1-2 1V4h4v7z" /></svg> -->
</div>
</button>
</template>
<script>
export default {
props: {
isRead: Boolean,
disabled: Boolean
},
data() {
return {}
},
computed: {},
methods: {
clickBtn(e) {
if (this.disabled) {
e.preventDefault()
return
}
this.$emit('click')
e.stopPropagation()
}
},
mounted() {}
}
</script>
<style>
button.icon-btn::before {
content: '';
position: absolute;
border-radius: 6px;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0);
transition: all 0.1s ease-in-out;
}
button.icon-btn:hover:not(:disabled)::before {
background-color: rgba(255, 255, 255, 0.1);
}
button.icon-btn:disabled::before {
background-color: rgba(0, 0, 0, 0.2);
}
</style>

View file

@ -22,8 +22,19 @@ export default {
isShowing: false
}
},
watch: {
text() {
this.updateText()
}
},
methods: {
updateText() {
if (this.tooltip) {
this.tooltip.innerHTML = this.text
}
},
createTooltip() {
if (!this.$refs.box) return
var boxChow = this.$refs.box.getBoundingClientRect()
var top = 0
var left = 0
@ -33,6 +44,9 @@ export default {
} else if (this.direction === 'bottom') {
top = boxChow.top + boxChow.height + 4
left = boxChow.left
} else if (this.direction === 'top') {
top = boxChow.top - 24
left = boxChow.left
}
var tooltip = document.createElement('div')
tooltip.className = 'absolute px-2 bg-black bg-opacity-90 py-1 text-white pointer-events-none text-xs rounded shadow-lg'