mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2025-07-10 05:54:47 +02:00
Add: Bookmarks, Fix: Playback rate updating, Fix: Sleep timer countdown, Fix: Prev chapter btn, Change: Loading indicator for new audio player, Fix: UI alignment issues #26
This commit is contained in:
parent
08195af0dd
commit
65706a52fc
23 changed files with 600 additions and 53 deletions
|
@ -211,6 +211,13 @@ class Server extends EventEmitter {
|
||||||
this.emit('currentUserAudiobookUpdate', payload)
|
this.emit('currentUserAudiobookUpdate', payload)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.socket.on('show_error_toast', (payload) => {
|
||||||
|
this.emit('show_error_toast', payload)
|
||||||
|
})
|
||||||
|
this.socket.on('show_success_toast', (payload) => {
|
||||||
|
this.emit('show_success_toast', payload)
|
||||||
|
})
|
||||||
|
|
||||||
this.socket.onAny((evt, args) => {
|
this.socket.onAny((evt, args) => {
|
||||||
console.log(`[SOCKET] ${this.socket.id}: ${evt} ${JSON.stringify(args)}`)
|
console.log(`[SOCKET] ${this.socket.id}: ${evt} ${JSON.stringify(args)}`)
|
||||||
})
|
})
|
||||||
|
|
|
@ -13,8 +13,8 @@ android {
|
||||||
applicationId "com.audiobookshelf.app"
|
applicationId "com.audiobookshelf.app"
|
||||||
minSdkVersion rootProject.ext.minSdkVersion
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
versionCode 33
|
versionCode 35
|
||||||
versionName "0.9.16-beta"
|
versionName "0.9.17-beta"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
aaptOptions {
|
aaptOptions {
|
||||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
<!-- <p class="text-lg font-book leading-4">AudioBookshelf</p> -->
|
<!-- <p class="text-lg font-book leading-4">AudioBookshelf</p> -->
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
|
|
||||||
<!-- <ui-menu :label="username" :items="menuItems" @action="menuAction" class="ml-5" /> -->
|
<!-- <ui-menu :label="username" :items="menuItems" @action="menuAction" class="ml-5" /> -->
|
||||||
|
|
||||||
<span class="material-icons cursor-pointer mx-4" :class="hasDownloadsFolder ? '' : 'text-warning'" @click="$store.commit('downloads/setShowModal', true)">source</span>
|
<span class="material-icons cursor-pointer mx-4" :class="hasDownloadsFolder ? '' : 'text-warning'" @click="$store.commit('downloads/setShowModal', true)">source</span>
|
||||||
|
|
|
@ -4,8 +4,8 @@
|
||||||
<div class="top-2 left-4 absolute cursor-pointer">
|
<div class="top-2 left-4 absolute cursor-pointer">
|
||||||
<span class="material-icons text-5xl" @click="collapseFullscreen">expand_more</span>
|
<span class="material-icons text-5xl" @click="collapseFullscreen">expand_more</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="top-2 right-4 absolute cursor-pointer">
|
<div class="top-3 right-4 absolute cursor-pointer">
|
||||||
<span class="material-icons text-3xl" @click="$emit('close')">close</span>
|
<span class="material-icons text-4xl" @click="$emit('close')">close</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -23,13 +23,13 @@
|
||||||
<div id="streamContainer" class="w-full z-20 bg-primary absolute bottom-0 left-0 right-0 p-2 pointer-events-auto transition-all" @click="clickContainer">
|
<div id="streamContainer" class="w-full z-20 bg-primary absolute bottom-0 left-0 right-0 p-2 pointer-events-auto transition-all" @click="clickContainer">
|
||||||
<div v-if="showFullscreen" class="absolute top-0 left-0 right-0 w-full py-3 mx-auto px-3" style="max-width: 380px">
|
<div v-if="showFullscreen" class="absolute top-0 left-0 right-0 w-full py-3 mx-auto px-3" style="max-width: 380px">
|
||||||
<div class="flex items-center justify-between pointer-events-auto">
|
<div class="flex items-center justify-between pointer-events-auto">
|
||||||
<span class="material-icons text-3xl text-white text-opacity-10 cursor-pointer">bookmark_border</span>
|
<span class="material-icons text-3xl text-white text-opacity-75 cursor-pointer" @click="$emit('showBookmarks')">{{ bookmarks.length ? 'bookmark' : 'bookmark_border' }}</span>
|
||||||
<span class="font-mono text-white text-opacity-75 cursor-pointer" style="font-size: 1.35rem" @click="$emit('selectPlaybackSpeed')">{{ playbackRate }}x</span>
|
<span class="font-mono text-white text-opacity-75 cursor-pointer" style="font-size: 1.35rem" @click="$emit('selectPlaybackSpeed')">{{ currentPlaybackRate }}x</span>
|
||||||
<svg v-if="!sleepTimerRunning" xmlns="http://www.w3.org/2000/svg" class="h-7 w-7 text-white text-opacity-75 cursor-pointer" fill="none" viewBox="0 0 24 24" stroke="currentColor" @click.stop="$emit('showSleepTimer')">
|
<svg v-if="!sleepTimerRunning" xmlns="http://www.w3.org/2000/svg" class="h-7 w-7 text-white text-opacity-75 cursor-pointer" fill="none" viewBox="0 0 24 24" stroke="currentColor" @click.stop="$emit('showSleepTimer')">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
|
||||||
</svg>
|
</svg>
|
||||||
<div v-else class="h-7 w-7 flex items-center justify-around cursor-pointer" @click.stop="$emit('showSleepTimer')">
|
<div v-else class="h-7 w-7 flex items-center justify-around cursor-pointer" @click.stop="$emit('showSleepTimer')">
|
||||||
<p v-if="sleepTimerEndOfChapterTime" class="text-xl font-mono text-warning">-{{ $secondsToTimestamp(Math.floor(sleepTimerEndOfChapterTime / 1000)) }}</p>
|
<p v-if="sleepTimerEndOfChapterTime" class="text-lg font-mono text-warning">-{{ $secondsToTimestamp(timeLeftInChapter) }}</p>
|
||||||
<p v-else class="text-xl font-mono text-success">{{ Math.ceil(sleepTimeoutCurrentTime / 1000 / 60) }}m</p>
|
<p v-else class="text-xl font-mono text-success">{{ Math.ceil(sleepTimeoutCurrentTime / 1000 / 60) }}m</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -39,18 +39,19 @@
|
||||||
|
|
||||||
<div id="playerControls" class="absolute right-0 bottom-0 py-2">
|
<div id="playerControls" class="absolute right-0 bottom-0 py-2">
|
||||||
<div class="flex items-center justify-center">
|
<div class="flex items-center justify-center">
|
||||||
<span v-show="showFullscreen" class="material-icons next-icon text-white text-opacity-75 cursor-pointer" @click.stop="jumpChapterStart">first_page</span>
|
<span v-show="showFullscreen" class="material-icons next-icon text-white text-opacity-75 cursor-pointer" :class="loading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpChapterStart">first_page</span>
|
||||||
<span class="material-icons jump-icon text-white text-opacity-75 cursor-pointer" @click.stop="backward10">replay_10</span>
|
<span class="material-icons jump-icon text-white cursor-pointer" :class="loading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="backward10">replay_10</span>
|
||||||
<div class="play-btn cursor-pointer shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-4" :class="seekLoading ? 'animate-spin' : ''" @mousedown.prevent @mouseup.prevent @click.stop="playPauseClick">
|
<div class="play-btn cursor-pointer shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-4" :class="seekLoading ? 'animate-spin' : ''" @mousedown.prevent @mouseup.prevent @click.stop="playPauseClick">
|
||||||
<span class="material-icons">{{ seekLoading ? 'autorenew' : isPaused ? 'play_arrow' : 'pause' }}</span>
|
<span v-if="!loading" class="material-icons">{{ seekLoading ? 'autorenew' : isPaused ? 'play_arrow' : 'pause' }}</span>
|
||||||
|
<widgets-spinner-icon v-else class="h-8 w-8" />
|
||||||
</div>
|
</div>
|
||||||
<span class="material-icons jump-icon text-white text-opacity-75 cursor-pointer" @click.stop="forward10">forward_10</span>
|
<span class="material-icons jump-icon text-white cursor-pointer" :class="loading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="forward10">forward_10</span>
|
||||||
<span v-show="showFullscreen" class="material-icons next-icon text-white cursor-pointer" :class="nextChapter ? 'text-opacity-75' : 'text-opacity-10'" @click.stop="jumpNextChapter">last_page</span>
|
<span v-show="showFullscreen" class="material-icons next-icon text-white cursor-pointer" :class="nextChapter && !loading ? 'text-opacity-75' : 'text-opacity-10'" @click.stop="jumpNextChapter">last_page</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="playerTrack" class="absolute bottom-0 left-0 w-full px-3">
|
<div id="playerTrack" class="absolute bottom-0 left-0 w-full px-3">
|
||||||
<div ref="track" class="h-2 w-full bg-gray-500 bg-opacity-50 relative" @click.stop="clickTrack">
|
<div ref="track" class="h-2 w-full bg-gray-500 bg-opacity-50 relative" :class="loading ? 'animate-pulse' : ''" @click.stop="clickTrack">
|
||||||
<div ref="readyTrack" class="h-full bg-gray-600 absolute top-0 left-0 pointer-events-none" />
|
<div ref="readyTrack" class="h-full bg-gray-600 absolute top-0 left-0 pointer-events-none" />
|
||||||
<div ref="bufferTrack" class="h-full bg-gray-400 absolute top-0 left-0 pointer-events-none" />
|
<div ref="bufferTrack" class="h-full bg-gray-400 absolute top-0 left-0 pointer-events-none" />
|
||||||
<div ref="playedTrack" class="h-full bg-gray-200 absolute top-0 left-0 pointer-events-none" />
|
<div ref="playedTrack" class="h-full bg-gray-200 absolute top-0 left-0 pointer-events-none" />
|
||||||
|
@ -80,6 +81,10 @@ export default {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => {}
|
default: () => {}
|
||||||
},
|
},
|
||||||
|
bookmarks: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
loading: Boolean,
|
loading: Boolean,
|
||||||
sleepTimerRunning: Boolean,
|
sleepTimerRunning: Boolean,
|
||||||
sleepTimeoutCurrentTime: Number,
|
sleepTimeoutCurrentTime: Number,
|
||||||
|
@ -129,6 +134,10 @@ export default {
|
||||||
if (!this.audiobook || !this.chapters.length) return null
|
if (!this.audiobook || !this.chapters.length) return null
|
||||||
return this.chapters.find((ch) => ch.start <= this.currentTime && ch.end > this.currentTime)
|
return this.chapters.find((ch) => ch.start <= this.currentTime && ch.end > this.currentTime)
|
||||||
},
|
},
|
||||||
|
nextChapter() {
|
||||||
|
if (!this.chapters.length) return
|
||||||
|
return this.chapters.find((c) => c.start >= this.currentTime)
|
||||||
|
},
|
||||||
currentChapterTitle() {
|
currentChapterTitle() {
|
||||||
return this.currentChapter ? this.currentChapter.title : ''
|
return this.currentChapter ? this.currentChapter.title : ''
|
||||||
},
|
},
|
||||||
|
@ -138,12 +147,9 @@ export default {
|
||||||
totalDurationPretty() {
|
totalDurationPretty() {
|
||||||
return this.$secondsToTimestamp(this.totalDuration)
|
return this.$secondsToTimestamp(this.totalDuration)
|
||||||
},
|
},
|
||||||
playbackRate() {
|
timeLeftInChapter() {
|
||||||
return this.$store.getters['user/getUserSetting']('playbackRate')
|
if (!this.currentChapter) return 0
|
||||||
},
|
return this.currentChapter.end - this.currentTime
|
||||||
nextChapter() {
|
|
||||||
if (!this.chapters.length) return
|
|
||||||
return this.chapters.find((c) => c.start >= this.currentTime)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -154,29 +160,44 @@ export default {
|
||||||
this.showFullscreen = false
|
this.showFullscreen = false
|
||||||
},
|
},
|
||||||
jumpNextChapter() {
|
jumpNextChapter() {
|
||||||
|
if (this.loading) return
|
||||||
if (!this.nextChapter) return
|
if (!this.nextChapter) return
|
||||||
this.seek(this.nextChapter.start)
|
this.seek(this.nextChapter.start)
|
||||||
},
|
},
|
||||||
jumpChapterStart() {
|
jumpChapterStart() {
|
||||||
|
if (this.loading) return
|
||||||
if (!this.currentChapter) {
|
if (!this.currentChapter) {
|
||||||
return this.restart()
|
return this.restart()
|
||||||
}
|
}
|
||||||
this.seek(this.currentChapter.start)
|
|
||||||
|
// If 1 second or less into current chapter, then go to previous
|
||||||
|
if (this.currentTime - this.currentChapter.start <= 1) {
|
||||||
|
var currChapterIndex = this.chapters.findIndex((ch) => ch.start <= this.currentTime && ch.end >= this.currentTime)
|
||||||
|
if (currChapterIndex > 0) {
|
||||||
|
var prevChapter = this.chapters[currChapterIndex - 1]
|
||||||
|
this.seek(prevChapter.start)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.seek(this.currentChapter.start)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
showSleepTimerModal() {
|
showSleepTimerModal() {
|
||||||
this.$emit('showSleepTimer')
|
this.$emit('showSleepTimer')
|
||||||
},
|
},
|
||||||
updatePlaybackRate() {
|
setPlaybackSpeed(speed) {
|
||||||
this.currentPlaybackRate = this.playbackRate
|
console.log(`[AudioPlayer] Set Playback Rate: ${speed}`)
|
||||||
MyNativeAudio.setPlaybackSpeed({ speed: this.playbackRate })
|
this.currentPlaybackRate = speed
|
||||||
|
MyNativeAudio.setPlaybackSpeed({ speed: speed })
|
||||||
},
|
},
|
||||||
restart() {
|
restart() {
|
||||||
this.seek(0)
|
this.seek(0)
|
||||||
},
|
},
|
||||||
backward10() {
|
backward10() {
|
||||||
|
if (this.loading) return
|
||||||
MyNativeAudio.seekBackward({ amount: '10000' })
|
MyNativeAudio.seekBackward({ amount: '10000' })
|
||||||
},
|
},
|
||||||
forward10() {
|
forward10() {
|
||||||
|
if (this.loading) return
|
||||||
MyNativeAudio.seekForward({ amount: '10000' })
|
MyNativeAudio.seekForward({ amount: '10000' })
|
||||||
},
|
},
|
||||||
sendStreamUpdate() {
|
sendStreamUpdate() {
|
||||||
|
@ -242,6 +263,7 @@ export default {
|
||||||
this.playedTrackWidth = ptWidth
|
this.playedTrackWidth = ptWidth
|
||||||
},
|
},
|
||||||
seek(time) {
|
seek(time) {
|
||||||
|
if (this.loading) return
|
||||||
if (this.seekLoading) {
|
if (this.seekLoading) {
|
||||||
console.error('Already seek loading', this.seekedTime)
|
console.error('Already seek loading', this.seekedTime)
|
||||||
return
|
return
|
||||||
|
@ -263,6 +285,7 @@ export default {
|
||||||
},
|
},
|
||||||
updateVolume(volume) {},
|
updateVolume(volume) {},
|
||||||
clickTrack(e) {
|
clickTrack(e) {
|
||||||
|
if (this.loading) return
|
||||||
var offsetX = e.offsetX
|
var offsetX = e.offsetX
|
||||||
var perc = offsetX / this.trackWidth
|
var perc = offsetX / this.trackWidth
|
||||||
var time = perc * this.totalDuration
|
var time = perc * this.totalDuration
|
||||||
|
@ -273,6 +296,7 @@ export default {
|
||||||
this.seek(time)
|
this.seek(time)
|
||||||
},
|
},
|
||||||
playPauseClick() {
|
playPauseClick() {
|
||||||
|
if (this.loading) return
|
||||||
if (this.isPaused) {
|
if (this.isPaused) {
|
||||||
console.log('playPause PLAY')
|
console.log('playPause PLAY')
|
||||||
this.play()
|
this.play()
|
||||||
|
@ -331,6 +355,8 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.currentPlaybackRate = this.initObject.playbackSpeed
|
this.currentPlaybackRate = this.initObject.playbackSpeed
|
||||||
|
console.log(`[AudioPlayer] Set Stream Playback Rate: ${this.currentPlaybackRate}`)
|
||||||
|
|
||||||
if (init)
|
if (init)
|
||||||
MyNativeAudio.initPlayer(this.initObject).then((res) => {
|
MyNativeAudio.initPlayer(this.initObject).then((res) => {
|
||||||
if (res && res.success) {
|
if (res && res.success) {
|
||||||
|
@ -559,10 +585,14 @@ export default {
|
||||||
}
|
}
|
||||||
#playerControls .play-btn {
|
#playerControls .play-btn {
|
||||||
transition: all 0.15s cubic-bezier(0.39, 0.575, 0.565, 1);
|
transition: all 0.15s cubic-bezier(0.39, 0.575, 0.565, 1);
|
||||||
transition-property: padding, margin;
|
transition-property: padding, margin, height, width, min-width, min-height;
|
||||||
|
|
||||||
|
height: 40px;
|
||||||
|
width: 40px;
|
||||||
|
min-width: 40px;
|
||||||
|
min-height: 40px;
|
||||||
margin: 0px 14px;
|
margin: 0px 14px;
|
||||||
padding: 8px;
|
/* padding: 8px; */
|
||||||
}
|
}
|
||||||
#playerControls .play-btn .material-icons {
|
#playerControls .play-btn .material-icons {
|
||||||
transition: all 0.15s cubic-bezier(0.39, 0.575, 0.565, 1);
|
transition: all 0.15s cubic-bezier(0.39, 0.575, 0.565, 1);
|
||||||
|
@ -591,7 +621,11 @@ export default {
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
}
|
}
|
||||||
.fullscreen #playerControls .play-btn {
|
.fullscreen #playerControls .play-btn {
|
||||||
padding: 16px;
|
/* padding: 16px; */
|
||||||
|
height: 65px;
|
||||||
|
width: 65px;
|
||||||
|
min-width: 65px;
|
||||||
|
min-height: 65px;
|
||||||
margin: 0px 26px;
|
margin: 0px 26px;
|
||||||
}
|
}
|
||||||
.fullscreen #playerControls .play-btn .material-icons {
|
.fullscreen #playerControls .play-btn .material-icons {
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
:audiobook="audiobook"
|
:audiobook="audiobook"
|
||||||
:download="download"
|
:download="download"
|
||||||
:loading="isLoading"
|
:loading="isLoading"
|
||||||
|
:bookmarks="bookmarks"
|
||||||
:sleep-timer-running="isSleepTimerRunning"
|
:sleep-timer-running="isSleepTimerRunning"
|
||||||
:sleep-timer-end-of-chapter-time="sleepTimerEndOfChapterTime"
|
:sleep-timer-end-of-chapter-time="sleepTimerEndOfChapterTime"
|
||||||
:sleep-timeout-current-time="sleepTimeoutCurrentTime"
|
:sleep-timeout-current-time="sleepTimeoutCurrentTime"
|
||||||
|
@ -14,6 +15,7 @@
|
||||||
@selectPlaybackSpeed="showPlaybackSpeedModal = true"
|
@selectPlaybackSpeed="showPlaybackSpeedModal = true"
|
||||||
@selectChapter="clickChapterBtn"
|
@selectChapter="clickChapterBtn"
|
||||||
@showSleepTimer="showSleepTimer"
|
@showSleepTimer="showSleepTimer"
|
||||||
|
@showBookmarks="showBookmarks"
|
||||||
@hook:mounted="audioPlayerMounted"
|
@hook:mounted="audioPlayerMounted"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -21,6 +23,7 @@
|
||||||
<modals-playback-speed-modal v-model="showPlaybackSpeedModal" :playback-speed.sync="playbackSpeed" @change="changePlaybackSpeed" />
|
<modals-playback-speed-modal v-model="showPlaybackSpeedModal" :playback-speed.sync="playbackSpeed" @change="changePlaybackSpeed" />
|
||||||
<modals-chapters-modal v-model="showChapterModal" :current-chapter="currentChapter" :chapters="chapters" @select="selectChapter" />
|
<modals-chapters-modal v-model="showChapterModal" :current-chapter="currentChapter" :chapters="chapters" @select="selectChapter" />
|
||||||
<modals-sleep-timer-modal v-model="showSleepTimerModal" :current-time="sleepTimeoutCurrentTime" :sleep-timer-running="isSleepTimerRunning" :current-end-of-chapter-time="currentEndOfChapterTime" :end-of-chapter-time-set="sleepTimerEndOfChapterTime" @change="selectSleepTimeout" @cancel="cancelSleepTimer" />
|
<modals-sleep-timer-modal v-model="showSleepTimerModal" :current-time="sleepTimeoutCurrentTime" :sleep-timer-running="isSleepTimerRunning" :current-end-of-chapter-time="currentEndOfChapterTime" :end-of-chapter-time-set="sleepTimerEndOfChapterTime" @change="selectSleepTimeout" @cancel="cancelSleepTimer" />
|
||||||
|
<modals-bookmarks-modal v-model="showBookmarksModal" :audiobook-id="audiobookId" :bookmarks="bookmarks" :current-time="currentTime" @select="selectBookmark" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -36,6 +39,7 @@ export default {
|
||||||
download: null,
|
download: null,
|
||||||
lastProgressTimeUpdate: 0,
|
lastProgressTimeUpdate: 0,
|
||||||
showPlaybackSpeedModal: false,
|
showPlaybackSpeedModal: false,
|
||||||
|
showBookmarksModal: false,
|
||||||
showSleepTimerModal: false,
|
showSleepTimerModal: false,
|
||||||
playbackSpeed: 1,
|
playbackSpeed: 1,
|
||||||
showChapterModal: false,
|
showChapterModal: false,
|
||||||
|
@ -60,6 +64,14 @@ export default {
|
||||||
userToken() {
|
userToken() {
|
||||||
return this.$store.getters['user/getToken']
|
return this.$store.getters['user/getToken']
|
||||||
},
|
},
|
||||||
|
userAudiobook() {
|
||||||
|
if (!this.audiobookId) return
|
||||||
|
return this.$store.getters['user/getMostRecentUserAudiobookData'](this.audiobookId)
|
||||||
|
},
|
||||||
|
bookmarks() {
|
||||||
|
if (!this.userAudiobook) return []
|
||||||
|
return this.userAudiobook.bookmarks || []
|
||||||
|
},
|
||||||
currentChapter() {
|
currentChapter() {
|
||||||
if (!this.audiobook || !this.chapters.length) return null
|
if (!this.audiobook || !this.chapters.length) return null
|
||||||
return this.chapters.find((ch) => ch.start <= this.currentTime && ch.end > this.currentTime)
|
return this.chapters.find((ch) => ch.start <= this.currentTime && ch.end > this.currentTime)
|
||||||
|
@ -127,6 +139,17 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
showBookmarks() {
|
||||||
|
this.showBookmarksModal = true
|
||||||
|
},
|
||||||
|
selectBookmark(bookmark) {
|
||||||
|
this.showBookmarksModal = false
|
||||||
|
if (!bookmark || isNaN(bookmark.time)) return
|
||||||
|
var bookmarkTime = Number(bookmark.time)
|
||||||
|
if (this.$refs.audioPlayer) {
|
||||||
|
this.$refs.audioPlayer.seek(bookmarkTime)
|
||||||
|
}
|
||||||
|
},
|
||||||
onSleepTimerEnded({ value: currentPosition }) {
|
onSleepTimerEnded({ value: currentPosition }) {
|
||||||
this.isSleepTimerRunning = false
|
this.isSleepTimerRunning = false
|
||||||
if (this.sleepInterval) clearInterval(this.sleepInterval)
|
if (this.sleepInterval) clearInterval(this.sleepInterval)
|
||||||
|
@ -263,7 +286,7 @@ export default {
|
||||||
if (this.$server.connected) {
|
if (this.$server.connected) {
|
||||||
this.$server.socket.emit('progress_update', progressUpdate)
|
this.$server.socket.emit('progress_update', progressUpdate)
|
||||||
}
|
}
|
||||||
this.$localStore.updateUserAudiobookProgress(progressUpdate).then(() => {
|
this.$localStore.updateUserAudiobookData(progressUpdate).then(() => {
|
||||||
console.log('Updated user audiobook progress', currentTime)
|
console.log('Updated user audiobook progress', currentTime)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -405,12 +428,18 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
changePlaybackSpeed(speed) {
|
changePlaybackSpeed(speed) {
|
||||||
|
console.log(`[AudioPlayerContainer] Change Playback Speed: ${speed}`)
|
||||||
|
if (this.$refs.audioPlayer) {
|
||||||
|
this.$refs.audioPlayer.setPlaybackSpeed(speed)
|
||||||
|
}
|
||||||
this.$store.dispatch('user/updateUserSettings', { playbackRate: speed })
|
this.$store.dispatch('user/updateUserSettings', { playbackRate: speed })
|
||||||
},
|
},
|
||||||
settingsUpdated(settings) {
|
settingsUpdated(settings) {
|
||||||
|
console.log(`[AudioPlayerContainer] Settings Update | PlaybackRate: ${settings.playbackRate}`)
|
||||||
|
this.playbackSpeed = settings.playbackRate
|
||||||
if (this.$refs.audioPlayer && this.$refs.audioPlayer.currentPlaybackRate !== settings.playbackRate) {
|
if (this.$refs.audioPlayer && this.$refs.audioPlayer.currentPlaybackRate !== settings.playbackRate) {
|
||||||
this.playbackSpeed = settings.playbackRate
|
console.log(`[AudioPlayerContainer] PlaybackRate Updated: ${this.playbackSpeed}`)
|
||||||
this.$refs.audioPlayer.updatePlaybackRate()
|
this.$refs.audioPlayer.setPlaybackSpeed(this.playbackSpeed)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
streamUpdated(type, data) {
|
streamUpdated(type, data) {
|
||||||
|
@ -441,9 +470,11 @@ export default {
|
||||||
this.onSleepTimerEndedListener = MyNativeAudio.addListener('onSleepTimerEnded', this.onSleepTimerEnded)
|
this.onSleepTimerEndedListener = MyNativeAudio.addListener('onSleepTimerEnded', this.onSleepTimerEnded)
|
||||||
|
|
||||||
this.playbackSpeed = this.$store.getters['user/getUserSetting']('playbackRate')
|
this.playbackSpeed = this.$store.getters['user/getUserSetting']('playbackRate')
|
||||||
|
console.log(`[AudioPlayerContainer] Init Playback Speed: ${this.playbackSpeed}`)
|
||||||
|
|
||||||
this.setListeners()
|
this.setListeners()
|
||||||
this.$store.commit('user/addSettingsListener', { id: 'streamContainer', meth: this.settingsUpdated })
|
this.$store.commit('user/addSettingsListener', { id: 'streamContainer', meth: this.settingsUpdated })
|
||||||
|
// this.$store.commit('user/addUserAudiobookListener', { id: 'streamContainer', meth: this.userAudiobooksUpdated })
|
||||||
this.$store.commit('setStreamListener', this.streamUpdated)
|
this.$store.commit('setStreamListener', this.streamUpdated)
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
|
@ -458,6 +489,7 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$store.commit('user/removeSettingsListener', 'streamContainer')
|
this.$store.commit('user/removeSettingsListener', 'streamContainer')
|
||||||
|
// this.$store.commit('user/removeUserAudiobookListener', 'streamContainer')
|
||||||
this.$store.commit('removeStreamListener')
|
this.$store.commit('removeStreamListener')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -70,7 +70,7 @@ export default {
|
||||||
return this.audiobook.id
|
return this.audiobook.id
|
||||||
},
|
},
|
||||||
mostRecentUserProgress() {
|
mostRecentUserProgress() {
|
||||||
return this.$store.getters['user/getMostRecentAudiobookProgress'](this.audiobookId)
|
return this.$store.getters['user/getMostRecentUserAudiobookData'](this.audiobookId)
|
||||||
},
|
},
|
||||||
userProgressPercent() {
|
userProgressPercent() {
|
||||||
return this.mostRecentUserProgress ? this.mostRecentUserProgress.progress || 0 : 0
|
return this.mostRecentUserProgress ? this.mostRecentUserProgress.progress || 0 : 0
|
||||||
|
|
|
@ -263,7 +263,7 @@ export default {
|
||||||
if (this.$server.connected) {
|
if (this.$server.connected) {
|
||||||
this.$server.socket.emit('progress_update', progressUpdate)
|
this.$server.socket.emit('progress_update', progressUpdate)
|
||||||
}
|
}
|
||||||
this.$localStore.updateUserAudiobookProgress(progressUpdate).then(() => {
|
this.$localStore.updateUserAudiobookData(progressUpdate).then(() => {
|
||||||
console.log('Updated user audiobook progress', currentTime)
|
console.log('Updated user audiobook progress', currentTime)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -82,7 +82,7 @@ export default {
|
||||||
return this.$store.getters['user/getUserSetting']('mobileOrderBy')
|
return this.$store.getters['user/getUserSetting']('mobileOrderBy')
|
||||||
},
|
},
|
||||||
mostRecentUserProgress() {
|
mostRecentUserProgress() {
|
||||||
return this.$store.getters['user/getMostRecentAudiobookProgress'](this.audiobookId)
|
return this.$store.getters['user/getMostRecentUserAudiobookData'](this.audiobookId)
|
||||||
},
|
},
|
||||||
userProgressPercent() {
|
userProgressPercent() {
|
||||||
return this.mostRecentUserProgress ? this.mostRecentUserProgress.progress || 0 : 0
|
return this.mostRecentUserProgress ? this.mostRecentUserProgress.progress || 0 : 0
|
||||||
|
|
137
components/modals/BookmarksModal.vue
Normal file
137
components/modals/BookmarksModal.vue
Normal file
|
@ -0,0 +1,137 @@
|
||||||
|
<template>
|
||||||
|
<modals-modal v-model="show" :width="300" height="100%">
|
||||||
|
<template #outer>
|
||||||
|
<div class="absolute top-5 left-4 z-40">
|
||||||
|
<p class="text-white text-2xl truncate">Bookmarks</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="w-full h-full overflow-hidden absolute top-0 left-0 flex items-center justify-center" @click="show = false">
|
||||||
|
<div ref="container" class="w-full rounded-lg bg-primary border border-white border-opacity-20 overflow-y-auto overflow-x-hidden" style="max-height: 80vh" @click.stop.prevent>
|
||||||
|
<div class="w-full h-full p-4" v-show="showBookmarkTitleInput">
|
||||||
|
<div class="flex mb-4 items-center">
|
||||||
|
<div class="w-9 h-9 flex items-center justify-center rounded-full hover:bg-white hover:bg-opacity-10 cursor-pointer" @click.stop="showBookmarkTitleInput = false">
|
||||||
|
<span class="material-icons text-3xl">arrow_back</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-xl pl-2">{{ selectedBookmark ? 'Edit Bookmark' : 'New Bookmark' }}</p>
|
||||||
|
<div class="flex-grow" />
|
||||||
|
<p class="text-xl font-mono">
|
||||||
|
{{ this.$secondsToTimestamp(currentTime) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ui-text-input-with-label v-model="newBookmarkTitle" label="Note" />
|
||||||
|
<div class="flex justify-end mt-6">
|
||||||
|
<ui-btn color="success" class="w-full" @click.stop="submitBookmark">{{ selectedBookmark ? 'Update' : 'Create' }}</ui-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-full h-full" v-show="!showBookmarkTitleInput">
|
||||||
|
<template v-for="bookmark in bookmarks">
|
||||||
|
<modals-bookmarks-bookmark-item :key="bookmark.id" :highlight="currentTime === bookmark.time" :bookmark="bookmark" @click="clickBookmark" @edit="editBookmark" @delete="deleteBookmark" />
|
||||||
|
</template>
|
||||||
|
<div v-if="!bookmarks.length" class="flex h-32 items-center justify-center">
|
||||||
|
<p class="text-xl">No Bookmarks</p>
|
||||||
|
</div>
|
||||||
|
<div v-show="canCreateBookmark" class="flex px-4 py-2 items-center text-center justify-between border-b border-white border-opacity-10 bg-blue-500 bg-opacity-20 cursor-pointer text-white text-opacity-80 hover:bg-opacity-40 hover:text-opacity-100" @click.stop="createBookmark">
|
||||||
|
<span class="material-icons">add</span>
|
||||||
|
<p class="text-base pl-2">Create Bookmark</p>
|
||||||
|
<p class="text-sm font-mono">
|
||||||
|
{{ this.$secondsToTimestamp(currentTime) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</modals-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
value: Boolean,
|
||||||
|
bookmarks: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
currentTime: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
audiobookId: String
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
selectedBookmark: null,
|
||||||
|
showBookmarkTitleInput: false,
|
||||||
|
newBookmarkTitle: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
show(newVal) {
|
||||||
|
if (newVal) {
|
||||||
|
this.showBookmarkTitleInput = false
|
||||||
|
this.newBookmarkTitle = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
show: {
|
||||||
|
get() {
|
||||||
|
return this.value
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.$emit('input', val)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isConnected() {
|
||||||
|
return this.$store.state.socketConnected
|
||||||
|
},
|
||||||
|
canCreateBookmark() {
|
||||||
|
if (!this.isConnected) return false
|
||||||
|
return !this.bookmarks.find((bm) => bm.time === this.currentTime)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
editBookmark(bm) {
|
||||||
|
this.selectedBookmark = bm
|
||||||
|
this.newBookmarkTitle = bm.title
|
||||||
|
this.showBookmarkTitleInput = true
|
||||||
|
},
|
||||||
|
deleteBookmark(bm) {
|
||||||
|
var bookmark = { ...bm, audiobookId: this.audiobookId }
|
||||||
|
this.$server.socket.emit('delete_bookmark', bookmark)
|
||||||
|
},
|
||||||
|
clickBookmark(bm) {
|
||||||
|
this.$emit('select', bm)
|
||||||
|
},
|
||||||
|
createBookmark() {
|
||||||
|
this.selectedBookmark = null
|
||||||
|
this.newBookmarkTitle = this.$formatDate(Date.now(), 'MMM dd, yyyy HH:mm')
|
||||||
|
this.showBookmarkTitleInput = true
|
||||||
|
},
|
||||||
|
submitBookmark() {
|
||||||
|
console.log(`[BookmarksModal] Submit Bookmark ${this.newBookmarkTitle}/${this.audiobookId}`)
|
||||||
|
if (this.selectedBookmark) {
|
||||||
|
if (this.selectedBookmark.title !== this.newBookmarkTitle) {
|
||||||
|
var bookmark = { ...this.selectedBookmark }
|
||||||
|
bookmark.audiobookId = this.audiobookId
|
||||||
|
bookmark.title = this.newBookmarkTitle
|
||||||
|
console.log(`[BookmarksModal] Update Bookmark ${JSON.stringify(bookmark)}`)
|
||||||
|
this.$server.socket.emit('update_bookmark', bookmark)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var bookmark = {
|
||||||
|
audiobookId: this.audiobookId,
|
||||||
|
title: this.newBookmarkTitle,
|
||||||
|
time: this.currentTime
|
||||||
|
}
|
||||||
|
console.log(`[BookmarksModal] Create Bookmark ${JSON.stringify(bookmark)}`)
|
||||||
|
this.$server.socket.emit('create_bookmark', bookmark)
|
||||||
|
}
|
||||||
|
this.newBookmarkTitle = ''
|
||||||
|
this.showBookmarkTitleInput = false
|
||||||
|
this.show = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<modals-modal v-model="show" :width="300" height="100%">
|
<modals-modal v-model="show" :width="300" height="100%">
|
||||||
<template #outer>
|
<template #outer>
|
||||||
<div v-if="currentChapter" class="absolute top-4 left-4 z-40" style="max-width: 80%">
|
<div v-if="currentChapter" class="absolute top-7 left-4 z-40" style="max-width: 80%">
|
||||||
<p class="text-white text-lg truncate">Current: {{ currentChapterTitle }}</p>
|
<p class="text-white text-lg truncate">Current: {{ currentChapterTitle }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -10,11 +10,13 @@
|
||||||
<div ref="container" 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>
|
<div ref="container" 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">
|
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
||||||
<template v-for="chapter in chapters">
|
<template v-for="chapter in chapters">
|
||||||
<li :key="chapter.id" :id="`chapter-row-${chapter.id}`" class="text-gray-50 select-none relative py-3 cursor-pointer hover:bg-black-400" :class="currentChapterId === chapter.id ? 'bg-bg bg-opacity-80' : ''" role="option" @click="clickedOption(chapter)">
|
<li :key="chapter.id" :id="`chapter-row-${chapter.id}`" class="text-gray-50 select-none relative py-4 cursor-pointer hover:bg-black-400" :class="currentChapterId === chapter.id ? 'bg-bg bg-opacity-80' : ''" role="option" @click="clickedOption(chapter)">
|
||||||
<div class="flex items-center justify-center px-3">
|
<div class="relative flex items-center justify-center pl-2 pr-16">
|
||||||
<span class="font-normal block truncate text-lg">{{ chapter.title }}</span>
|
<p class="font-normal block truncate text-sm text-white text-opacity-80">{{ chapter.title }}</p>
|
||||||
<div class="flex-grow" />
|
<!-- <div class="flex-grow" /> -->
|
||||||
<span class="font-mono text-gray-300">{{ $secondsToTimestamp(chapter.start) }}</span>
|
<div class="absolute top-0 right-2 -mt-0.5">
|
||||||
|
<span class="font-mono text-white text-opacity-90 leading-3" style="letter-spacing: -0.5px">{{ $secondsToTimestamp(chapter.start) }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-show="chapter.id === currentChapterId" class="w-0.5 h-full absolute top-0 left-0 bg-yellow-400" />
|
<div v-show="chapter.id === currentChapterId" class="w-0.5 h-full absolute top-0 left-0 bg-yellow-400" />
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div ref="wrapper" class="modal modal-bg w-full h-full max-h-screen fixed top-0 left-0 bg-primary bg-opacity-75 flex items-center justify-center z-50 opacity-0">
|
<div ref="wrapper" class="modal modal-bg w-full h-full max-h-screen fixed top-0 left-0 bg-primary bg-opacity-75 flex items-center justify-center z-50 opacity-0">
|
||||||
<div class="absolute top-0 left-0 w-full h-36 bg-gradient-to-b from-black to-transparent opacity-70 pointer-events-none" />
|
<div class="absolute top-0 left-0 w-full h-40 bg-gradient-to-b from-black to-transparent opacity-90 pointer-events-none" />
|
||||||
|
|
||||||
<div class="absolute z-40 top-4 right-4 h-12 w-12 flex items-center justify-center cursor-pointer text-white hover:text-gray-300" @click="show = false">
|
<div class="absolute z-40 top-4 right-4 h-12 w-12 flex items-center justify-center cursor-pointer text-white hover:text-gray-300" @click="show = false">
|
||||||
<span class="material-icons text-4xl">close</span>
|
<span class="material-icons text-4xl">close</span>
|
||||||
|
|
|
@ -1,12 +1,18 @@
|
||||||
<template>
|
<template>
|
||||||
<modals-modal v-model="show" :width="200" height="100%">
|
<modals-modal v-model="show" :width="200" height="100%">
|
||||||
|
<template #outer>
|
||||||
|
<div class="absolute top-5 left-4 z-40">
|
||||||
|
<p class="text-white text-2xl truncate">Playback Speed</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<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 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>
|
<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">
|
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
||||||
<template v-for="rate in rates">
|
<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)">
|
<li :key="rate" class="text-gray-50 select-none relative py-4 cursor-pointer hover:bg-black-400" :class="rate === selected ? 'bg-bg bg-opacity-80' : ''" role="option" @click="clickedOption(rate)">
|
||||||
<div class="flex items-center justify-center">
|
<div class="flex items-center justify-center">
|
||||||
<span class="font-normal ml-3 block truncate text-lg">{{ rate }}x</span>
|
<span class="font-normal block truncate text-lg">{{ rate }}x</span>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<modals-modal v-model="show" :width="200" height="100%">
|
<modals-modal v-model="show" :width="200" height="100%">
|
||||||
<template #outer>
|
<template #outer>
|
||||||
<div class="absolute top-4 left-4 z-40">
|
<div class="absolute top-5 left-4 z-40">
|
||||||
<p class="text-white text-2xl truncate">Sleep Timer</p>
|
<p class="text-white text-2xl truncate">Sleep Timer</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
43
components/modals/bookmarks/BookmarkItem.vue
Normal file
43
components/modals/bookmarks/BookmarkItem.vue
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
<template>
|
||||||
|
<div :key="bookmark.id" :id="`bookmark-row-${bookmark.id}`" class="flex items-center px-4 py-4 justify-start cursor-pointer hover:bg-bg relative" :class="highlight ? 'bg-bg bg-opacity-60' : ' bg-opacity-20'" @click="click">
|
||||||
|
<span class="material-icons" :class="highlight ? 'text-success' : 'text-white text-opacity-60'">{{ highlight ? 'bookmark' : 'bookmark_border' }}</span>
|
||||||
|
<div class="flex-grow overflow-hidden">
|
||||||
|
<p class="pl-2 pr-2 truncate">{{ bookmark.title }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="h-full flex items-center w-16 justify-end">
|
||||||
|
<span class="font-mono text-sm text-gray-300">{{ $secondsToTimestamp(bookmark.time) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="h-full flex items-center justify-end transform w-16">
|
||||||
|
<span class="material-icons text-lg mr-2 text-gray-200 hover:text-yellow-400" @click.stop="editClick">edit</span>
|
||||||
|
<span class="material-icons text-lg text-gray-200 hover:text-error cursor-pointer" @click.stop="deleteClick">delete</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
bookmark: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {}
|
||||||
|
},
|
||||||
|
highlight: Boolean
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
computed: {},
|
||||||
|
methods: {
|
||||||
|
click() {
|
||||||
|
this.$emit('click', this.bookmark)
|
||||||
|
},
|
||||||
|
deleteClick() {
|
||||||
|
this.$emit('delete', this.bookmark)
|
||||||
|
},
|
||||||
|
editClick() {
|
||||||
|
this.$emit('edit', this.bookmark)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
235
components/widgets/SpinnerIcon.vue
Normal file
235
components/widgets/SpinnerIcon.vue
Normal file
|
@ -0,0 +1,235 @@
|
||||||
|
<template>
|
||||||
|
<div class="la-ball-spin-clockwise la-dark la-sm">
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
computed: {},
|
||||||
|
methods: {},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/*!
|
||||||
|
* Load Awesome v1.1.0 (http://github.danielcardoso.net/load-awesome/)
|
||||||
|
* Copyright 2015 Daniel Cardoso <@DanielCardoso>
|
||||||
|
* Licensed under MIT
|
||||||
|
*/
|
||||||
|
.la-ball-spin-clockwise,
|
||||||
|
.la-ball-spin-clockwise > div {
|
||||||
|
position: relative;
|
||||||
|
-webkit-box-sizing: border-box;
|
||||||
|
-moz-box-sizing: border-box;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.la-ball-spin-clockwise {
|
||||||
|
display: block;
|
||||||
|
font-size: 0;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.la-ball-spin-clockwise.la-dark {
|
||||||
|
color: #262626;
|
||||||
|
}
|
||||||
|
.la-ball-spin-clockwise > div {
|
||||||
|
display: inline-block;
|
||||||
|
float: none;
|
||||||
|
background-color: currentColor;
|
||||||
|
border: 0 solid currentColor;
|
||||||
|
}
|
||||||
|
.la-ball-spin-clockwise {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
.la-ball-spin-clockwise > div {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
margin-top: -4px;
|
||||||
|
margin-left: -4px;
|
||||||
|
border-radius: 100%;
|
||||||
|
-webkit-animation: ball-spin-clockwise 1s infinite ease-in-out;
|
||||||
|
-moz-animation: ball-spin-clockwise 1s infinite ease-in-out;
|
||||||
|
-o-animation: ball-spin-clockwise 1s infinite ease-in-out;
|
||||||
|
animation: ball-spin-clockwise 1s infinite ease-in-out;
|
||||||
|
}
|
||||||
|
.la-ball-spin-clockwise > div:nth-child(1) {
|
||||||
|
top: 5%;
|
||||||
|
left: 50%;
|
||||||
|
-webkit-animation-delay: -0.875s;
|
||||||
|
-moz-animation-delay: -0.875s;
|
||||||
|
-o-animation-delay: -0.875s;
|
||||||
|
animation-delay: -0.875s;
|
||||||
|
}
|
||||||
|
.la-ball-spin-clockwise > div:nth-child(2) {
|
||||||
|
top: 18.1801948466%;
|
||||||
|
left: 81.8198051534%;
|
||||||
|
-webkit-animation-delay: -0.75s;
|
||||||
|
-moz-animation-delay: -0.75s;
|
||||||
|
-o-animation-delay: -0.75s;
|
||||||
|
animation-delay: -0.75s;
|
||||||
|
}
|
||||||
|
.la-ball-spin-clockwise > div:nth-child(3) {
|
||||||
|
top: 50%;
|
||||||
|
left: 95%;
|
||||||
|
-webkit-animation-delay: -0.625s;
|
||||||
|
-moz-animation-delay: -0.625s;
|
||||||
|
-o-animation-delay: -0.625s;
|
||||||
|
animation-delay: -0.625s;
|
||||||
|
}
|
||||||
|
.la-ball-spin-clockwise > div:nth-child(4) {
|
||||||
|
top: 81.8198051534%;
|
||||||
|
left: 81.8198051534%;
|
||||||
|
-webkit-animation-delay: -0.5s;
|
||||||
|
-moz-animation-delay: -0.5s;
|
||||||
|
-o-animation-delay: -0.5s;
|
||||||
|
animation-delay: -0.5s;
|
||||||
|
}
|
||||||
|
.la-ball-spin-clockwise > div:nth-child(5) {
|
||||||
|
top: 94.9999999966%;
|
||||||
|
left: 50.0000000005%;
|
||||||
|
-webkit-animation-delay: -0.375s;
|
||||||
|
-moz-animation-delay: -0.375s;
|
||||||
|
-o-animation-delay: -0.375s;
|
||||||
|
animation-delay: -0.375s;
|
||||||
|
}
|
||||||
|
.la-ball-spin-clockwise > div:nth-child(6) {
|
||||||
|
top: 81.8198046966%;
|
||||||
|
left: 18.1801949248%;
|
||||||
|
-webkit-animation-delay: -0.25s;
|
||||||
|
-moz-animation-delay: -0.25s;
|
||||||
|
-o-animation-delay: -0.25s;
|
||||||
|
animation-delay: -0.25s;
|
||||||
|
}
|
||||||
|
.la-ball-spin-clockwise > div:nth-child(7) {
|
||||||
|
top: 49.9999750815%;
|
||||||
|
left: 5.0000051215%;
|
||||||
|
-webkit-animation-delay: -0.125s;
|
||||||
|
-moz-animation-delay: -0.125s;
|
||||||
|
-o-animation-delay: -0.125s;
|
||||||
|
animation-delay: -0.125s;
|
||||||
|
}
|
||||||
|
.la-ball-spin-clockwise > div:nth-child(8) {
|
||||||
|
top: 18.179464974%;
|
||||||
|
left: 18.1803700518%;
|
||||||
|
-webkit-animation-delay: 0s;
|
||||||
|
-moz-animation-delay: 0s;
|
||||||
|
-o-animation-delay: 0s;
|
||||||
|
animation-delay: 0s;
|
||||||
|
}
|
||||||
|
.la-ball-spin-clockwise.la-sm {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
.la-ball-spin-clockwise.la-sm > div {
|
||||||
|
width: 4px;
|
||||||
|
height: 4px;
|
||||||
|
margin-top: -2px;
|
||||||
|
margin-left: -2px;
|
||||||
|
}
|
||||||
|
.la-ball-spin-clockwise.la-2x {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
}
|
||||||
|
.la-ball-spin-clockwise.la-2x > div {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
margin-top: -8px;
|
||||||
|
margin-left: -8px;
|
||||||
|
}
|
||||||
|
.la-ball-spin-clockwise.la-3x {
|
||||||
|
width: 96px;
|
||||||
|
height: 96px;
|
||||||
|
}
|
||||||
|
.la-ball-spin-clockwise.la-3x > div {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
margin-top: -12px;
|
||||||
|
margin-left: -12px;
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
* Animation
|
||||||
|
*/
|
||||||
|
@-webkit-keyframes ball-spin-clockwise {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
-webkit-transform: scale(1);
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
20% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
80% {
|
||||||
|
opacity: 0;
|
||||||
|
-webkit-transform: scale(0);
|
||||||
|
transform: scale(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@-moz-keyframes ball-spin-clockwise {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
-moz-transform: scale(1);
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
20% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
80% {
|
||||||
|
opacity: 0;
|
||||||
|
-moz-transform: scale(0);
|
||||||
|
transform: scale(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@-o-keyframes ball-spin-clockwise {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
-o-transform: scale(1);
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
20% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
80% {
|
||||||
|
opacity: 0;
|
||||||
|
-o-transform: scale(0);
|
||||||
|
transform: scale(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes ball-spin-clockwise {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
-webkit-transform: scale(1);
|
||||||
|
-moz-transform: scale(1);
|
||||||
|
-o-transform: scale(1);
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
20% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
80% {
|
||||||
|
opacity: 0;
|
||||||
|
-webkit-transform: scale(0);
|
||||||
|
-moz-transform: scale(0);
|
||||||
|
-o-transform: scale(0);
|
||||||
|
transform: scale(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -96,7 +96,8 @@ export default {
|
||||||
},
|
},
|
||||||
currentUserAudiobookUpdate({ id, data }) {
|
currentUserAudiobookUpdate({ id, data }) {
|
||||||
if (data) {
|
if (data) {
|
||||||
this.$localStore.updateUserAudiobookProgress(data)
|
console.log(`Current User Audiobook Updated ${id} ${JSON.stringify(data)}`)
|
||||||
|
this.$localStore.updateUserAudiobookData(data)
|
||||||
} else {
|
} else {
|
||||||
this.$localStore.removeAudiobookProgress(id)
|
this.$localStore.removeAudiobookProgress(id)
|
||||||
}
|
}
|
||||||
|
@ -413,6 +414,12 @@ export default {
|
||||||
console.log('Network status changed', status.connected, status.connectionType)
|
console.log('Network status changed', status.connected, status.connectionType)
|
||||||
this.$store.commit('setNetworkStatus', status)
|
this.$store.commit('setNetworkStatus', status)
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
showErrorToast(message) {
|
||||||
|
this.$toast.error(message)
|
||||||
|
},
|
||||||
|
showSuccessToast(message) {
|
||||||
|
this.$toast.success(message)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
@ -423,6 +430,8 @@ export default {
|
||||||
this.$server.on('connectionFailed', this.socketConnectionFailed)
|
this.$server.on('connectionFailed', this.socketConnectionFailed)
|
||||||
this.$server.on('initialStream', this.initialStream)
|
this.$server.on('initialStream', this.initialStream)
|
||||||
this.$server.on('currentUserAudiobookUpdate', this.currentUserAudiobookUpdate)
|
this.$server.on('currentUserAudiobookUpdate', this.currentUserAudiobookUpdate)
|
||||||
|
this.$server.on('show_error_toast', this.showErrorToast)
|
||||||
|
this.$server.on('show_success_toast', this.showSuccessToast)
|
||||||
|
|
||||||
if (this.$store.state.isFirstLoad) {
|
if (this.$store.state.isFirstLoad) {
|
||||||
this.$store.commit('setIsFirstLoad', false)
|
this.$store.commit('setIsFirstLoad', false)
|
||||||
|
@ -458,6 +467,8 @@ export default {
|
||||||
this.$server.off('connected', this.connected)
|
this.$server.off('connected', this.connected)
|
||||||
this.$server.off('connectionFailed', this.socketConnectionFailed)
|
this.$server.off('connectionFailed', this.socketConnectionFailed)
|
||||||
this.$server.off('initialStream', this.initialStream)
|
this.$server.off('initialStream', this.initialStream)
|
||||||
|
this.$server.off('show_error_toast', this.showErrorToast)
|
||||||
|
this.$server.off('show_success_toast', this.showSuccessToast)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
7
package-lock.json
generated
7
package-lock.json
generated
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "audiobookshelf-app",
|
"name": "audiobookshelf-app",
|
||||||
"version": "v0.9.6-beta",
|
"version": "v0.9.17-beta",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -5151,6 +5151,11 @@
|
||||||
"type": "^1.0.1"
|
"type": "^1.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"date-fns": {
|
||||||
|
"version": "2.25.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.25.0.tgz",
|
||||||
|
"integrity": "sha512-ovYRFnTrbGPD4nqaEqescPEv1mNwvt+UTqI3Ay9SzNtey9NZnYu6E2qCcBBgJ6/2VF1zGGygpyTDITqpQQ5e+w=="
|
||||||
|
},
|
||||||
"de-indent": {
|
"de-indent": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "audiobookshelf-app",
|
"name": "audiobookshelf-app",
|
||||||
"version": "v0.9.16-beta",
|
"version": "v0.9.17-beta",
|
||||||
"author": "advplyr",
|
"author": "advplyr",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "nuxt --hostname localhost --port 1337",
|
"dev": "nuxt --hostname localhost --port 1337",
|
||||||
|
@ -23,6 +23,7 @@
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
"capacitor-data-storage-sqlite": "^3.2.0",
|
"capacitor-data-storage-sqlite": "^3.2.0",
|
||||||
"core-js": "^3.15.1",
|
"core-js": "^3.15.1",
|
||||||
|
"date-fns": "^2.25.0",
|
||||||
"epubjs": "^0.3.88",
|
"epubjs": "^0.3.88",
|
||||||
"hls.js": "^1.0.9",
|
"hls.js": "^1.0.9",
|
||||||
"libarchive.js": "^1.3.0",
|
"libarchive.js": "^1.3.0",
|
||||||
|
@ -36,4 +37,4 @@
|
||||||
"@nuxtjs/tailwindcss": "^4.2.0",
|
"@nuxtjs/tailwindcss": "^4.2.0",
|
||||||
"postcss": "^8.3.5"
|
"postcss": "^8.3.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -229,7 +229,7 @@ export default {
|
||||||
console.error('Progress reset failed', error)
|
console.error('Progress reset failed', error)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
this.$localStore.updateUserAudiobookProgress({
|
this.$localStore.updateUserAudiobookData({
|
||||||
audiobookId: this.audiobookId,
|
audiobookId: this.audiobookId,
|
||||||
currentTime: 0,
|
currentTime: 0,
|
||||||
totalDuration: this.duration,
|
totalDuration: this.duration,
|
||||||
|
|
|
@ -1,6 +1,17 @@
|
||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
|
import { formatDistance, format } from 'date-fns'
|
||||||
|
|
||||||
Vue.prototype.$isDev = process.env.NODE_ENV !== 'production'
|
Vue.prototype.$isDev = process.env.NODE_ENV !== 'production'
|
||||||
|
|
||||||
|
Vue.prototype.$dateDistanceFromNow = (unixms) => {
|
||||||
|
if (!unixms) return ''
|
||||||
|
return formatDistance(unixms, Date.now(), { addSuffix: true })
|
||||||
|
}
|
||||||
|
Vue.prototype.$formatDate = (unixms, fnsFormat = 'MM/dd/yyyy HH:mm') => {
|
||||||
|
if (!unixms) return ''
|
||||||
|
return format(unixms, fnsFormat)
|
||||||
|
}
|
||||||
|
|
||||||
Vue.prototype.$bytesPretty = (bytes, decimals = 2) => {
|
Vue.prototype.$bytesPretty = (bytes, decimals = 2) => {
|
||||||
if (isNaN(bytes) || bytes === null) return 'Invalid Bytes'
|
if (isNaN(bytes) || bytes === null) return 'Invalid Bytes'
|
||||||
if (bytes === 0) {
|
if (bytes === 0) {
|
||||||
|
|
|
@ -458,15 +458,17 @@ class LocalStorage {
|
||||||
async setAllAudiobookProgress(progresses) {
|
async setAllAudiobookProgress(progresses) {
|
||||||
this.userAudiobooks = progresses
|
this.userAudiobooks = progresses
|
||||||
await this.saveUserAudiobooks()
|
await this.saveUserAudiobooks()
|
||||||
|
|
||||||
this.vuexStore.commit('user/setLocalUserAudiobooks', { ...this.userAudiobooks })
|
this.vuexStore.commit('user/setLocalUserAudiobooks', { ...this.userAudiobooks })
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateUserAudiobookProgress(progressPayload) {
|
async updateUserAudiobookData(progressPayload) {
|
||||||
this.userAudiobooks[progressPayload.audiobookId] = {
|
this.userAudiobooks[progressPayload.audiobookId] = {
|
||||||
...progressPayload
|
...progressPayload
|
||||||
}
|
}
|
||||||
console.log('[LocalStorage] Updated User Audiobook Progress ' + progressPayload.audiobookId)
|
|
||||||
await this.saveUserAudiobooks()
|
await this.saveUserAudiobooks()
|
||||||
|
|
||||||
|
this.vuexStore.commit('user/setUserAudiobooks', { ...this.userAudiobooks })
|
||||||
this.vuexStore.commit('user/setLocalUserAudiobooks', { ...this.userAudiobooks })
|
this.vuexStore.commit('user/setLocalUserAudiobooks', { ...this.userAudiobooks })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -474,6 +476,8 @@ class LocalStorage {
|
||||||
if (!this.userAudiobooks[audiobookId]) return
|
if (!this.userAudiobooks[audiobookId]) return
|
||||||
delete this.userAudiobooks[audiobookId]
|
delete this.userAudiobooks[audiobookId]
|
||||||
await this.saveUserAudiobooks()
|
await this.saveUserAudiobooks()
|
||||||
|
|
||||||
|
this.vuexStore.commit('user/setUserAudiobooks', { ...this.userAudiobooks })
|
||||||
this.vuexStore.commit('user/setLocalUserAudiobooks', { ...this.userAudiobooks })
|
this.vuexStore.commit('user/setLocalUserAudiobooks', { ...this.userAudiobooks })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -53,7 +53,7 @@ export const getters = {
|
||||||
|
|
||||||
if (settings.mobileOrderBy === 'recent') {
|
if (settings.mobileOrderBy === 'recent') {
|
||||||
return sort(filtered)[direction]((ab) => {
|
return sort(filtered)[direction]((ab) => {
|
||||||
var abprogress = rootGetters['user/getMostRecentAudiobookProgress'](ab.id)
|
var abprogress = rootGetters['user/getMostRecentUserAudiobookData'](ab.id)
|
||||||
if (!abprogress) return 0
|
if (!abprogress) return 0
|
||||||
return abprogress.lastUpdate
|
return abprogress.lastUpdate
|
||||||
})
|
})
|
||||||
|
|
|
@ -11,7 +11,8 @@ export const state = () => ({
|
||||||
playbackRate: 1,
|
playbackRate: 1,
|
||||||
bookshelfCoverSize: 120
|
bookshelfCoverSize: 120
|
||||||
},
|
},
|
||||||
settingsListeners: []
|
settingsListeners: [],
|
||||||
|
userAudiobooksListeners: []
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getters = {
|
export const getters = {
|
||||||
|
@ -23,9 +24,9 @@ export const getters = {
|
||||||
return state.user && state.user.audiobooks ? state.user.audiobooks[audiobookId] || null : null
|
return state.user && state.user.audiobooks ? state.user.audiobooks[audiobookId] || null : null
|
||||||
},
|
},
|
||||||
getLocalUserAudiobook: (state) => (audiobookId) => {
|
getLocalUserAudiobook: (state) => (audiobookId) => {
|
||||||
return state.user && state.user.localUserAudiobooks ? state.user.localUserAudiobooks[audiobookId] || null : null
|
return state.localUserAudiobooks ? state.localUserAudiobooks[audiobookId] || null : null
|
||||||
},
|
},
|
||||||
getMostRecentAudiobookProgress: (state, getters) => (audiobookId) => {
|
getMostRecentUserAudiobookData: (state, getters) => (audiobookId) => {
|
||||||
var userAb = getters.getUserAudiobook(audiobookId)
|
var userAb = getters.getUserAudiobook(audiobookId)
|
||||||
var localUserAb = getters.getLocalUserAudiobook(audiobookId)
|
var localUserAb = getters.getLocalUserAudiobook(audiobookId)
|
||||||
if (!localUserAb) return userAb
|
if (!localUserAb) return userAb
|
||||||
|
@ -67,6 +68,15 @@ export const actions = {
|
||||||
export const mutations = {
|
export const mutations = {
|
||||||
setLocalUserAudiobooks(state, userAudiobooks) {
|
setLocalUserAudiobooks(state, userAudiobooks) {
|
||||||
state.localUserAudiobooks = userAudiobooks
|
state.localUserAudiobooks = userAudiobooks
|
||||||
|
state.userAudiobooksListeners.forEach((listener) => {
|
||||||
|
listener.meth()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
setUserAudiobooks(state, userAudiobooks) {
|
||||||
|
if (!state.user) return
|
||||||
|
state.user.audiobooks = {
|
||||||
|
...userAudiobooks
|
||||||
|
}
|
||||||
},
|
},
|
||||||
setUser(state, user) {
|
setUser(state, user) {
|
||||||
state.user = user
|
state.user = user
|
||||||
|
@ -102,5 +112,13 @@ export const mutations = {
|
||||||
},
|
},
|
||||||
removeSettingsListener(state, listenerId) {
|
removeSettingsListener(state, listenerId) {
|
||||||
state.settingsListeners = state.settingsListeners.filter(l => l.id !== listenerId)
|
state.settingsListeners = state.settingsListeners.filter(l => l.id !== listenerId)
|
||||||
|
},
|
||||||
|
addUserAudiobookListener(state, listener) {
|
||||||
|
var index = state.userAudiobooksListeners.findIndex(l => l.id === listener.id)
|
||||||
|
if (index >= 0) state.userAudiobooksListeners.splice(index, 1, listener)
|
||||||
|
else state.userAudiobooksListeners.push(listener)
|
||||||
|
},
|
||||||
|
removeUserAudiobookListener(state, listenerId) {
|
||||||
|
state.userAudiobooksListeners = state.userAudiobooksListeners.filter(l => l.id !== listenerId)
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Add table
Add a link
Reference in a new issue