This commit is contained in:
Ruby Marx 2024-05-23 04:40:25 +02:00 committed by GitHub
commit 2b454991a2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 298 additions and 92 deletions

View file

@ -4,7 +4,18 @@
<button type="button" aria-label="Previous page" class="w-24 max-w-24 h-full hidden sm:flex items-center overflow-x-hidden justify-center opacity-50 hover:opacity-100"> <button type="button" aria-label="Previous page" class="w-24 max-w-24 h-full hidden sm:flex items-center overflow-x-hidden justify-center opacity-50 hover:opacity-100">
<span v-if="hasPrev" class="material-icons text-6xl" @mousedown.prevent @click="prev">chevron_left</span> <span v-if="hasPrev" class="material-icons text-6xl" @mousedown.prevent @click="prev">chevron_left</span>
</button> </button>
<div id="frame" class="w-full" style="height: 80%"> <div id="frame" class="w-full shrink">
<div id="viewer" ref="viewer"></div>
<div v-if="showAnnotator" class="rounded bg-bg absolute" :style="{ top: annotatorPosition.top + 'px', left: annotatorPosition.left + 'px' }">
<button v-if="isTextSelected" class="opacity-80 hover:opacity-100" @mousedown.prevent @mouseup.prevent @click="addAnotationClicked">
<span class="material-icons text-4xl"> bookmark_add </span>
</button>
<button v-else class="opacity-80 hover:opacity-100" @mousedown.prevent @mouseup.prevent @click.stop="removeAnnotation">
<span class="material-icons text-4xl"> bookmark_remove </span>
</button>
</div>
<div id="viewer"></div> <div id="viewer"></div>
</div> </div>
<button type="button" aria-label="Next page" class="w-24 max-w-24 h-full hidden sm:flex items-center justify-center overflow-x-hidden opacity-50 hover:opacity-100"> <button type="button" aria-label="Next page" class="w-24 max-w-24 h-full hidden sm:flex items-center justify-center overflow-x-hidden opacity-50 hover:opacity-100">
@ -22,6 +33,23 @@ import ePub from 'epubjs'
* @property {ePub.Book} book * @property {ePub.Book} book
* @property {ePub.Rendition} rendition * @property {ePub.Rendition} rendition
*/ */
/**
* @typedef {object} Chapter
* @property {string} title
* @property {string} cfi
* @property {number} start - percentage
* @property {number} end - percentage
* @property {string} href
* @property {number} id
* @property {Annotation[]} searchResults
* @property {Chapter[]} subitems - nested chapters
*/
/**
* @typedef {object} Annotation
* @property {string} cfi
* @property {number} start - percentage
* @property {string} excerpt - highlighted text
*/
export default { export default {
props: { props: {
libraryItem: { libraryItem: {
@ -34,13 +62,22 @@ export default {
}, },
data() { data() {
return { return {
annotatorPosition: { top: 0, left: 0 },
isTextSelected: false,
showAnnotator: false,
mousePosition: { x: 0, y: 0 },
windowWidth: 0, windowWidth: 0,
windowHeight: 0, windowHeight: 0,
/** @type {ePub.Book} */ /** @type {ePub.Book} */
book: null, book: null,
/** @type {ePub.Rendition} */ /** @type {ePub.Rendition} */
rendition: null, rendition: null,
/** @type {Chapter[]} */
chapters: [], chapters: [],
/** @type {Annotation[]} */
bookmarks: [],
selectedCFI: '',
flattenedChapters: [],
ereaderSettings: { ereaderSettings: {
theme: 'dark', theme: 'dark',
font: 'serif', font: 'serif',
@ -157,26 +194,105 @@ export default {
} }
return foundChapter return foundChapter
}, },
/** @returns {Array} Returns an array of chapters that only includes chapters with query results */ addResultsToChapters(chapters, results) {
async searchBook(query) { results.forEach((chapter) => {
const chapters = structuredClone(await this.chapters)
const searchResults = await Promise.all(this.book.spine.spineItems.map((item) => item.load(this.book.load.bind(this.book)).then(item.find.bind(item, query)).finally(item.unload.bind(item))))
const mergedResults = [].concat(...searchResults)
mergedResults.forEach((chapter) => {
chapter.start = this.book.locations.percentageFromCfi(chapter.cfi) chapter.start = this.book.locations.percentageFromCfi(chapter.cfi)
const foundChapter = this.findChapterFromPosition(chapters, chapter.start) const foundChapter = this.findChapterFromPosition(chapters, chapter.start)
if (foundChapter) foundChapter.searchResults.push(chapter) if (foundChapter) foundChapter.searchResults.push(chapter)
}) })
let filteredResults = chapters.filter(function f(o) { return chapters.filter(function f(o) {
if (o.searchResults.length) return true if (o.searchResults.length) return true
if (o.subitems.length) { if (o.subitems.length) {
return (o.subitems = o.subitems.filter(f)).length return (o.subitems = o.subitems.filter(f)).length
} }
}) })
},
async searchBook(query) {
const chapters = structuredClone(await this.chapters)
const searchResults = await Promise.all(this.book.spine.spineItems.map((item) => item.load(this.book.load.bind(this.book)).then(item.find.bind(item, query)).finally(item.unload.bind(item))))
const mergedResults = [].concat(...searchResults)
const filteredResults = this.addResultsToChapters(chapters, mergedResults)
return filteredResults return filteredResults
}, },
getBookmarks() {
const chapters = structuredClone(this.chapters)
const mergedResults = structuredClone(this.bookmarks)
let filteredResults = this.addResultsToChapters(chapters, mergedResults)
return filteredResults
},
captureMousePosition(evt, contents) {
const viewElementRect = contents.document.defaultView.frameElement.getBoundingClientRect()
this.annotatorPosition = { top: evt.clientY + viewElementRect.top + 20, left: evt.clientX + viewElementRect.left - 10 }
},
onTextSelected(cfiRange, contents) {
this.isTextSelected = true
this.showAnnotator = true
this.selectedCFI = cfiRange
},
onAnnotationClicked(cfiRange) {
this.isTextSelected = false
this.showAnnotator = true
this.selectedCFI = cfiRange
},
removeAnnotation() {
this.rendition.annotations.remove(this.selectedCFI, 'highlight')
this.bookmarks = this.bookmarks.filter((item) => item.cfi !== this.selectedCFI)
this.showAnnotator = false
var bookmark = {
title: this.rendition.getRange(this.selectedCFI).toString(),
time: this.selectedCFI
}
this.$axios
.$delete(`/api/me/item/${this.libraryItemId}/bookmark/${encodeURIComponent(bookmark.time)}?type=epub`)
.then(() => {
this.$toast.success(this.$strings.ToastBookmarkRemoveSuccess)
})
.catch((error) => {
this.$toast.error(this.$strings.ToastBookmarkRemoveFailed)
console.error(error)
})
},
addAnotationClicked() {
var bookmark = {
title: this.rendition.getRange(this.selectedCFI).toString(),
time: this.selectedCFI,
type: 'epub'
}
this.submitCreateBookmark(bookmark)
this.addAnotation(bookmark)
// Deselect text
this.rendition.getContents().forEach((contents) => contents.window.getSelection().removeAllRanges())
this.showAnnotator = false
this.isTextSelected = false
},
addAnotation(bookmark) {
this.bookmarks.push({
excerpt: bookmark.title,
cfi: bookmark.time,
start: this.book.locations.percentageFromCfi(bookmark.time)
})
this.rendition.annotations.add('highlight', bookmark.time, {}, null, 'hl', {
fill: 'red',
'fill-opacity': '0.5',
'mix-blend-mode': 'multiply'
})
},
submitCreateBookmark(bookmark) {
this.$axios
.$post(`/api/me/item/${this.libraryItemId}/bookmark`, bookmark)
.then(() => {
this.$toast.success(this.$strings.ToastBookmarkCreateSuccess)
})
.catch((error) => {
this.$toast.error(this.$strings.ToastBookmarkCreateFailed)
console.error(error)
})
},
keyUp(e) { keyUp(e) {
const rtl = this.book.package.metadata.direction === 'rtl' const rtl = this.book.package.metadata.direction === 'rtl'
if ((e.keyCode || e.which) == 37) { if ((e.keyCode || e.which) == 37) {
@ -283,6 +399,7 @@ export default {
}, },
/** @param {string} location - CFI of the new location */ /** @param {string} location - CFI of the new location */
relocated(location) { relocated(location) {
this.showAnnotator = false
if (this.savedEbookLocation === location.start.cfi) { if (this.savedEbookLocation === location.start.cfi) {
return return
} }
@ -342,6 +459,15 @@ export default {
this.$emit('touchend', event) this.$emit('touchend', event)
}) })
// Show popup to annotate selected text
reader.rendition.on('selected', this.onTextSelected)
reader.rendition.on('mouseup', this.captureMousePosition)
reader.rendition.on('mousedown', () => {
this.showAnnotator = false
})
// Show popup to remove an annotation
reader.rendition.on('markClicked', this.onAnnotationClicked)
// load ebook cfi locations // load ebook cfi locations
const savedLocations = this.loadLocations() const savedLocations = this.loadLocations()
if (savedLocations) { if (savedLocations) {
@ -353,6 +479,11 @@ export default {
} }
this.getChapters() this.getChapters()
}) })
const bookmarks = this.$store.getters['user/getUserBookmarksForItem'](this.libraryItemId, 'epub')
bookmarks.forEach((bookmark) => {
this.addAnotation(bookmark)
})
}, },
getChapters() { getChapters() {
// Load the list of chapters in the book. See https://github.com/futurepress/epub.js/issues/759 // Load the list of chapters in the book. See https://github.com/futurepress/epub.js/issues/759
@ -400,6 +531,8 @@ export default {
} }
return createTree(toc, tocTree).then(() => { return createTree(toc, tocTree).then(() => {
this.chapters = tocTree this.chapters = tocTree
this.$emit('chaptersLoaded', this.chapters)
this.flattenChapters()
}) })
}, },
flattenChapters(chapters) { flattenChapters(chapters) {

View file

@ -1,10 +1,13 @@
<template> <template>
<div v-if="show" id="reader" :data-theme="ereaderTheme" class="group absolute top-0 left-0 w-full z-60 data-[theme=dark]:bg-primary data-[theme=dark]:text-white data-[theme=light]:bg-white data-[theme=light]:text-black" :class="{ 'reader-player-open': !!streamLibraryItem }"> <div v-if="show" id="reader" :data-theme="ereaderTheme" class="group absolute top-0 left-0 w-full z-60 data-[theme=dark]:bg-primary data-[theme=dark]:text-white data-[theme=light]:bg-white data-[theme=light]:text-black" :class="{ 'reader-player-open': !!streamLibraryItem }">
<div class="absolute top-4 left-4 z-20 flex items-center"> <div class="absolute top-4 left-4 z-20 inline-flex gap-4 items-center">
<button v-if="isEpub" @click="toggleToC" type="button" aria-label="Table of contents menu" class="inline-flex opacity-80 hover:opacity-100"> <button v-if="isEpub" @click="openToC" type="button" aria-label="Table of contents menu" class="opacity-80 hover:opacity-100">
<span class="material-icons text-2xl">menu</span> <span class="material-icons text-2xl">menu</span>
</button> </button>
<button v-if="hasSettings" @click="openSettings" type="button" aria-label="Ereader settings" class="mx-4 inline-flex opacity-80 hover:opacity-100"> <button v-if="isEpub" @click="openBookmarks" type="button" aria-label="Bookmarks" class="opacity-80 hover:opacity-100">
<span class="material-icons text-2xl">bookmarks</span>
</button>
<button v-if="hasSettings" @click="openSettings" type="button" aria-label="Ereader settings" class="opacity-80 hover:opacity-100">
<span class="material-icons text-1.5xl">settings</span> <span class="material-icons text-1.5xl">settings</span>
</button> </button>
</div> </div>
@ -23,48 +26,9 @@
</button> </button>
</div> </div>
<component v-if="componentName" ref="readerComponent" :is="componentName" :library-item="selectedLibraryItem" :player-open="!!streamLibraryItem" :keep-progress="keepProgress" :file-id="ebookFileId" @touchstart="touchstart" @touchend="touchend" @hook:mounted="readerMounted" /> <component v-if="componentName" ref="readerComponent" :is="componentName" :library-item="selectedLibraryItem" :player-open="!!streamLibraryItem" :keep-progress="keepProgress" :file-id="ebookFileId" @touchstart="touchstart" @touchend="touchend" @hook:mounted="readerMounted" @chaptersLoaded="chaptersLoaded" />
<!-- TOC side nav --> <readers-sidebar v-if="isEpub" ref="sidebar" :chapters="chapters" :bookmarks="bookmarks" :bookmarksOpen="bookmarksOpen" :sidebarOpen="sidebarOpen" @toggle-sidebar="sidebarOpen = false" @goToChapter="goToChapter" />
<div v-if="tocOpen" class="w-full h-full overflow-y-scroll absolute inset-0 bg-black/20 z-20" @click.stop.prevent="toggleToC"></div>
<div v-if="isEpub" class="w-96 h-full max-h-full absolute top-0 left-0 shadow-xl transition-transform z-30 group-data-[theme=dark]:bg-primary group-data-[theme=dark]:text-white group-data-[theme=light]:bg-white group-data-[theme=light]:text-black" :class="tocOpen ? 'translate-x-0' : '-translate-x-96'" @click.stop.prevent>
<div class="flex flex-col p-4 h-full">
<div class="flex items-center mb-2">
<button @click.stop.prevent="toggleToC" type="button" aria-label="Close table of contents" class="inline-flex opacity-80 hover:opacity-100">
<span class="material-icons text-2xl">arrow_back</span>
</button>
<p class="text-lg font-semibold ml-2">{{ $strings.HeaderTableOfContents }}</p>
</div>
<form @submit.prevent="searchBook" @click.stop.prevent>
<ui-text-input clearable ref="input" @clear="searchBook" v-model="searchQuery" :placeholder="$strings.PlaceholderSearch" class="h-8 w-full text-sm flex mb-2" />
</form>
<div class="overflow-y-auto">
<div v-if="isSearching && !this.searchResults.length" class="w-full h-40 justify-center">
<p class="text-center text-xl py-4">{{ $strings.MessageNoResults }}</p>
</div>
<ul>
<li v-for="chapter in isSearching ? this.searchResults : chapters" :key="chapter.id" class="py-1">
<a :href="chapter.href" class="opacity-80 hover:opacity-100" @click.prevent="goToChapter(chapter.href)">{{ chapter.title }}</a>
<div v-for="searchResults in chapter.searchResults" :key="searchResults.cfi" class="text-sm py-1 pl-4">
<a :href="searchResults.cfi" class="opacity-50 hover:opacity-100" @click.prevent="goToChapter(searchResults.cfi)">{{ searchResults.excerpt }}</a>
</div>
<ul v-if="chapter.subitems.length">
<li v-for="subchapter in chapter.subitems" :key="subchapter.id" class="py-1 pl-4">
<a :href="subchapter.href" class="opacity-80 hover:opacity-100" @click.prevent="goToChapter(subchapter.href)">{{ subchapter.title }}</a>
<div v-for="subChapterSearchResults in subchapter.searchResults" :key="subChapterSearchResults.cfi" class="text-sm py-1 pl-4">
<a :href="subChapterSearchResults.cfi" class="opacity-50 hover:opacity-100" @click.prevent="goToChapter(subChapterSearchResults.cfi)">{{ subChapterSearchResults.excerpt }}</a>
</div>
</li>
</ul>
</li>
</ul>
</div>
</div>
</div>
<!-- ereader settings modal --> <!-- ereader settings modal -->
<modals-modal v-model="showSettings" name="ereader-settings-modal" :width="500" :height="'unset'" :processing="false"> <modals-modal v-model="showSettings" name="ereader-settings-modal" :width="500" :height="'unset'" :processing="false">
@ -119,11 +83,12 @@ export default {
touchendY: 0, touchendY: 0,
touchstartTime: 0, touchstartTime: 0,
touchIdentifier: null, touchIdentifier: null,
bookmarks: [],
chapters: [], chapters: [],
isSearching: false, isSearching: false,
searchResults: [],
searchQuery: '', searchQuery: '',
tocOpen: false, sidebarOpen: false,
bookmarksOpen: false,
showSettings: false, showSettings: false,
ereaderSettings: { ereaderSettings: {
theme: 'dark', theme: 'dark',
@ -273,7 +238,7 @@ export default {
}, },
methods: { methods: {
goToChapter(uri) { goToChapter(uri) {
this.toggleToC() this.sidebarOpen = false
this.$refs.readerComponent.goToChapter(uri) this.$refs.readerComponent.goToChapter(uri)
}, },
readerMounted() { readerMounted() {
@ -285,9 +250,17 @@ export default {
this.$refs.readerComponent?.updateSettings?.(this.ereaderSettings) this.$refs.readerComponent?.updateSettings?.(this.ereaderSettings)
localStorage.setItem('ereaderSettings', JSON.stringify(this.ereaderSettings)) localStorage.setItem('ereaderSettings', JSON.stringify(this.ereaderSettings))
}, },
toggleToC() { openToC() {
this.tocOpen = !this.tocOpen this.sidebarOpen = true
this.chapters = this.$refs.readerComponent.chapters this.bookmarksOpen = false
},
chaptersLoaded(chapters) {
this.chapters = chapters
},
openBookmarks() {
this.sidebarOpen = true
this.bookmarksOpen = true
this.bookmarks = this.$refs.readerComponent.getBookmarks()
}, },
openSettings() { openSettings() {
this.showSettings = true this.showSettings = true
@ -303,15 +276,6 @@ export default {
this.close() this.close()
} }
}, },
async searchBook() {
if (this.searchQuery.length > 1) {
this.searchResults = await this.$refs.readerComponent.searchBook(this.searchQuery)
this.isSearching = true
} else {
this.isSearching = false
this.searchResults = []
}
},
next() { next() {
if (this.$refs.readerComponent?.next) this.$refs.readerComponent.next() if (this.$refs.readerComponent?.next) this.$refs.readerComponent.next()
}, },
@ -390,8 +354,8 @@ export default {
}, },
close() { close() {
this.unregisterListeners() this.unregisterListeners()
this.isSearching = false this.$refs.sidebar.isSearching = false
this.searchQuery = '' this.$refs.sidebar.searchQuery = ''
this.show = false this.show = false
} }
}, },

View file

@ -0,0 +1,96 @@
<template>
<div>
<div v-if="sidebarOpen" class="w-full h-full overflow-y-scroll absolute inset-0 bg-black/20 z-20" @click.stop.prevent="toggleSidebar"></div>
<div class="w-96 h-full max-h-full absolute top-0 left-0 shadow-xl transition-transform z-50 group-data-[theme=dark]:bg-primary group-data-[theme=dark]:text-white group-data-[theme=light]:bg-white group-data-[theme=light]:text-black" :class="sidebarOpen ? 'translate-x-0' : '-translate-x-96'" @click.stop.prevent>
<div class="flex flex-col p-4 h-full">
<div class="flex items-center mb-2">
<button @click.stop.prevent="toggleSidebar" type="button" aria-label="Close table of contents" class="inline-flex opacity-80 hover:opacity-100">
<span class="material-icons text-2xl">arrow_back</span>
</button>
<p class="text-lg font-semibold ml-2">{{ sidebarTitle }}</p>
</div>
<template v-if="!bookmarksOpen">
<form @submit.prevent="searchBook">
<ui-text-input clearable ref="input" @clear="searchBook" v-model="searchQuery" :placeholder="$strings.PlaceholderSearch" class="h-8 w-full text-sm flex mb-2" />
</form>
</template>
<div class="overflow-y-auto">
<div v-if="isSearching && !searchResults.length" class="w-full h-40 justify-center">
<p class="text-center text-xl py-4">{{ $strings.MessageNoResults }}</p>
</div>
<div v-if="bookmarksOpen && !bookmarks.length" class="w-full h-40 justify-center">
<p class="text-center text-xl py-4">{{ $strings.MessageNoBookmarks }}</p>
</div>
<div v-else-if="!chapters.length" class="w-full h-40 justify-center">
<p class="text-center text-xl py-4">{{ $strings.MessageNoChapters }}</p>
</div>
<ul>
<li v-for="chapter in contents" :key="chapter.id" class="py-1">
<a :href="chapter.href" class="opacity-80 hover:opacity-100" @click.prevent="goToChapter(chapter.href)">{{ chapter.title }}</a>
<div v-for="searchResults in chapter.searchResults" :key="searchResults.cfi" class="text-sm py-1 pl-4">
<a :href="searchResults.cfi" class="opacity-50 hover:opacity-100" @click.prevent="goToChapter(searchResults.cfi)">{{ searchResults.excerpt }}</a>
</div>
<ul v-if="chapter.subitems.length">
<li v-for="subchapter in chapter.subitems" :key="subchapter.id" class="py-1 pl-4">
<a :href="subchapter.href" class="opacity-80 hover:opacity-100" @click.prevent="goToChapter(subchapter.href)">{{ subchapter.title }}</a>
<div v-for="subChapterSearchResults in subchapter.searchResults" :key="subChapterSearchResults.cfi" class="text-sm py-1 pl-4">
<a :href="subChapterSearchResults.cfi" class="opacity-50 hover:opacity-100" @click.prevent="goToChapter(subChapterSearchResults.cfi)">{{ subChapterSearchResults.excerpt }}</a>
</div>
</li>
</ul>
</li>
</ul>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
searchQuery: '',
isSearching: false,
searchResults: []
}
},
props: {
sidebarOpen: false,
bookmarksOpen: false,
chapters: [],
bookmarks: []
},
computed: {
sidebarTitle() {
return this.bookmarksOpen ? this.$strings.LabelYourBookmarks : this.$strings.HeaderTableOfContents
},
contents() {
return !this.bookmarksOpen ? (this.isSearching ? this.searchResults : this.chapters) : this.bookmarks
}
},
methods: {
toggleSidebar() {
this.$emit('toggle-sidebar')
},
goToChapter(target) {
this.$emit('goToChapter', target)
},
async searchBook() {
if (this.searchQuery.length > 1) {
this.searchResults = await this.$parent.$refs.readerComponent.searchBook(this.searchQuery)
this.isSearching = true
} else {
this.isSearching = false
this.searchResults = []
}
}
}
}
</script>

View file

@ -30,9 +30,10 @@ export const getters = {
return li.libraryItemId == libraryItemId return li.libraryItemId == libraryItemId
}) })
}, },
getUserBookmarksForItem: (state) => (libraryItemId) => { getUserBookmarksForItem: (state) => (libraryItemId, bookmarkType) => {
if (!state.user.bookmarks) return [] if (!state.user.bookmarks) return []
return state.user.bookmarks.filter(bm => bm.libraryItemId === libraryItemId) bookmarkType = bookmarkType || 'audio'
return state.user.bookmarks.filter(bm => bm.libraryItemId === libraryItemId && bm.type === bookmarkType)
}, },
getUserSetting: (state) => (key) => { getUserSetting: (state) => (key) => {
return state.settings?.[key] || null return state.settings?.[key] || null

View file

@ -124,9 +124,10 @@ class MeController {
// POST: api/me/item/:id/bookmark // POST: api/me/item/:id/bookmark
async createBookmark(req, res) { async createBookmark(req, res) {
if (!await Database.libraryItemModel.checkExistsById(req.params.id)) return res.sendStatus(404) if (!await Database.libraryItemModel.checkExistsById(req.params.id)) return res.sendStatus(404)
req.body.type = req.body.type || 'audio'
const { time, title } = req.body const { type, time, title } = req.body
const bookmark = req.user.createBookmark(req.params.id, time, title) const bookmark = req.user.createBookmark(req.params.id, type, time, title)
await Database.updateUser(req.user) await Database.updateUser(req.user)
SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser()) SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
res.json(bookmark) res.json(bookmark)
@ -135,14 +136,15 @@ class MeController {
// PATCH: api/me/item/:id/bookmark // PATCH: api/me/item/:id/bookmark
async updateBookmark(req, res) { async updateBookmark(req, res) {
if (!await Database.libraryItemModel.checkExistsById(req.params.id)) return res.sendStatus(404) if (!await Database.libraryItemModel.checkExistsById(req.params.id)) return res.sendStatus(404)
req.body.type = req.body.type || 'audio'
const { time, title } = req.body const { type, time, title } = req.body
if (!req.user.findBookmark(req.params.id, time)) { if (!req.user.findBookmark(req.params.id, type, time)) {
Logger.error(`[MeController] updateBookmark not found`) Logger.error(`[MeController] updateBookmark not found`)
return res.sendStatus(404) return res.sendStatus(404)
} }
const bookmark = req.user.updateBookmark(req.params.id, time, title) const bookmark = req.user.updateBookmark(req.params.id, type, time, title)
if (!bookmark) return res.sendStatus(500) if (!bookmark) return res.sendStatus(500)
await Database.updateUser(req.user) await Database.updateUser(req.user)
@ -150,19 +152,25 @@ class MeController {
res.json(bookmark) res.json(bookmark)
} }
// DELETE: api/me/item/:id/bookmark/:time // DELETE: api/me/item/:id/bookmark
async removeBookmark(req, res) { async removeBookmark(req, res) {
if (!await Database.libraryItemModel.checkExistsById(req.params.id)) return res.sendStatus(404) if (!await Database.libraryItemModel.checkExistsById(req.params.id)) return res.sendStatus(404)
req.query.type = req.query.type || 'audio'
const time = Number(req.params.time) if (req.query.type == 'audio') {
if (isNaN(time)) return res.sendStatus(500) var time = Number(req.params.time)
if (isNaN(time)) return res.sendStatus(500)
}
else {
var time = req.params.time
}
if (!req.user.findBookmark(req.params.id, time)) { if (!req.user.findBookmark(req.params.id, req.query.type, time)) {
Logger.error(`[MeController] removeBookmark not found`) Logger.error(`[MeController] removeBookmark not found`)
return res.sendStatus(404) return res.sendStatus(404)
} }
req.user.removeBookmark(req.params.id, time) req.user.removeBookmark(req.params.id, req.query.type, time)
await Database.updateUser(req.user) await Database.updateUser(req.user)
SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser()) SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
res.sendStatus(200) res.sendStatus(200)

View file

@ -1,6 +1,7 @@
class AudioBookmark { class AudioBookmark {
constructor(bookmark) { constructor(bookmark) {
this.libraryItemId = null this.libraryItemId = null
this.type = null
this.title = null this.title = null
this.time = null this.time = null
this.createdAt = null this.createdAt = null
@ -13,6 +14,7 @@ class AudioBookmark {
toJSON() { toJSON() {
return { return {
libraryItemId: this.libraryItemId, libraryItemId: this.libraryItemId,
type: this.type || 'audio',
title: this.title || '', title: this.title || '',
time: this.time, time: this.time,
createdAt: this.createdAt createdAt: this.createdAt
@ -21,13 +23,15 @@ class AudioBookmark {
construct(bookmark) { construct(bookmark) {
this.libraryItemId = bookmark.libraryItemId this.libraryItemId = bookmark.libraryItemId
this.type = bookmark.type || 'audio'
this.title = bookmark.title || '' this.title = bookmark.title || ''
this.time = bookmark.time || 0 this.time = bookmark.time || 0
this.createdAt = bookmark.createdAt this.createdAt = bookmark.createdAt
} }
setData(libraryItemId, time, title) { setData(libraryItemId, type, time, title) {
this.libraryItemId = libraryItemId this.libraryItemId = libraryItemId
this.type = type
this.title = title this.title = title
this.time = time this.time = time
this.createdAt = Date.now() this.createdAt = Date.now()

View file

@ -456,25 +456,25 @@ class User {
return this.checkCanAccessLibraryItemWithTags(tags) return this.checkCanAccessLibraryItemWithTags(tags)
} }
findBookmark(libraryItemId, time) { findBookmark(libraryItemId, type, time) {
return this.bookmarks.find(bm => bm.libraryItemId === libraryItemId && bm.time == time) return this.bookmarks.find(bm => bm.libraryItemId === libraryItemId && bm.time == time && bm.type === type)
} }
createBookmark(libraryItemId, time, title) { createBookmark(libraryItemId, type, time, title) {
var existingBookmark = this.findBookmark(libraryItemId, time) var existingBookmark = this.findBookmark(libraryItemId, type, time)
if (existingBookmark) { if (existingBookmark) {
Logger.warn('[User] Create Bookmark already exists for this time') Logger.warn('[User] Create Bookmark already exists for this time')
existingBookmark.title = title existingBookmark.title = title
return existingBookmark return existingBookmark
} }
var newBookmark = new AudioBookmark() var newBookmark = new AudioBookmark()
newBookmark.setData(libraryItemId, time, title) newBookmark.setData(libraryItemId, type, time, title)
this.bookmarks.push(newBookmark) this.bookmarks.push(newBookmark)
return newBookmark return newBookmark
} }
updateBookmark(libraryItemId, time, title) { updateBookmark(libraryItemId, type, time, title) {
var bookmark = this.findBookmark(libraryItemId, time) var bookmark = this.findBookmark(libraryItemId, type, time)
if (!bookmark) { if (!bookmark) {
Logger.error(`[User] updateBookmark not found`) Logger.error(`[User] updateBookmark not found`)
return null return null
@ -483,8 +483,8 @@ class User {
return bookmark return bookmark
} }
removeBookmark(libraryItemId, time) { removeBookmark(libraryItemId, type, time) {
this.bookmarks = this.bookmarks.filter(bm => (bm.libraryItemId !== libraryItemId || bm.time !== time)) this.bookmarks = this.bookmarks.filter(bm => (bm.libraryItemId !== libraryItemId || bm.time !== time || bm.type !== type))
} }
checkShouldHideSeriesFromContinueListening(seriesId) { checkShouldHideSeriesFromContinueListening(seriesId) {