mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-01-26 00:14:49 +01:00
This commit is contained in:
parent
f9bf846b30
commit
c5eafdfa8a
@ -1,25 +1,29 @@
|
||||
<template>
|
||||
<div class="w-full">
|
||||
<div class="w-full relative mb-4">
|
||||
<div class="absolute left-2 top-0 bottom-0 h-full flex items-center">
|
||||
<p ref="currentTimestamp" class="font-mono text-sm">00:00:00</p>
|
||||
</div>
|
||||
<div class="absolute right-2 top-0 bottom-0 h-full flex items-center">
|
||||
<p class="font-mono text-sm">{{ totalDurationPretty }}</p>
|
||||
<div class="w-full -mt-4">
|
||||
<div class="w-full relative mb-2">
|
||||
<div class="absolute top-0 left-0 w-full h-full bg-red flex items-end pointer-events-none">
|
||||
<p ref="currentTimestamp" class="font-mono text-sm text-gray-100 pointer-events-auto">00:00:00</p>
|
||||
<p class="font-mono text-sm text-gray-100 pointer-events-auto"> / {{ progressPercent }}%</p>
|
||||
<div class="flex-grow" />
|
||||
<p class="font-mono text-sm text-gray-100 pointer-events-auto">{{ timeRemainingPretty }}</p>
|
||||
</div>
|
||||
|
||||
<div class="absolute right-20 top-0 bottom-0">
|
||||
<div v-if="chapters.length" class="cursor-pointer flex items-center justify-center text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="showChapters">
|
||||
<div v-if="chapters.length" class="absolute right-20 top-0 bottom-0 h-full flex items-end">
|
||||
<div class="cursor-pointer text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="showChapters">
|
||||
<span class="material-icons text-3xl">format_list_bulleted</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="absolute right-32 top-0 bottom-0">
|
||||
<div v-if="showExperimentalFeatures" class="absolute top-0 bottom-0 h-full flex items-end" :class="chapters.length ? ' right-32' : 'right-20'">
|
||||
<div class="cursor-pointer text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="showBookmarks">
|
||||
<span class="material-icons text-3xl">{{ bookmarks.length ? 'bookmarks' : 'bookmark_border' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="absolute top-0 bottom-0 h-full flex items-end" :class="!showExperimentalFeatures ? (chapters.length ? ' right-32' : 'right-20') : chapters.length ? ' right-44' : 'right-32'">
|
||||
<controls-volume-control ref="volumeControl" v-model="volume" @input="updateVolume" />
|
||||
</div>
|
||||
<div class="flex my-2">
|
||||
<div class="flex-grow" />
|
||||
|
||||
<div class="flex pb-2">
|
||||
<div class="flex-grow" />
|
||||
<template v-if="!loading">
|
||||
<div class="cursor-pointer flex items-center justify-center text-gray-300 mr-8" @mousedown.prevent @mouseup.prevent @click.stop="restart">
|
||||
<span class="material-icons text-3xl">first_page</span>
|
||||
@ -85,6 +89,10 @@ export default {
|
||||
chapters: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
bookmarks: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@ -116,6 +124,17 @@ export default {
|
||||
totalDurationPretty() {
|
||||
return this.$secondsToTimestamp(this.totalDuration)
|
||||
},
|
||||
timeRemaining() {
|
||||
if (!this.audioEl) return 0
|
||||
return this.totalDuration - this.currentTime
|
||||
},
|
||||
timeRemainingPretty() {
|
||||
return '-' + this.$secondsToTimestamp(this.timeRemaining)
|
||||
},
|
||||
progressPercent() {
|
||||
if (!this.totalDuration) return 0
|
||||
return Math.round((100 * this.currentTime) / this.totalDuration)
|
||||
},
|
||||
chapterTicks() {
|
||||
return this.chapters.map((chap) => {
|
||||
var perc = chap.start / this.totalDuration
|
||||
@ -127,6 +146,9 @@ export default {
|
||||
},
|
||||
currentChapter() {
|
||||
return this.chapters.find((chapter) => chapter.start <= this.currentTime && this.currentTime < chapter.end)
|
||||
},
|
||||
showExperimentalFeatures() {
|
||||
return this.$store.state.showExperimentalFeatures
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@ -134,6 +156,11 @@ export default {
|
||||
this.seek(chapter.start)
|
||||
this.showChaptersModal = false
|
||||
},
|
||||
selectBookmark(bookmark) {
|
||||
if (bookmark) {
|
||||
this.seek(bookmark.time)
|
||||
}
|
||||
},
|
||||
seek(time) {
|
||||
if (this.loading) {
|
||||
return
|
||||
@ -407,6 +434,7 @@ export default {
|
||||
},
|
||||
audioLoadedData() {
|
||||
this.totalDuration = this.audioEl.duration
|
||||
this.$emit('loaded', this.totalDuration)
|
||||
},
|
||||
set(url, currentTime, playOnLoad = false) {
|
||||
if (this.hlsInstance) {
|
||||
@ -462,6 +490,9 @@ export default {
|
||||
if (!this.chapters.length) return
|
||||
this.showChaptersModal = !this.showChaptersModal
|
||||
},
|
||||
showBookmarks() {
|
||||
this.$emit('showBookmarks', this.currentTime)
|
||||
},
|
||||
play() {
|
||||
if (!this.$refs.audio) {
|
||||
console.error('No Audio ref')
|
||||
|
@ -1,20 +1,23 @@
|
||||
<template>
|
||||
<div v-if="streamAudiobook" id="streamContainer" class="w-full fixed bottom-0 left-0 right-0 h-40 z-40 bg-primary p-4">
|
||||
<div v-if="streamAudiobook" id="streamContainer" class="w-full fixed bottom-0 left-0 right-0 h-40 z-40 bg-primary px-4 pb-4 pt-2">
|
||||
<nuxt-link :to="`/audiobook/${streamAudiobook.id}`" class="absolute -top-16 left-4 cursor-pointer">
|
||||
<cards-book-cover :audiobook="streamAudiobook" :width="88" />
|
||||
</nuxt-link>
|
||||
<div class="flex items-center pl-24">
|
||||
<div>
|
||||
<nuxt-link :to="`/audiobook/${streamAudiobook.id}`" class="hover:underline cursor-pointer">
|
||||
<nuxt-link :to="`/audiobook/${streamAudiobook.id}`" class="hover:underline cursor-pointer text-lg">
|
||||
{{ title }} <span v-if="stream && $isDev" class="text-xs text-gray-400">({{ stream.id }})</span>
|
||||
</nuxt-link>
|
||||
<p class="text-gray-400 text-sm hover:underline cursor-pointer" @click="filterByAuthor">by {{ author }}</p>
|
||||
<p class="text-gray-200 text-base hover:underline cursor-pointer" @click="filterByAuthor">by {{ author }}</p>
|
||||
<p class="font-mono text-sm text-gray-200">{{ totalDurationPretty }}</p>
|
||||
</div>
|
||||
<div class="flex-grow" />
|
||||
<span v-if="stream" class="material-icons px-4 cursor-pointer" @click="cancelStream">close</span>
|
||||
</div>
|
||||
|
||||
<audio-player ref="audioPlayer" :chapters="chapters" :loading="isLoading" @close="cancelStream" @updateTime="updateTime" @hook:mounted="audioPlayerMounted" />
|
||||
<audio-player ref="audioPlayer" :chapters="chapters" :loading="isLoading" :bookmarks="bookmarks" @close="cancelStream" @updateTime="updateTime" @loaded="(d) => (totalDuration = d)" @showBookmarks="showBookmarks" @hook:mounted="audioPlayerMounted" />
|
||||
|
||||
<modals-bookmarks-modal v-model="showBookmarksModal" :bookmarks="bookmarks" :audiobook-id="bookmarkAudiobookId" :current-time="bookmarkCurrentTime" @select="selectBookmark" @create="createBookmark" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -24,7 +27,12 @@ export default {
|
||||
return {
|
||||
audioPlayerReady: false,
|
||||
lastServerUpdateSentSeconds: 0,
|
||||
stream: null
|
||||
stream: null,
|
||||
totalDuration: 0,
|
||||
showBookmarksModal: false,
|
||||
bookmarkCurrentTime: 0,
|
||||
bookmarkAudiobookId: null,
|
||||
bookmarkTimeCreating: 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@ -35,6 +43,14 @@ export default {
|
||||
user() {
|
||||
return this.$store.state.user.user
|
||||
},
|
||||
userAudiobook() {
|
||||
if (!this.audiobookId) return
|
||||
return this.$store.getters['user/getUserAudiobook'](this.audiobookId)
|
||||
},
|
||||
bookmarks() {
|
||||
if (!this.userAudiobook) return []
|
||||
return this.userAudiobook.bookmarks || []
|
||||
},
|
||||
isLoading() {
|
||||
if (!this.streamAudiobook) return false
|
||||
if (this.stream) {
|
||||
@ -46,6 +62,9 @@ export default {
|
||||
streamAudiobook() {
|
||||
return this.$store.state.streamAudiobook
|
||||
},
|
||||
audiobookId() {
|
||||
return this.streamAudiobook ? this.streamAudiobook.id : null
|
||||
},
|
||||
book() {
|
||||
return this.streamAudiobook ? this.streamAudiobook.book || {} : {}
|
||||
},
|
||||
@ -66,9 +85,35 @@ export default {
|
||||
},
|
||||
libraryId() {
|
||||
return this.streamAudiobook ? this.streamAudiobook.libraryId : null
|
||||
},
|
||||
totalDurationPretty() {
|
||||
return this.$secondsToTimestamp(this.totalDuration)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showBookmarks(currentTime) {
|
||||
this.bookmarkAudiobookId = this.audiobookId
|
||||
this.bookmarkCurrentTime = currentTime
|
||||
this.showBookmarksModal = true
|
||||
},
|
||||
bookmarkCreated(time) {
|
||||
if (time === this.bookmarkTimeCreating) {
|
||||
this.bookmarkTimeCreating = 0
|
||||
this.$toast.success(`${this.$secondsToTimestamp(time)} Bookmarked`)
|
||||
}
|
||||
},
|
||||
createBookmark(bookmark) {
|
||||
this.bookmarkTimeCreating = bookmark.time
|
||||
this.$root.socket.once('bookmark_created', this.bookmarkCreated)
|
||||
this.$root.socket.emit('create_bookmark', bookmark)
|
||||
this.showBookmarksModal = false
|
||||
},
|
||||
selectBookmark(bookmark) {
|
||||
if (this.$refs.audioPlayer) {
|
||||
this.$refs.audioPlayer.selectBookmark(bookmark)
|
||||
}
|
||||
this.showBookmarksModal = false
|
||||
},
|
||||
filterByAuthor() {
|
||||
if (this.$route.name !== 'index') {
|
||||
this.$router.push(`/library/${this.libraryId || this.$store.state.libraries.currentLibraryId}/bookshelf`)
|
||||
|
105
client/components/modals/BookmarksModal.vue
Normal file
105
client/components/modals/BookmarksModal.vue
Normal file
@ -0,0 +1,105 @@
|
||||
<template>
|
||||
<modals-modal v-model="show" name="bookmarks" :width="500" :height="'unset'">
|
||||
<div ref="container" class="w-full rounded-lg bg-primary box-shadow-md overflow-y-auto overflow-x-hidden" style="max-height: 80vh">
|
||||
<div class="w-full h-full px-6 py-6" 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="showBookmarkTitleInput = false">
|
||||
<span class="material-icons text-3xl">arrow_back</span>
|
||||
</div>
|
||||
<p class="text-xl pl-2">New Bookmark</p>
|
||||
<div class="flex-grow" />
|
||||
<p class="text-xl font-mono">
|
||||
{{ this.$secondsToTimestamp(currentTime) }}
|
||||
</p>
|
||||
</div>
|
||||
<form @submit.prevent="submitBookmark">
|
||||
<ui-text-input-with-label v-model="newBookmarkTitle" label="Note" />
|
||||
<div class="flex justify-end mt-6">
|
||||
<ui-btn color="success" class="w-1/2" type="submit">Create Bookmark</ui-btn>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="w-full h-full" v-show="!showBookmarkTitleInput">
|
||||
<template v-for="bookmark in bookmarks">
|
||||
<div :key="bookmark.id" :id="`bookmark-row-${bookmark.id}`" class="flex items-center px-4 py-4 justify-start cursor-pointer bg-opacity-20 hover:bg-bg relative" @click="clickBookmark(bookmark)">
|
||||
<span class="material-icons text-white text-opacity-60">bookmark_border</span>
|
||||
<p class="pl-2 pr-16 truncate">{{ bookmark.title }}</p>
|
||||
|
||||
<div class="absolute right-0 top-0 h-full flex items-center pr-4">
|
||||
<span class="font-mono text-sm text-gray-300">{{ $secondsToTimestamp(bookmark.time) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="!bookmarks.length" class="flex h-32 items-center justify-center">
|
||||
<p class="text-xl">No Bookmarks</p>
|
||||
</div>
|
||||
<div 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="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>
|
||||
</modals-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
value: Boolean,
|
||||
bookmarks: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
currentTime: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
audiobookId: String
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
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)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
clickBookmark(bm) {
|
||||
this.$emit('select', bm)
|
||||
},
|
||||
createBookmark() {
|
||||
this.showBookmarkTitleInput = true
|
||||
},
|
||||
submitBookmark() {
|
||||
var bookmark = {
|
||||
audiobookId: this.audiobookId,
|
||||
title: this.newBookmarkTitle,
|
||||
time: this.currentTime
|
||||
}
|
||||
this.$emit('create', bookmark)
|
||||
this.newBookmarkTitle = ''
|
||||
this.showBookmarkTitleInput = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "audiobookshelf-client",
|
||||
"version": "1.5.1",
|
||||
"version": "1.5.2",
|
||||
"description": "Audiobook manager and player",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "audiobookshelf",
|
||||
"version": "1.5.1",
|
||||
"version": "1.5.2",
|
||||
"description": "Self-hosted audiobook server for managing and playing audiobooks",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
@ -259,6 +259,9 @@ class Server {
|
||||
socket.on('create_backup', () => this.backupManager.requestCreateBackup(socket))
|
||||
socket.on('apply_backup', (id) => this.backupManager.requestApplyBackup(socket, id))
|
||||
|
||||
// Bookmarks
|
||||
socket.on('create_bookmark', (payload) => this.createBookmark(socket, payload))
|
||||
|
||||
socket.on('test', () => {
|
||||
socket.emit('test_received', socket.id)
|
||||
})
|
||||
@ -466,6 +469,23 @@ class Server {
|
||||
}
|
||||
}
|
||||
|
||||
async createBookmark(socket, payload) {
|
||||
var client = socket.sheepClient
|
||||
if (!client || !client.user) {
|
||||
Logger.error('[Server] createBookmark invalid socket client')
|
||||
return
|
||||
}
|
||||
var userAudiobook = client.user.createBookmark(payload)
|
||||
if (userAudiobook) {
|
||||
await this.db.updateEntity('user', client.user)
|
||||
socket.emit('bookmark_created', payload.time)
|
||||
socket.emit('current_user_audiobook_update', {
|
||||
id: userAudiobook.audiobookId,
|
||||
data: userAudiobook || null
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async authenticateSocket(socket, token) {
|
||||
var user = await this.auth.verifyToken(token)
|
||||
if (!user) {
|
||||
|
32
server/objects/AudioBookmark.js
Normal file
32
server/objects/AudioBookmark.js
Normal file
@ -0,0 +1,32 @@
|
||||
class AudioBookmark {
|
||||
constructor(bookmark) {
|
||||
this.title = null
|
||||
this.time = null
|
||||
this.createdAt = null
|
||||
|
||||
if (bookmark) {
|
||||
this.construct(bookmark)
|
||||
}
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
title: this.title || '',
|
||||
time: this.time,
|
||||
createdAt: this.createdAt
|
||||
}
|
||||
}
|
||||
|
||||
construct(bookmark) {
|
||||
this.title = bookmark.title || ''
|
||||
this.time = bookmark.time || 0
|
||||
this.createdAt = bookmark.createdAt
|
||||
}
|
||||
|
||||
setData(time, title) {
|
||||
this.title = title
|
||||
this.time = time
|
||||
this.createdAt = Date.now()
|
||||
}
|
||||
}
|
||||
module.exports = AudioBookmark
|
@ -1,3 +1,5 @@
|
||||
const AudioBookmark = require('./AudioBookmark')
|
||||
|
||||
class AudiobookProgress {
|
||||
constructor(progress) {
|
||||
this.audiobookId = null
|
||||
@ -10,12 +12,18 @@ class AudiobookProgress {
|
||||
this.lastUpdate = null
|
||||
this.startedAt = null
|
||||
this.finishedAt = null
|
||||
this.bookmarks = []
|
||||
|
||||
if (progress) {
|
||||
this.construct(progress)
|
||||
}
|
||||
}
|
||||
|
||||
bookmarksToJSON() {
|
||||
if (!this.bookmarks) return []
|
||||
return this.bookmarks.map(b => b.toJSON())
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
audiobookId: this.audiobookId,
|
||||
@ -25,7 +33,8 @@ class AudiobookProgress {
|
||||
isRead: this.isRead,
|
||||
lastUpdate: this.lastUpdate,
|
||||
startedAt: this.startedAt,
|
||||
finishedAt: this.finishedAt
|
||||
finishedAt: this.finishedAt,
|
||||
bookmarks: this.bookmarksToJSON()
|
||||
}
|
||||
}
|
||||
|
||||
@ -38,6 +47,11 @@ class AudiobookProgress {
|
||||
this.lastUpdate = progress.lastUpdate
|
||||
this.startedAt = progress.startedAt
|
||||
this.finishedAt = progress.finishedAt || null
|
||||
if (progress.bookmarks) {
|
||||
this.bookmarks = progress.bookmarks.map(b => new AudioBookmark(b))
|
||||
} else {
|
||||
this.bookmarks = []
|
||||
}
|
||||
}
|
||||
|
||||
updateFromStream(stream) {
|
||||
@ -90,5 +104,12 @@ class AudiobookProgress {
|
||||
}
|
||||
return hasUpdates
|
||||
}
|
||||
|
||||
createBookmark(time, title) {
|
||||
var newBookmark = new AudioBookmark()
|
||||
newBookmark.setData(time, title)
|
||||
this.bookmarks.push(newBookmark)
|
||||
return newBookmark
|
||||
}
|
||||
}
|
||||
module.exports = AudiobookProgress
|
@ -272,5 +272,14 @@ class User {
|
||||
getAudiobookJSON(audiobookId) {
|
||||
return this.audiobooks[audiobookId] ? this.audiobooks[audiobookId].toJSON() : null
|
||||
}
|
||||
|
||||
createBookmark({ audiobookId, time, title }) {
|
||||
if (!this.audiobooks || !this.audiobooks[audiobookId]) {
|
||||
return false
|
||||
}
|
||||
var success = this.audiobooks[audiobookId].createBookmark(time, title)
|
||||
if (success) return this.audiobooks[audiobookId]
|
||||
return null
|
||||
}
|
||||
}
|
||||
module.exports = User
|
Loading…
Reference in New Issue
Block a user