mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2025-08-04 18:15:01 +02:00
Check app version, fix close stream bug, show audiobook progress, add toasts
This commit is contained in:
parent
98076927ff
commit
619b6f3686
51 changed files with 551 additions and 141 deletions
|
@ -16,8 +16,8 @@
|
|||
<div class="cursor-pointer flex items-center justify-center text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="forward10">
|
||||
<span class="material-icons text-3xl">forward_10</span>
|
||||
</div>
|
||||
<div class="cursor-pointer flex items-center justify-center text-gray-300 ml-8" @mousedown.prevent @mouseup.prevent>
|
||||
<span class="font-mono text-lg uppercase">1x</span>
|
||||
<div class="cursor-pointer flex items-center justify-center text-gray-300 ml-7 w-10 text-center" @mousedown.prevent @mouseup.prevent @click="$emit('selectPlaybackSpeed')">
|
||||
<span class="font-mono text-lg">{{ playbackRate }}x</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
|
@ -58,8 +58,9 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
totalDuration: 0,
|
||||
currentPlaybackRate: 1,
|
||||
currentTime: 0,
|
||||
isTerminated: false,
|
||||
isResetting: false,
|
||||
initObject: null,
|
||||
stateName: 'idle',
|
||||
playInterval: null,
|
||||
|
@ -77,17 +78,24 @@ export default {
|
|||
computed: {
|
||||
totalDurationPretty() {
|
||||
return this.$secondsToTimestamp(this.totalDuration)
|
||||
},
|
||||
playbackRate() {
|
||||
return this.$store.getters['user/getUserSetting']('playbackRate')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updatePlaybackRate() {
|
||||
this.currentPlaybackRate = this.playbackRate
|
||||
MyNativeAudio.setPlaybackSpeed({ speed: this.playbackRate })
|
||||
},
|
||||
restart() {
|
||||
this.seek(0)
|
||||
},
|
||||
backward10() {
|
||||
MyNativeAudio.seekBackward10()
|
||||
MyNativeAudio.seekBackward({ amount: '10000' })
|
||||
},
|
||||
forward10() {
|
||||
MyNativeAudio.seekForward10()
|
||||
MyNativeAudio.seekForward({ amount: '10000' })
|
||||
},
|
||||
sendStreamUpdate() {
|
||||
this.$emit('updateTime', this.currentTime)
|
||||
|
@ -191,7 +199,9 @@ export default {
|
|||
}
|
||||
},
|
||||
set(audiobookStreamData) {
|
||||
this.isResetting = false
|
||||
this.initObject = { ...audiobookStreamData }
|
||||
this.currentPlaybackRate = this.initObject.playbackSpeed
|
||||
MyNativeAudio.initPlayer(this.initObject)
|
||||
},
|
||||
setFromObj() {
|
||||
|
@ -199,6 +209,7 @@ export default {
|
|||
console.error('Cannot set from obj')
|
||||
return
|
||||
}
|
||||
this.isResetting = false
|
||||
MyNativeAudio.initPlayer(this.initObject)
|
||||
},
|
||||
play() {
|
||||
|
@ -220,13 +231,17 @@ export default {
|
|||
stopPlayInterval() {
|
||||
clearInterval(this.playInterval)
|
||||
},
|
||||
terminateStream(startTime) {
|
||||
resetStream(startTime) {
|
||||
var _time = String(Math.floor(startTime * 1000))
|
||||
if (!this.initObject) {
|
||||
console.error('Terminate stream when no init object is set...')
|
||||
return
|
||||
}
|
||||
this.isResetting = true
|
||||
this.initObject.currentTime = _time
|
||||
this.terminateStream()
|
||||
},
|
||||
terminateStream() {
|
||||
MyNativeAudio.terminateStream()
|
||||
},
|
||||
init() {
|
||||
|
@ -245,7 +260,7 @@ export default {
|
|||
this.currentTime = Number((data.currentTime / 1000).toFixed(2))
|
||||
this.stateName = data.stateName
|
||||
|
||||
if (this.stateName === 'ended' && this.isTerminated) {
|
||||
if (this.stateName === 'ended' && this.isResetting) {
|
||||
this.setFromObj()
|
||||
}
|
||||
|
||||
|
|
|
@ -3,11 +3,12 @@
|
|||
<template v-for="(shelf, index) in groupedBooks">
|
||||
<div :key="index" class="border-b border-opacity-10 w-full bookshelfRow py-4 flex justify-around relative">
|
||||
<template v-for="audiobook in shelf">
|
||||
<div :key="audiobook.id" class="relative px-4">
|
||||
<nuxt-link :to="`/audiobook/${audiobook.id}`">
|
||||
<!-- <div :key="audiobook.id" class="relative px-4"> -->
|
||||
<cards-book-card :key="audiobook.id" :audiobook="audiobook" :width="cardWidth" :user-progress="userAudiobooks[audiobook.id]" />
|
||||
<!-- <nuxt-link :to="`/audiobook/${audiobook.id}`">
|
||||
<cards-book-cover :audiobook="audiobook" :width="cardWidth" class="mx-auto -mb-px" style="box-shadow: 4px 1px 8px #11111166, -4px 1px 8px #11111166, 1px -4px 8px #11111166" />
|
||||
</nuxt-link>
|
||||
</div>
|
||||
</nuxt-link> -->
|
||||
<!-- </div> -->
|
||||
</template>
|
||||
<div class="bookshelfDivider h-4 w-full absolute bottom-0 left-0 right-0 z-10" />
|
||||
</div>
|
||||
|
@ -40,6 +41,9 @@ export default {
|
|||
},
|
||||
hasFilters() {
|
||||
return this.$store.getters['user/getUserSetting']('filterBy') !== 'all'
|
||||
},
|
||||
userAudiobooks() {
|
||||
return this.$store.state.user.user ? this.$store.state.user.user.audiobooks || {} : {}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
|
|
@ -12,8 +12,9 @@
|
|||
<div class="absolute left-2 -top-10">
|
||||
<cards-book-cover :audiobook="streamAudiobook" :width="64" />
|
||||
</div>
|
||||
<audio-player-mini ref="audioPlayerMini" :loading="!stream || currStreamAudiobookId !== streamAudiobookId" @updateTime="updateTime" @hook:mounted="audioPlayerMounted" />
|
||||
<audio-player-mini ref="audioPlayerMini" :loading="!stream || currStreamAudiobookId !== streamAudiobookId" @updateTime="updateTime" @selectPlaybackSpeed="showPlaybackSpeedModal = true" @hook:mounted="audioPlayerMounted" />
|
||||
</div>
|
||||
<modals-playback-speed-modal v-model="showPlaybackSpeedModal" :playback-speed.sync="playbackSpeed" @change="changePlaybackSpeed" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -25,7 +26,9 @@ export default {
|
|||
return {
|
||||
audioPlayerReady: false,
|
||||
stream: null,
|
||||
lastServerUpdateSentSeconds: 0
|
||||
lastServerUpdateSentSeconds: 0,
|
||||
showPlaybackSpeedModal: false,
|
||||
playbackSpeed: 1
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -85,6 +88,10 @@ export default {
|
|||
})
|
||||
if (value) {
|
||||
this.$server.socket.emit('close_stream')
|
||||
this.$store.commit('setStreamAudiobook', null)
|
||||
if (this.$refs.audioPlayerMini) {
|
||||
this.$refs.audioPlayerMini.terminateStream()
|
||||
}
|
||||
}
|
||||
},
|
||||
updateTime(currentTime) {
|
||||
|
@ -122,7 +129,7 @@ export default {
|
|||
streamReset({ streamId, startTime }) {
|
||||
if (this.$refs.audioPlayerMini) {
|
||||
if (this.stream && this.stream.id === streamId) {
|
||||
this.$refs.audioPlayerMini.terminateStream(startTime)
|
||||
this.$refs.audioPlayerMini.resetStream(startTime)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -133,10 +140,6 @@ export default {
|
|||
return
|
||||
}
|
||||
|
||||
if (this.stream && this.stream.id !== stream.id) {
|
||||
console.error('STREAM CHANGED', this.stream.id, stream.id)
|
||||
}
|
||||
|
||||
this.stream = stream
|
||||
|
||||
var playlistUrl = stream.clientPlaylistUri
|
||||
|
@ -149,6 +152,7 @@ export default {
|
|||
author: this.author,
|
||||
playWhenReady: !!playOnLoad,
|
||||
startTime: String(Math.floor(currentTime * 1000)),
|
||||
playbackSpeed: this.playbackSpeed || 1,
|
||||
cover: this.coverForNative,
|
||||
duration: String(Math.floor(this.duration * 1000)),
|
||||
series: this.seriesTxt,
|
||||
|
@ -156,8 +160,6 @@ export default {
|
|||
token: this.$store.getters['user/getToken']
|
||||
}
|
||||
|
||||
console.log('audiobook stream data', audiobookStreamData.token, JSON.stringify(audiobookStreamData))
|
||||
|
||||
this.$refs.audioPlayerMini.set(audiobookStreamData)
|
||||
},
|
||||
audioPlayerMounted() {
|
||||
|
@ -177,11 +179,31 @@ export default {
|
|||
this.$server.socket.on('stream_progress', this.streamProgress)
|
||||
this.$server.socket.on('stream_ready', this.streamReady)
|
||||
this.$server.socket.on('stream_reset', this.streamReset)
|
||||
},
|
||||
changePlaybackSpeed(speed) {
|
||||
this.$store.dispatch('user/updateUserSettings', { playbackRate: speed })
|
||||
},
|
||||
settingsUpdated(settings) {
|
||||
if (this.$refs.audioPlayerMini && this.$refs.audioPlayerMini.currentPlaybackRate !== settings.playbackRate) {
|
||||
this.playbackSpeed = settings.playbackRate
|
||||
this.$refs.audioPlayerMini.updatePlaybackRate()
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
console.warn('Stream Container Mounted')
|
||||
this.playbackSpeed = this.$store.getters['user/getUserSetting']('playbackRate')
|
||||
|
||||
this.setListeners()
|
||||
this.$store.commit('user/addSettingsListener', { id: 'streamContainer', meth: this.settingsUpdated })
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.$server.socket.off('stream_open', this.streamOpen)
|
||||
this.$server.socket.off('stream_closed', this.streamClosed)
|
||||
this.$server.socket.off('stream_progress', this.streamProgress)
|
||||
this.$server.socket.off('stream_ready', this.streamReady)
|
||||
this.$server.socket.off('stream_reset', this.streamReset)
|
||||
|
||||
this.$store.commit('user/removeSettingsListener', 'streamContainer')
|
||||
}
|
||||
}
|
||||
</script>
|
118
components/cards/BookCard.vue
Normal file
118
components/cards/BookCard.vue
Normal file
|
@ -0,0 +1,118 @@
|
|||
<template>
|
||||
<div class="relative">
|
||||
<!-- New Book Flag -->
|
||||
<div v-if="isNew" class="absolute top-4 left-0 w-4 h-10 pr-2 bg-darkgreen box-shadow-xl">
|
||||
<div class="absolute top-0 left-0 w-full h-full transform -rotate-90 flex items-center justify-center">
|
||||
<p class="text-center text-sm">New</p>
|
||||
</div>
|
||||
<div class="absolute -bottom-4 left-0 triangle-right" />
|
||||
</div>
|
||||
|
||||
<div class="rounded-sm h-full overflow-hidden relative box-shadow-book">
|
||||
<nuxt-link :to="`/audiobook/${audiobookId}`" class="cursor-pointer">
|
||||
<div class="w-full relative" :style="{ height: height + 'px' }">
|
||||
<cards-book-cover :audiobook="audiobook" :author-override="authorFormat" :width="width" />
|
||||
|
||||
<div class="absolute bottom-0 left-0 h-1.5 bg-yellow-400 shadow-sm" :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">
|
||||
<span class="material-icons text-red-100 pr-1" :style="{ fontSize: 0.875 * sizeMultiplier + 'rem' }">priority_high</span>
|
||||
</div>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
</nuxt-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
audiobook: {
|
||||
type: Object,
|
||||
default: () => null
|
||||
},
|
||||
userProgress: {
|
||||
type: Object,
|
||||
default: () => null
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
default: 140
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
isNew() {
|
||||
return this.tags.includes('new')
|
||||
},
|
||||
tags() {
|
||||
return this.audiobook.tags || []
|
||||
},
|
||||
audiobookId() {
|
||||
return this.audiobook.id
|
||||
},
|
||||
book() {
|
||||
return this.audiobook.book || {}
|
||||
},
|
||||
height() {
|
||||
return this.width * 1.6
|
||||
},
|
||||
sizeMultiplier() {
|
||||
return this.width / 120
|
||||
},
|
||||
paddingX() {
|
||||
return 16 * this.sizeMultiplier
|
||||
},
|
||||
author() {
|
||||
return this.book.author
|
||||
},
|
||||
authorFL() {
|
||||
return this.book.authorFL || this.author
|
||||
},
|
||||
authorLF() {
|
||||
return this.book.authorLF || this.author
|
||||
},
|
||||
authorFormat() {
|
||||
if (!this.orderBy || !this.orderBy.startsWith('book.author')) return null
|
||||
return this.orderBy === 'book.authorLF' ? this.authorLF : this.authorFL
|
||||
},
|
||||
orderBy() {
|
||||
return this.$store.getters['user/getUserSetting']('orderBy')
|
||||
},
|
||||
userProgressPercent() {
|
||||
return this.userProgress ? this.userProgress.progress || 0 : 0
|
||||
},
|
||||
showError() {
|
||||
return this.hasMissingParts || this.hasInvalidParts
|
||||
},
|
||||
hasMissingParts() {
|
||||
return this.audiobook.hasMissingParts
|
||||
},
|
||||
hasInvalidParts() {
|
||||
return this.audiobook.hasInvalidParts
|
||||
},
|
||||
errorText() {
|
||||
var txt = ''
|
||||
if (this.hasMissingParts) {
|
||||
txt = `${this.hasMissingParts} missing parts.`
|
||||
}
|
||||
if (this.hasInvalidParts) {
|
||||
if (this.hasMissingParts) txt += ' '
|
||||
txt += `${this.hasInvalidParts} invalid parts.`
|
||||
}
|
||||
return txt || 'Unknown Error'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
play() {
|
||||
this.$store.commit('setStreamAudiobook', this.audiobook)
|
||||
this.$root.socket.emit('open_stream', this.audiobookId)
|
||||
}
|
||||
},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
|
@ -6,7 +6,7 @@
|
|||
<span class="material-icons text-4xl">close</span>
|
||||
</div>
|
||||
<slot name="outer" />
|
||||
<div ref="content" style="min-width: 90%; min-height: 200px" class="relative text-white max-h-screen" :style="{ height: modalHeight, width: modalWidth }" v-click-outside="clickBg">
|
||||
<div ref="content" style="max-width: 90%; min-height: 200px" class="relative text-white max-h-screen" :style="{ height: modalHeight, width: modalWidth }" v-click-outside="clickBg">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
|
62
components/modals/PlaybackSpeedModal.vue
Normal file
62
components/modals/PlaybackSpeedModal.vue
Normal file
|
@ -0,0 +1,62 @@
|
|||
<template>
|
||||
<modals-modal v-model="show" :width="200" height="100%">
|
||||
<div class="w-full h-full overflow-hidden absolute top-0 left-0 flex items-center justify-center" @click="show = false">
|
||||
<div class="w-full overflow-x-hidden overflow-y-auto bg-primary rounded-lg border border-white border-opacity-20" style="max-height: 75%" @click.stop>
|
||||
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
||||
<template v-for="rate in rates">
|
||||
<li :key="rate" class="text-gray-50 select-none relative py-4 pr-9 cursor-pointer hover:bg-black-400" :class="rate === selected ? 'bg-bg bg-opacity-50' : ''" role="option" @click="clickedOption(rate)">
|
||||
<div class="flex items-center justify-center">
|
||||
<span class="font-normal ml-3 block truncate text-lg">{{ rate }}x</span>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</modals-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
value: Boolean,
|
||||
playbackSpeed: Number
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
show: {
|
||||
get() {
|
||||
return this.value
|
||||
},
|
||||
set(val) {
|
||||
this.$emit('input', val)
|
||||
}
|
||||
},
|
||||
selected: {
|
||||
get() {
|
||||
return this.playbackSpeed
|
||||
},
|
||||
set(val) {
|
||||
this.$emit('update:playbackSpeed', val)
|
||||
}
|
||||
},
|
||||
rates() {
|
||||
return [0.25, 0.5, 0.8, 1, 1.3, 1.5, 2, 2.5, 3]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
clickedOption(speed) {
|
||||
if (this.selected === speed) {
|
||||
this.show = false
|
||||
return
|
||||
}
|
||||
this.selected = speed
|
||||
this.show = false
|
||||
this.$nextTick(() => this.$emit('change', speed))
|
||||
}
|
||||
},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<button class="btn outline-none rounded-md shadow-md relative border border-gray-600" :disabled="loading" :type="type" :class="classList" @click="click">
|
||||
<button class="btn outline-none rounded-md shadow-md relative border border-gray-600" :disabled="disabled || loading" :type="type" :class="classList" @click="click">
|
||||
<slot />
|
||||
<div v-if="loading" class="text-white absolute top-0 left-0 w-full h-full flex items-center justify-center text-opacity-100">
|
||||
<!-- <span class="material-icons animate-spin">refresh</span> -->
|
||||
|
@ -23,7 +23,8 @@ export default {
|
|||
},
|
||||
paddingX: Number,
|
||||
small: Boolean,
|
||||
loading: Boolean
|
||||
loading: Boolean,
|
||||
disabled: Boolean
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue