mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2024-12-20 19:06:06 +01:00
Add: Experimental bookmarks edit and delete #115
This commit is contained in:
parent
fffa02e7e8
commit
9f66054a72
@ -20,7 +20,7 @@
|
||||
-webkit-font-feature-settings: 'liga';
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
.material-icons:not(.text-sm):not(.text-md):not(.text-base):not(.text-lg):not(.text-xl):not(.text-2xl):not(.text-3xl):not(.text-4xl):not(.text-5xl):not(.text-6xl) {
|
||||
.material-icons:not(.text-xs):not(.text-sm):not(.text-md):not(.text-base):not(.text-lg):not(.text-xl):not(.text-2xl):not(.text-3xl):not(.text-4xl):not(.text-5xl):not(.text-6xl) {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
|
@ -15,7 +15,7 @@
|
||||
</div>
|
||||
<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>
|
||||
<span class="material-icons" style="font-size: 1.7rem">{{ 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'">
|
||||
|
@ -24,7 +24,7 @@
|
||||
|
||||
<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" />
|
||||
<modals-bookmarks-modal v-model="showBookmarksModal" :bookmarks="bookmarks" :audiobook-id="bookmarkAudiobookId" :current-time="bookmarkCurrentTime" @select="selectBookmark" @create="createBookmark" @update="updateBookmark" @delete="deleteBookmark" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -38,8 +38,7 @@ export default {
|
||||
totalDuration: 0,
|
||||
showBookmarksModal: false,
|
||||
bookmarkCurrentTime: 0,
|
||||
bookmarkAudiobookId: null,
|
||||
bookmarkTimeCreating: 0
|
||||
bookmarkAudiobookId: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@ -103,24 +102,38 @@ export default {
|
||||
this.bookmarkCurrentTime = currentTime
|
||||
this.showBookmarksModal = true
|
||||
},
|
||||
bookmarkCreated(time) {
|
||||
if (time === this.bookmarkTimeCreating) {
|
||||
this.bookmarkTimeCreating = 0
|
||||
this.$toast.success(`${this.$secondsToTimestamp(time)} Bookmarked`)
|
||||
}
|
||||
},
|
||||
// bookmarkCreated(time) {
|
||||
// if (time === this.bookmarkTimeProcessing) {
|
||||
// this.bookmarkTimeProcessing = 0
|
||||
// this.$toast.success(`${this.$secondsToTimestamp(time)} Bookmarked`)
|
||||
// }
|
||||
// },
|
||||
createBookmark(bookmark) {
|
||||
this.bookmarkTimeCreating = bookmark.time
|
||||
this.$root.socket.once('bookmark_created', this.bookmarkCreated)
|
||||
// this.bookmarkTimeProcessing = bookmark.time
|
||||
this.$root.socket.emit('create_bookmark', bookmark)
|
||||
this.showBookmarksModal = false
|
||||
},
|
||||
// bookmarkUpdated(time) {
|
||||
// if (time === this.bookmarkTimeProcessing) {
|
||||
// this.bookmarkTimeProcessing = 0
|
||||
// this.$toast.success(`Bookmark @${this.$secondsToTimestamp(time)} Updated`)
|
||||
// }
|
||||
// },
|
||||
updateBookmark(bookmark) {
|
||||
// this.bookmarkTimeProcessing = bookmark.time
|
||||
this.$root.socket.emit('update_bookmark', bookmark)
|
||||
this.showBookmarksModal = false
|
||||
},
|
||||
selectBookmark(bookmark) {
|
||||
if (this.$refs.audioPlayer) {
|
||||
this.$refs.audioPlayer.selectBookmark(bookmark)
|
||||
}
|
||||
this.showBookmarksModal = false
|
||||
},
|
||||
deleteBookmark(bookmark) {
|
||||
this.$root.socket.emit('delete_bookmark', bookmark)
|
||||
this.showBookmarksModal = false
|
||||
},
|
||||
filterByAuthor() {
|
||||
if (this.$route.name !== 'index') {
|
||||
this.$router.push(`/library/${this.libraryId || this.$store.state.libraries.currentLibraryId}/bookshelf`)
|
||||
|
@ -6,7 +6,7 @@
|
||||
<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>
|
||||
<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) }}
|
||||
@ -15,25 +15,18 @@
|
||||
<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>
|
||||
<ui-btn color="success" class="w-1/2" type="submit">{{ selectedBookmark ? 'Update' : '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>
|
||||
<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 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">
|
||||
<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="createBookmark">
|
||||
<span class="material-icons">add</span>
|
||||
<p class="text-base pl-2">Create Bookmark</p>
|
||||
<p class="text-sm font-mono">
|
||||
@ -61,6 +54,7 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedBookmark: null,
|
||||
showBookmarkTitleInput: false,
|
||||
newBookmarkTitle: ''
|
||||
}
|
||||
@ -81,22 +75,45 @@ export default {
|
||||
set(val) {
|
||||
this.$emit('input', val)
|
||||
}
|
||||
},
|
||||
canCreateBookmark() {
|
||||
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.$emit('delete', 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() {
|
||||
var bookmark = {
|
||||
audiobookId: this.audiobookId,
|
||||
title: this.newBookmarkTitle,
|
||||
time: this.currentTime
|
||||
if (this.selectedBookmark) {
|
||||
if (this.selectedBookmark.title !== this.newBookmarkTitle) {
|
||||
var bookmark = { ...this.selectedBookmark }
|
||||
bookmark.audiobookId = this.audiobookId
|
||||
bookmark.title = this.newBookmarkTitle
|
||||
this.$emit('update', bookmark)
|
||||
}
|
||||
} else {
|
||||
var bookmark = {
|
||||
audiobookId: this.audiobookId,
|
||||
title: this.newBookmarkTitle,
|
||||
time: this.currentTime
|
||||
}
|
||||
this.$emit('create', bookmark)
|
||||
}
|
||||
this.$emit('create', bookmark)
|
||||
this.newBookmarkTitle = ''
|
||||
this.showBookmarkTitleInput = false
|
||||
}
|
||||
|
45
client/components/modals/bookmarks/BookmarkItem.vue
Normal file
45
client/components/modals/bookmarks/BookmarkItem.vue
Normal file
@ -0,0 +1,45 @@
|
||||
<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" @mouseover="isHovering = true" @mouseleave="isHovering = false">
|
||||
<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" :class="isHovering ? 'transition-transform translate-0 w-16' : 'translate-x-40 w-0'">
|
||||
<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 {
|
||||
isHovering: false
|
||||
}
|
||||
},
|
||||
computed: {},
|
||||
methods: {
|
||||
click() {
|
||||
this.$emit('click', this.bookmark)
|
||||
},
|
||||
deleteClick() {
|
||||
this.$emit('delete', this.bookmark)
|
||||
},
|
||||
editClick() {
|
||||
this.$emit('edit', this.bookmark)
|
||||
}
|
||||
},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
@ -239,6 +239,12 @@ export default {
|
||||
download.status = this.$constants.DownloadStatus.EXPIRED
|
||||
this.$store.commit('downloads/addUpdateDownload', download)
|
||||
},
|
||||
showErrorToast(message) {
|
||||
this.$toast.error(message)
|
||||
},
|
||||
showSuccessToast(message) {
|
||||
this.$toast.success(message)
|
||||
},
|
||||
logEvtReceived(payload) {
|
||||
this.$store.commit('logs/logEvt', payload)
|
||||
},
|
||||
@ -303,6 +309,10 @@ export default {
|
||||
this.socket.on('download_killed', this.downloadKilled)
|
||||
this.socket.on('download_expired', this.downloadExpired)
|
||||
|
||||
// Toast Listeners
|
||||
this.socket.on('show_error_toast', this.showErrorToast)
|
||||
this.socket.on('show_success_toast', this.showSuccessToast)
|
||||
|
||||
this.socket.on('log', this.logEvtReceived)
|
||||
|
||||
this.socket.on('backup_applied', this.backupApplied)
|
||||
|
@ -11,6 +11,7 @@ const { version } = require('../package.json')
|
||||
// Utils
|
||||
const { ScanResult } = require('./utils/constants')
|
||||
const filePerms = require('./utils/filePerms')
|
||||
const { secondsToTimestamp } = require('./utils/fileUtils')
|
||||
const Logger = require('./Logger')
|
||||
|
||||
// Classes
|
||||
@ -261,6 +262,8 @@ class Server {
|
||||
|
||||
// Bookmarks
|
||||
socket.on('create_bookmark', (payload) => this.createBookmark(socket, payload))
|
||||
socket.on('update_bookmark', (payload) => this.updateBookmark(socket, payload))
|
||||
socket.on('delete_bookmark', (payload) => this.deleteBookmark(socket, payload))
|
||||
|
||||
socket.on('test', () => {
|
||||
socket.emit('test_received', socket.id)
|
||||
@ -481,15 +484,66 @@ class Server {
|
||||
return
|
||||
}
|
||||
var userAudiobook = client.user.createBookmark(payload)
|
||||
if (userAudiobook) {
|
||||
await this.db.updateEntity('user', client.user)
|
||||
|
||||
this.clientEmitter(client.user.id, 'bookmark_created', payload.time)
|
||||
this.clientEmitter(client.user.id, 'current_user_audiobook_update', {
|
||||
id: userAudiobook.audiobookId,
|
||||
data: userAudiobook || null
|
||||
})
|
||||
if (!userAudiobook || userAudiobook.error) {
|
||||
var failMessage = (userAudiobook ? userAudiobook.error : null) || 'Unknown Error'
|
||||
socket.emit('show_error_toast', `Failed to create Bookmark: ${failMessage}`)
|
||||
return
|
||||
}
|
||||
|
||||
await this.db.updateEntity('user', client.user)
|
||||
|
||||
socket.emit('show_success_toast', `${secondsToTimestamp(payload.time)} Bookmarked`)
|
||||
|
||||
this.clientEmitter(client.user.id, 'current_user_audiobook_update', {
|
||||
id: userAudiobook.audiobookId,
|
||||
data: userAudiobook || null
|
||||
})
|
||||
}
|
||||
|
||||
async updateBookmark(socket, payload) {
|
||||
var client = socket.sheepClient
|
||||
if (!client || !client.user) {
|
||||
Logger.error('[Server] updateBookmark invalid socket client')
|
||||
return
|
||||
}
|
||||
var userAudiobook = client.user.updateBookmark(payload)
|
||||
if (!userAudiobook || userAudiobook.error) {
|
||||
var failMessage = (userAudiobook ? userAudiobook.error : null) || 'Unknown Error'
|
||||
socket.emit('show_error_toast', `Failed to update Bookmark: ${failMessage}`)
|
||||
return
|
||||
}
|
||||
|
||||
await this.db.updateEntity('user', client.user)
|
||||
|
||||
socket.emit('show_success_toast', `Bookmark ${secondsToTimestamp(payload.time)} Updated`)
|
||||
|
||||
this.clientEmitter(client.user.id, 'current_user_audiobook_update', {
|
||||
id: userAudiobook.audiobookId,
|
||||
data: userAudiobook || null
|
||||
})
|
||||
}
|
||||
|
||||
async deleteBookmark(socket, payload) {
|
||||
var client = socket.sheepClient
|
||||
if (!client || !client.user) {
|
||||
Logger.error('[Server] deleteBookmark invalid socket client')
|
||||
return
|
||||
}
|
||||
var userAudiobook = client.user.deleteBookmark(payload)
|
||||
if (!userAudiobook || userAudiobook.error) {
|
||||
var failMessage = (userAudiobook ? userAudiobook.error : null) || 'Unknown Error'
|
||||
socket.emit('show_error_toast', `Failed to delete Bookmark: ${failMessage}`)
|
||||
return
|
||||
}
|
||||
|
||||
await this.db.updateEntity('user', client.user)
|
||||
|
||||
socket.emit('show_success_toast', `Bookmark ${secondsToTimestamp(payload.time)} Removed`)
|
||||
|
||||
this.clientEmitter(client.user.id, 'current_user_audiobook_update', {
|
||||
id: userAudiobook.audiobookId,
|
||||
data: userAudiobook || null
|
||||
})
|
||||
}
|
||||
|
||||
async authenticateSocket(socket, token) {
|
||||
|
@ -107,11 +107,26 @@ class AudiobookProgress {
|
||||
return hasUpdates
|
||||
}
|
||||
|
||||
checkBookmarkExists(time) {
|
||||
return this.bookmarks.find(bm => bm.time === time)
|
||||
}
|
||||
|
||||
createBookmark(time, title) {
|
||||
var newBookmark = new AudioBookmark()
|
||||
newBookmark.setData(time, title)
|
||||
this.bookmarks.push(newBookmark)
|
||||
return newBookmark
|
||||
}
|
||||
|
||||
updateBookmark(time, title) {
|
||||
var bookmark = this.bookmarks.find(bm => bm.time === time)
|
||||
if (!bookmark) return false
|
||||
bookmark.title = title
|
||||
return bookmark
|
||||
}
|
||||
|
||||
deleteBookmark(time) {
|
||||
this.bookmarks = this.bookmarks.filter(bm => bm.time !== time)
|
||||
}
|
||||
}
|
||||
module.exports = AudiobookProgress
|
@ -281,11 +281,52 @@ class User {
|
||||
|
||||
createBookmark({ audiobookId, time, title }) {
|
||||
if (!this.audiobooks || !this.audiobooks[audiobookId]) {
|
||||
return false
|
||||
return {
|
||||
error: 'Invalid Audiobook'
|
||||
}
|
||||
}
|
||||
if (this.audiobooks[audiobookId].checkBookmarkExists(time)) {
|
||||
return {
|
||||
error: 'Bookmark already exists'
|
||||
}
|
||||
}
|
||||
|
||||
var success = this.audiobooks[audiobookId].createBookmark(time, title)
|
||||
if (success) return this.audiobooks[audiobookId]
|
||||
return null
|
||||
}
|
||||
|
||||
updateBookmark({ audiobookId, time, title }) {
|
||||
if (!this.audiobooks || !this.audiobooks[audiobookId]) {
|
||||
return {
|
||||
error: 'Invalid Audiobook'
|
||||
}
|
||||
}
|
||||
if (!this.audiobooks[audiobookId].checkBookmarkExists(time)) {
|
||||
return {
|
||||
error: 'Bookmark does not exist'
|
||||
}
|
||||
}
|
||||
|
||||
var success = this.audiobooks[audiobookId].updateBookmark(time, title)
|
||||
if (success) return this.audiobooks[audiobookId]
|
||||
return null
|
||||
}
|
||||
|
||||
deleteBookmark({ audiobookId, time }) {
|
||||
if (!this.audiobooks || !this.audiobooks[audiobookId]) {
|
||||
return {
|
||||
error: 'Invalid Audiobook'
|
||||
}
|
||||
}
|
||||
if (!this.audiobooks[audiobookId].checkBookmarkExists(time)) {
|
||||
return {
|
||||
error: 'Bookmark does not exist'
|
||||
}
|
||||
}
|
||||
|
||||
this.audiobooks[audiobookId].deleteBookmark(time)
|
||||
return this.audiobooks[audiobookId]
|
||||
}
|
||||
}
|
||||
module.exports = User
|
@ -69,7 +69,7 @@ function secondsToTimestamp(seconds) {
|
||||
_seconds -= _minutes * 60
|
||||
var _hours = Math.floor(_minutes / 60)
|
||||
_minutes -= _hours * 60
|
||||
_seconds = Math.round(_seconds)
|
||||
_seconds = Math.floor(_seconds)
|
||||
if (!_hours) {
|
||||
return `${_minutes}:${_seconds.toString().padStart(2, '0')}`
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user