diff --git a/Dockerfile b/Dockerfile index f9c46117..816bdd3c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -57,7 +57,7 @@ WORKDIR /app # Copy compiled frontend and server from build stages COPY --from=build-client /client/dist /app/client/dist COPY --from=build-server /server /app -COPY --from=build-server /usr/local/lib/nusqlite3 /usr/local/lib/nusqlite3 +COPY --from=build-server ${NUSQLITE3_PATH} ${NUSQLITE3_PATH} EXPOSE 80 diff --git a/client/components/app/ConfigSideNav.vue b/client/components/app/ConfigSideNav.vue index 50fa7a06..32e7e694 100644 --- a/client/components/app/ConfigSideNav.vue +++ b/client/components/app/ConfigSideNav.vue @@ -70,6 +70,11 @@ export default { title: this.$strings.HeaderUsers, path: '/config/users' }, + { + id: 'config-api-keys', + title: this.$strings.HeaderApiKeys, + path: '/config/api-keys' + }, { id: 'config-sessions', title: this.$strings.HeaderListeningSessions, diff --git a/client/components/app/LazyBookshelf.vue b/client/components/app/LazyBookshelf.vue index 61331fb9..854b61b2 100644 --- a/client/components/app/LazyBookshelf.vue +++ b/client/components/app/LazyBookshelf.vue @@ -778,10 +778,6 @@ export default { windowResize() { this.executeRebuild() }, - socketInit() { - // Server settings are set on socket init - this.executeRebuild() - }, initListeners() { window.addEventListener('resize', this.windowResize) @@ -794,7 +790,6 @@ export default { }) this.$eventBus.$on('bookshelf_clear_selection', this.clearSelectedEntities) - this.$eventBus.$on('socket_init', this.socketInit) this.$eventBus.$on('user-settings', this.settingsUpdated) if (this.$root.socket) { @@ -826,7 +821,6 @@ export default { } this.$eventBus.$off('bookshelf_clear_selection', this.clearSelectedEntities) - this.$eventBus.$off('socket_init', this.socketInit) this.$eventBus.$off('user-settings', this.settingsUpdated) if (this.$root.socket) { diff --git a/client/components/cards/AuthorCard.vue b/client/components/cards/AuthorCard.vue index 82645c57..05347393 100644 --- a/client/components/cards/AuthorCard.vue +++ b/client/components/cards/AuthorCard.vue @@ -71,9 +71,6 @@ export default { coverHeight() { return this.cardHeight }, - userToken() { - return this.store.getters['user/getToken'] - }, _author() { return this.author || {} }, diff --git a/client/components/cards/LazyBookCard.vue b/client/components/cards/LazyBookCard.vue index 35c959fa..41b73310 100644 --- a/client/components/cards/LazyBookCard.vue +++ b/client/components/cards/LazyBookCard.vue @@ -198,7 +198,7 @@ export default { return this.store.getters['user/getSizeMultiplier'] }, dateFormat() { - return this.store.state.serverSettings.dateFormat + return this.store.getters['getServerSetting']('dateFormat') }, _libraryItem() { return this.libraryItem || {} diff --git a/client/components/cards/LazySeriesCard.vue b/client/components/cards/LazySeriesCard.vue index 3532095b..34cea7e2 100644 --- a/client/components/cards/LazySeriesCard.vue +++ b/client/components/cards/LazySeriesCard.vue @@ -71,7 +71,7 @@ export default { return this.height * this.sizeMultiplier }, dateFormat() { - return this.store.state.serverSettings.dateFormat + return this.store.getters['getServerSetting']('dateFormat') }, labelFontSize() { if (this.width < 160) return 0.75 diff --git a/client/components/controls/LibraryFilterSelect.vue b/client/components/controls/LibraryFilterSelect.vue index f5eec41a..62a9b803 100644 --- a/client/components/controls/LibraryFilterSelect.vue +++ b/client/components/controls/LibraryFilterSelect.vue @@ -94,6 +94,9 @@ export default { userIsAdminOrUp() { return this.$store.getters['user/getIsAdminOrUp'] }, + userCanAccessExplicitContent() { + return this.$store.getters['user/getUserCanAccessExplicitContent'] + }, libraryMediaType() { return this.$store.getters['libraries/getCurrentLibraryMediaType'] }, @@ -239,6 +242,15 @@ export default { sublist: false } ] + + if (this.userCanAccessExplicitContent) { + items.push({ + text: this.$strings.LabelExplicit, + value: 'explicit', + sublist: false + }) + } + if (this.userIsAdminOrUp) { items.push({ text: this.$strings.LabelShareOpen, @@ -249,7 +261,7 @@ export default { return items }, podcastItems() { - return [ + const items = [ { text: this.$strings.LabelAll, value: 'all' @@ -283,6 +295,16 @@ export default { sublist: false } ] + + if (this.userCanAccessExplicitContent) { + items.push({ + text: this.$strings.LabelExplicit, + value: 'explicit', + sublist: false + }) + } + + return items }, selectItems() { if (this.isSeries) return this.seriesItems diff --git a/client/components/covers/AuthorImage.vue b/client/components/covers/AuthorImage.vue index e320e552..084492b0 100644 --- a/client/components/covers/AuthorImage.vue +++ b/client/components/covers/AuthorImage.vue @@ -39,9 +39,6 @@ export default { } }, computed: { - userToken() { - return this.$store.getters['user/getToken'] - }, _author() { return this.author || {} }, diff --git a/client/components/modals/AccountModal.vue b/client/components/modals/AccountModal.vue index 71ac8155..6f4b7b67 100644 --- a/client/components/modals/AccountModal.vue +++ b/client/components/modals/AccountModal.vue @@ -309,9 +309,9 @@ export default { } else { console.log('Account updated', data.user) - if (data.user.id === this.user.id && data.user.token !== this.user.token) { - console.log('Current user token was updated') - this.$store.commit('user/setUserToken', data.user.token) + if (data.user.id === this.user.id && data.user.accessToken !== this.user.accessToken) { + console.log('Current user access token was updated') + this.$store.commit('user/setAccessToken', data.user.accessToken) } this.$toast.success(this.$strings.ToastAccountUpdateSuccess) @@ -351,9 +351,6 @@ export default { this.$toast.error(errMsg || 'Failed to create account') }) }, - toggleActive() { - this.newUser.isActive = !this.newUser.isActive - }, userTypeUpdated(type) { this.newUser.permissions = { download: type !== 'guest', diff --git a/client/components/modals/ApiKeyCreatedModal.vue b/client/components/modals/ApiKeyCreatedModal.vue new file mode 100644 index 00000000..96442a17 --- /dev/null +++ b/client/components/modals/ApiKeyCreatedModal.vue @@ -0,0 +1,60 @@ + + + diff --git a/client/components/modals/ApiKeyModal.vue b/client/components/modals/ApiKeyModal.vue new file mode 100644 index 00000000..b347abd0 --- /dev/null +++ b/client/components/modals/ApiKeyModal.vue @@ -0,0 +1,198 @@ + + + diff --git a/client/components/modals/BookmarksModal.vue b/client/components/modals/BookmarksModal.vue index de8c72b7..d84a8ed8 100644 --- a/client/components/modals/BookmarksModal.vue +++ b/client/components/modals/BookmarksModal.vue @@ -79,10 +79,10 @@ export default { return !this.bookmarks.find((bm) => Math.abs(this.currentTime - bm.time) < 1) }, dateFormat() { - return this.$store.state.serverSettings.dateFormat + return this.$store.getters['getServerSetting']('dateFormat') }, timeFormat() { - return this.$store.state.serverSettings.timeFormat + return this.$store.getters['getServerSetting']('timeFormat') } }, methods: { diff --git a/client/components/modals/ListeningSessionModal.vue b/client/components/modals/ListeningSessionModal.vue index a2469836..ecf00f78 100644 --- a/client/components/modals/ListeningSessionModal.vue +++ b/client/components/modals/ListeningSessionModal.vue @@ -159,10 +159,10 @@ export default { return 'Unknown' }, dateFormat() { - return this.$store.state.serverSettings.dateFormat + return this.$store.getters['getServerSetting']('dateFormat') }, timeFormat() { - return this.$store.state.serverSettings.timeFormat + return this.$store.getters['getServerSetting']('timeFormat') }, isOpenSession() { return !!this._session.open diff --git a/client/components/modals/Modal.vue b/client/components/modals/Modal.vue index a7d9c0ae..31ea1e61 100644 --- a/client/components/modals/Modal.vue +++ b/client/components/modals/Modal.vue @@ -23,7 +23,7 @@ export default { processing: Boolean, persistent: { type: Boolean, - default: true + default: false }, width: { type: [String, Number], @@ -99,7 +99,7 @@ export default { this.preventClickoutside = false return } - if (this.processing && this.persistent) return + if (this.processing || this.persistent) return if (ev.srcElement && ev.srcElement.classList.contains('modal-bg')) { this.show = false } diff --git a/client/components/modals/ShareModal.vue b/client/components/modals/ShareModal.vue index 24994b22..bd0c9acf 100644 --- a/client/components/modals/ShareModal.vue +++ b/client/components/modals/ShareModal.vue @@ -144,7 +144,7 @@ export default { expirationDateString() { if (!this.expireDurationSeconds) return this.$strings.LabelPermanent const dateMs = Date.now() + this.expireDurationSeconds * 1000 - return this.$formatDatetime(dateMs, this.$store.state.serverSettings.dateFormat, this.$store.state.serverSettings.timeFormat) + return this.$formatDatetime(dateMs, this.$store.getters['getServerSetting']('dateFormat'), this.$store.getters['getServerSetting']('timeFormat')) } }, methods: { diff --git a/client/components/modals/changelog/ViewModal.vue b/client/components/modals/changelog/ViewModal.vue index 1b332a1d..939ee71d 100644 --- a/client/components/modals/changelog/ViewModal.vue +++ b/client/components/modals/changelog/ViewModal.vue @@ -40,7 +40,7 @@ export default { } }, dateFormat() { - return this.$store.state.serverSettings.dateFormat + return this.$store.getters['getServerSetting']('dateFormat') }, releasesToShow() { return this.versionData?.releasesToShow || [] diff --git a/client/components/modals/item/tabs/Files.vue b/client/components/modals/item/tabs/Files.vue index 7be286fe..15c44261 100644 --- a/client/components/modals/item/tabs/Files.vue +++ b/client/components/modals/item/tabs/Files.vue @@ -29,9 +29,6 @@ export default { media() { return this.libraryItem.media || {} }, - userToken() { - return this.$store.getters['user/getToken'] - }, userCanUpdate() { return this.$store.getters['user/getUserCanUpdate'] }, diff --git a/client/components/modals/podcast/EpisodeFeed.vue b/client/components/modals/podcast/EpisodeFeed.vue index 7ec14ccd..6b99cee7 100644 --- a/client/components/modals/podcast/EpisodeFeed.vue +++ b/client/components/modals/podcast/EpisodeFeed.vue @@ -35,7 +35,14 @@

{{ episode.subtitle }}

-

Published {{ episode.publishedAt ? $dateDistanceFromNow(episode.publishedAt) : 'Unknown' }}

+
+ +

Published {{ episode.publishedAt ? $dateDistanceFromNow(episode.publishedAt) : 'Unknown' }}

+ +

{{ $strings.LabelDuration }}: {{ $elapsedPretty(episode.durationSeconds) }}

+ +

{{ $strings.LabelSize }}: {{ $bytesPretty(Number(episode.enclosure.length)) }}

+
diff --git a/client/components/modals/podcast/RemoveEpisode.vue b/client/components/modals/podcast/RemoveEpisode.vue index 38dd71cf..b2cebe84 100644 --- a/client/components/modals/podcast/RemoveEpisode.vue +++ b/client/components/modals/podcast/RemoveEpisode.vue @@ -11,7 +11,7 @@ {{ $getString('MessageConfirmRemoveEpisode', [episodeTitle]) }}

{{ $getString('MessageConfirmRemoveEpisodes', [episodes.length]) }}

-

Note: This does not delete the audio file unless toggling "Hard delete file"

+

{{ $strings.MessageConfirmRemoveEpisodeNote }}

diff --git a/client/components/modals/podcast/ViewEpisode.vue b/client/components/modals/podcast/ViewEpisode.vue index 5a520ef4..2502a5ea 100644 --- a/client/components/modals/podcast/ViewEpisode.vue +++ b/client/components/modals/podcast/ViewEpisode.vue @@ -16,7 +16,7 @@

{{ title }}

-
+

{{ $strings.MessageNoDescription }}

@@ -34,6 +34,12 @@ {{ audioFileSize }}

+
+

{{ $strings.LabelDuration }}

+

+ {{ audioFileDuration }} +

+
@@ -68,7 +74,7 @@ export default { return this.episode.title || 'No Episode Title' }, description() { - return this.episode.description || '' + return this.parseDescription(this.episode.description || '') }, media() { return this.libraryItem?.media || {} @@ -90,11 +96,49 @@ export default { return this.$bytesPretty(size) }, + audioFileDuration() { + const duration = this.episode.duration || 0 + return this.$elapsedPretty(duration) + }, bookCoverAspectRatio() { return this.$store.getters['libraries/getBookCoverAspectRatio'] } }, - methods: {}, + methods: { + handleDescriptionClick(e) { + if (e.target.matches('span.time-marker')) { + const time = parseInt(e.target.dataset.time) + if (!isNaN(time)) { + this.$eventBus.$emit('play-item', { + episodeId: this.episodeId, + libraryItemId: this.libraryItem.id, + startTime: time + }) + } + e.preventDefault() + } + }, + parseDescription(description) { + const timeMarkerLinkRegex = /(.*?)<\/a>/g + const timeMarkerRegex = /\b\d{1,2}:\d{1,2}(?::\d{1,2})?\b/g + + function convertToSeconds(time) { + const timeParts = time.split(':').map(Number) + return timeParts.reduce((acc, part, index) => acc * 60 + part, 0) + } + + return description + .replace(timeMarkerLinkRegex, (match, href, displayTime) => { + const time = displayTime.match(timeMarkerRegex)[0] + const seekTimeInSeconds = convertToSeconds(time) + return `${displayTime}` + }) + .replace(timeMarkerRegex, (match) => { + const seekTimeInSeconds = convertToSeconds(match) + return `${match}` + }) + } + }, mounted() {} } diff --git a/client/components/player/PlayerUi.vue b/client/components/player/PlayerUi.vue index 82d53552..f929943c 100644 --- a/client/components/player/PlayerUi.vue +++ b/client/components/player/PlayerUi.vue @@ -129,9 +129,6 @@ export default { return `${hoursRounded}h` } }, - token() { - return this.$store.getters['user/getToken'] - }, timeRemaining() { if (this.useChapterTrack && this.currentChapter) { var currChapTime = this.currentTime - this.currentChapter.start diff --git a/client/components/readers/ComicReader.vue b/client/components/readers/ComicReader.vue index 28d79bf2..fce26939 100644 --- a/client/components/readers/ComicReader.vue +++ b/client/components/readers/ComicReader.vue @@ -104,9 +104,6 @@ export default { } }, computed: { - userToken() { - return this.$store.getters['user/getToken'] - }, libraryItemId() { return this.libraryItem?.id }, @@ -234,10 +231,7 @@ export default { async extract() { this.loading = true var buff = await this.$axios.$get(this.ebookUrl, { - responseType: 'blob', - headers: { - Authorization: `Bearer ${this.userToken}` - } + responseType: 'blob' }) const archive = await Archive.open(buff) const originalFilesObject = await archive.getFilesObject() diff --git a/client/components/readers/EpubReader.vue b/client/components/readers/EpubReader.vue index 350d8596..ac8e3397 100644 --- a/client/components/readers/EpubReader.vue +++ b/client/components/readers/EpubReader.vue @@ -57,9 +57,6 @@ export default { } }, computed: { - userToken() { - return this.$store.getters['user/getToken'] - }, /** @returns {string} */ libraryItemId() { return this.libraryItem?.id @@ -97,9 +94,9 @@ export default { }, ebookUrl() { if (this.fileId) { - return `${this.$config.routerBasePath}/api/items/${this.libraryItemId}/ebook/${this.fileId}` + return `/api/items/${this.libraryItemId}/ebook/${this.fileId}` } - return `${this.$config.routerBasePath}/api/items/${this.libraryItemId}/ebook` + return `/api/items/${this.libraryItemId}/ebook` }, themeRules() { const isDark = this.ereaderSettings.theme === 'dark' @@ -309,14 +306,24 @@ export default { /** @type {EpubReader} */ const reader = this + // Use axios to make request because we have token refresh logic in interceptor + const customRequest = async (url) => { + try { + return this.$axios.$get(url, { + responseType: 'arraybuffer' + }) + } catch (error) { + console.error('EpubReader.initEpub customRequest failed:', error) + throw error + } + } + /** @type {ePub.Book} */ reader.book = new ePub(reader.ebookUrl, { width: this.readerWidth, height: this.readerHeight - 50, openAs: 'epub', - requestHeaders: { - Authorization: `Bearer ${this.userToken}` - } + requestMethod: customRequest }) /** @type {ePub.Rendition} */ @@ -337,29 +344,33 @@ export default { this.applyTheme() }) - reader.book.ready.then(() => { - // set up event listeners - reader.rendition.on('relocated', reader.relocated) - reader.rendition.on('keydown', reader.keyUp) + reader.book.ready + .then(() => { + // set up event listeners + reader.rendition.on('relocated', reader.relocated) + reader.rendition.on('keydown', reader.keyUp) - reader.rendition.on('touchstart', (event) => { - this.$emit('touchstart', event) - }) - reader.rendition.on('touchend', (event) => { - this.$emit('touchend', event) - }) - - // load ebook cfi locations - const savedLocations = this.loadLocations() - if (savedLocations) { - reader.book.locations.load(savedLocations) - } else { - reader.book.locations.generate().then(() => { - this.checkSaveLocations(reader.book.locations.save()) + reader.rendition.on('touchstart', (event) => { + this.$emit('touchstart', event) }) - } - this.getChapters() - }) + reader.rendition.on('touchend', (event) => { + this.$emit('touchend', event) + }) + + // load ebook cfi locations + const savedLocations = this.loadLocations() + if (savedLocations) { + reader.book.locations.load(savedLocations) + } else { + reader.book.locations.generate().then(() => { + this.checkSaveLocations(reader.book.locations.save()) + }) + } + this.getChapters() + }) + .catch((error) => { + console.error('EpubReader.initEpub failed:', error) + }) }, getChapters() { // Load the list of chapters in the book. See https://github.com/futurepress/epub.js/issues/759 diff --git a/client/components/readers/MobiReader.vue b/client/components/readers/MobiReader.vue index 3e784f77..459ae55b 100644 --- a/client/components/readers/MobiReader.vue +++ b/client/components/readers/MobiReader.vue @@ -26,9 +26,6 @@ export default { return {} }, computed: { - userToken() { - return this.$store.getters['user/getToken'] - }, libraryItemId() { return this.libraryItem?.id }, @@ -96,11 +93,8 @@ export default { }, async initMobi() { // Fetch mobi file as blob - var buff = await this.$axios.$get(this.ebookUrl, { - responseType: 'blob', - headers: { - Authorization: `Bearer ${this.userToken}` - } + const buff = await this.$axios.$get(this.ebookUrl, { + responseType: 'blob' }) var reader = new FileReader() reader.onload = async (event) => { diff --git a/client/components/readers/PdfReader.vue b/client/components/readers/PdfReader.vue index c05f459c..d9459d76 100644 --- a/client/components/readers/PdfReader.vue +++ b/client/components/readers/PdfReader.vue @@ -55,7 +55,8 @@ export default { loadedRatio: 0, page: 1, numPages: 0, - pdfDocInitParams: null + pdfDocInitParams: null, + isRefreshing: false } }, computed: { @@ -152,7 +153,34 @@ export default { this.page++ this.updateProgress() }, - error(err) { + async refreshToken() { + if (this.isRefreshing) return + this.isRefreshing = true + const newAccessToken = await this.$store.dispatch('user/refreshToken').catch((error) => { + console.error('Failed to refresh token', error) + return null + }) + if (!newAccessToken) { + // Redirect to login on failed refresh + this.$router.push('/login') + return + } + + // Force Vue to re-render the PDF component by creating a new object + this.pdfDocInitParams = { + url: this.ebookUrl, + httpHeaders: { + Authorization: `Bearer ${newAccessToken}` + } + } + this.isRefreshing = false + }, + async error(err) { + if (err && err.status === 401) { + console.log('Received 401 error, refreshing token') + await this.refreshToken() + return + } console.error(err) }, resize() { diff --git a/client/components/readers/Reader.vue b/client/components/readers/Reader.vue index c2e5986e..a7a5ac3d 100644 --- a/client/components/readers/Reader.vue +++ b/client/components/readers/Reader.vue @@ -266,9 +266,6 @@ export default { isComic() { return this.ebookFormat == 'cbz' || this.ebookFormat == 'cbr' }, - userToken() { - return this.$store.getters['user/getToken'] - }, keepProgress() { return this.$store.state.ereaderKeepProgress }, diff --git a/client/components/tables/ApiKeysTable.vue b/client/components/tables/ApiKeysTable.vue new file mode 100644 index 00000000..feab4e68 --- /dev/null +++ b/client/components/tables/ApiKeysTable.vue @@ -0,0 +1,177 @@ + + + + + diff --git a/client/components/tables/BackupsTable.vue b/client/components/tables/BackupsTable.vue index 769c8d25..f769abdb 100644 --- a/client/components/tables/BackupsTable.vue +++ b/client/components/tables/BackupsTable.vue @@ -78,10 +78,10 @@ export default { return this.$store.getters['user/getToken'] }, dateFormat() { - return this.$store.state.serverSettings.dateFormat + return this.$store.getters['getServerSetting']('dateFormat') }, timeFormat() { - return this.$store.state.serverSettings.timeFormat + return this.$store.getters['getServerSetting']('timeFormat') } }, methods: { diff --git a/client/components/tables/EbookFilesTable.vue b/client/components/tables/EbookFilesTable.vue index cc968acd..3ce9d30f 100644 --- a/client/components/tables/EbookFilesTable.vue +++ b/client/components/tables/EbookFilesTable.vue @@ -49,9 +49,6 @@ export default { libraryItemId() { return this.libraryItem.id }, - userToken() { - return this.$store.getters['user/getToken'] - }, userCanDownload() { return this.$store.getters['user/getUserCanDownload'] }, diff --git a/client/components/tables/LibraryFilesTable.vue b/client/components/tables/LibraryFilesTable.vue index 9be7e249..6f6e74b8 100644 --- a/client/components/tables/LibraryFilesTable.vue +++ b/client/components/tables/LibraryFilesTable.vue @@ -53,9 +53,6 @@ export default { libraryItemId() { return this.libraryItem.id }, - userToken() { - return this.$store.getters['user/getToken'] - }, userCanDownload() { return this.$store.getters['user/getUserCanDownload'] }, diff --git a/client/components/tables/UsersTable.vue b/client/components/tables/UsersTable.vue index 20f41228..c7171018 100644 --- a/client/components/tables/UsersTable.vue +++ b/client/components/tables/UsersTable.vue @@ -76,10 +76,10 @@ export default { return usermap }, dateFormat() { - return this.$store.state.serverSettings.dateFormat + return this.$store.getters['getServerSetting']('dateFormat') }, timeFormat() { - return this.$store.state.serverSettings.timeFormat + return this.$store.getters['getServerSetting']('timeFormat') } }, methods: { diff --git a/client/components/tables/podcast/LazyEpisodeRow.vue b/client/components/tables/podcast/LazyEpisodeRow.vue index ae99e6d3..ee189961 100644 --- a/client/components/tables/podcast/LazyEpisodeRow.vue +++ b/client/components/tables/podcast/LazyEpisodeRow.vue @@ -112,7 +112,7 @@ export default { return this.episode?.publishedAt }, dateFormat() { - return this.store.state.serverSettings.dateFormat + return this.store.getters['getServerSetting']('dateFormat') }, itemProgress() { return this.store.getters['user/getUserMediaProgress'](this.libraryItemId, this.episodeId) diff --git a/client/components/tables/podcast/LazyEpisodesTable.vue b/client/components/tables/podcast/LazyEpisodesTable.vue index b23cc560..d23ee3d3 100644 --- a/client/components/tables/podcast/LazyEpisodesTable.vue +++ b/client/components/tables/podcast/LazyEpisodesTable.vue @@ -239,10 +239,10 @@ export default { }) }, dateFormat() { - return this.$store.state.serverSettings.dateFormat + return this.$store.getters['getServerSetting']('dateFormat') }, timeFormat() { - return this.$store.state.serverSettings.timeFormat + return this.$store.getters['getServerSetting']('timeFormat') } }, methods: { diff --git a/client/components/ui/MultiSelectQueryInput.vue b/client/components/ui/MultiSelectQueryInput.vue index 6de16bf2..18abc66e 100644 --- a/client/components/ui/MultiSelectQueryInput.vue +++ b/client/components/ui/MultiSelectQueryInput.vue @@ -85,9 +85,6 @@ export default { this.$emit('input', val) } }, - userToken() { - return this.$store.getters['user/getToken'] - }, wrapperClass() { var classes = [] if (this.disabled) classes.push('bg-black-300') diff --git a/client/components/ui/SelectInput.vue b/client/components/ui/SelectInput.vue index 9e0961c1..f38414ac 100644 --- a/client/components/ui/SelectInput.vue +++ b/client/components/ui/SelectInput.vue @@ -1,9 +1,9 @@ @@ -21,6 +21,7 @@ export default { type: String, default: 'text' }, + min: [String, Number], readonly: Boolean, disabled: Boolean, inputClass: String, diff --git a/client/components/ui/VueTrix.vue b/client/components/ui/VueTrix.vue index 2687d934..0836df15 100644 --- a/client/components/ui/VueTrix.vue +++ b/client/components/ui/VueTrix.vue @@ -318,10 +318,8 @@ export default { } }, handleAttachmentAdd(event) { - // Prevent pasting in images from the browser - if (!event.attachment.file) { - event.attachment.remove() - } + // Prevent pasting in images/any files from the browser + event.attachment.remove() } }, mounted() { diff --git a/client/components/widgets/CronExpressionBuilder.vue b/client/components/widgets/CronExpressionBuilder.vue index 77b5a54c..600ed81a 100644 --- a/client/components/widgets/CronExpressionBuilder.vue +++ b/client/components/widgets/CronExpressionBuilder.vue @@ -85,7 +85,7 @@ export default { nextRun() { if (!this.cronExpression) return '' const parsed = this.$getNextScheduledDate(this.cronExpression) - return this.$formatJsDatetime(parsed, this.$store.state.serverSettings.dateFormat, this.$store.state.serverSettings.timeFormat) || '' + return this.$formatJsDatetime(parsed, this.$store.getters['getServerSetting']('dateFormat'), this.$store.getters['getServerSetting']('timeFormat')) || '' }, description() { if ((this.selectedInterval !== 'custom' || !this.selectedWeekdays.length) && this.selectedInterval !== 'daily') return '' diff --git a/client/components/widgets/EncoderOptionsCard.vue b/client/components/widgets/EncoderOptionsCard.vue index 005563b4..977d766b 100644 --- a/client/components/widgets/EncoderOptionsCard.vue +++ b/client/components/widgets/EncoderOptionsCard.vue @@ -143,10 +143,18 @@ export default { localStorage.setItem('embedMetadataCodec', val) }, getEncodingOptions() { - return { - codec: this.selectedCodec || 'aac', - bitrate: this.selectedBitrate || '128k', - channels: this.selectedChannels || 2 + if (this.showAdvancedView) { + return { + codec: this.customCodec || this.selectedCodec || 'aac', + bitrate: this.customBitrate || this.selectedBitrate || '128k', + channels: this.customChannels || this.selectedChannels || 2 + } + } else { + return { + codec: this.selectedCodec || 'aac', + bitrate: this.selectedBitrate || '128k', + channels: this.selectedChannels || 2 + } } }, setPreset() { diff --git a/client/components/widgets/LoadingSpinner.vue b/client/components/widgets/LoadingSpinner.vue index a9c4ef47..8f3de84a 100644 --- a/client/components/widgets/LoadingSpinner.vue +++ b/client/components/widgets/LoadingSpinner.vue @@ -248,4 +248,4 @@ export default { transform: scale(0); } } - \ No newline at end of file + diff --git a/client/components/widgets/SeriesInputWidget.vue b/client/components/widgets/SeriesInputWidget.vue index d6c8cf9f..3dab0605 100644 --- a/client/components/widgets/SeriesInputWidget.vue +++ b/client/components/widgets/SeriesInputWidget.vue @@ -109,4 +109,4 @@ export default { } } } - \ No newline at end of file + diff --git a/client/cypress/tests/components/cards/LazySeriesCard.cy.js b/client/cypress/tests/components/cards/LazySeriesCard.cy.js index 346259d2..12eec692 100644 --- a/client/cypress/tests/components/cards/LazySeriesCard.cy.js +++ b/client/cypress/tests/components/cards/LazySeriesCard.cy.js @@ -40,6 +40,7 @@ describe('LazySeriesCard', () => { }, $store: { getters: { + getServerSetting: () => 'MM/dd/yyyy', 'user/getUserCanUpdate': true, 'user/getUserMediaProgress': (id) => null, 'user/getSizeMultiplier': 1, diff --git a/client/layouts/default.vue b/client/layouts/default.vue index 33e7aa15..9f15af67 100644 --- a/client/layouts/default.vue +++ b/client/layouts/default.vue @@ -33,6 +33,7 @@ export default { return { socket: null, isSocketConnected: false, + isSocketAuthenticated: false, isFirstSocketConnection: true, socketConnectionToastId: null, currentLang: null, @@ -81,9 +82,28 @@ export default { document.body.classList.add('app-bar') } }, + tokenRefreshed(newAccessToken) { + if (this.isSocketConnected && !this.isSocketAuthenticated) { + console.log('[SOCKET] Re-authenticating socket after token refresh') + this.socket.emit('auth', newAccessToken) + } + }, updateSocketConnectionToast(content, type, timeout) { if (this.socketConnectionToastId !== null && this.socketConnectionToastId !== undefined) { - this.$toast.update(this.socketConnectionToastId, { content: content, options: { timeout: timeout, type: type, closeButton: false, position: 'bottom-center', onClose: () => null, closeOnClick: timeout !== null } }, false) + const toastUpdateOptions = { + content: content, + options: { + timeout: timeout, + type: type, + closeButton: false, + position: 'bottom-center', + onClose: () => { + this.socketConnectionToastId = null + }, + closeOnClick: timeout !== null + } + } + this.$toast.update(this.socketConnectionToastId, toastUpdateOptions, false) } else { this.socketConnectionToastId = this.$toast[type](content, { position: 'bottom-center', timeout: timeout, closeButton: false, closeOnClick: timeout !== null }) } @@ -109,7 +129,7 @@ export default { this.updateSocketConnectionToast(this.$strings.ToastSocketDisconnected, 'error', null) }, reconnect() { - console.error('[SOCKET] reconnected') + console.log('[SOCKET] reconnected') }, reconnectAttempt(val) { console.log(`[SOCKET] reconnect attempt ${val}`) @@ -120,6 +140,10 @@ export default { reconnectFailed() { console.error('[SOCKET] reconnect failed') }, + authFailed(payload) { + console.error('[SOCKET] auth failed', payload.message) + this.isSocketAuthenticated = false + }, init(payload) { console.log('Init Payload', payload) @@ -127,7 +151,7 @@ export default { this.$store.commit('users/setUsersOnline', payload.usersOnline) } - this.$eventBus.$emit('socket_init') + this.isSocketAuthenticated = true }, streamOpen(stream) { if (this.$refs.mediaPlayerContainer) this.$refs.mediaPlayerContainer.streamOpen(stream) @@ -354,6 +378,15 @@ export default { this.$store.commit('scanners/removeCustomMetadataProvider', provider) }, initializeSocket() { + if (this.$root.socket) { + // Can happen in dev due to hot reload + console.warn('Socket already initialized') + this.socket = this.$root.socket + this.isSocketConnected = this.$root.socket?.connected + this.isFirstSocketConnection = false + this.socketConnectionToastId = null + return + } this.socket = this.$nuxtSocket({ name: process.env.NODE_ENV === 'development' ? 'dev' : 'prod', persist: 'main', @@ -364,6 +397,7 @@ export default { path: `${this.$config.routerBasePath}/socket.io` }) this.$root.socket = this.socket + this.isSocketAuthenticated = false console.log('Socket initialized') // Pre-defined socket events @@ -377,6 +411,7 @@ export default { // Event received after authorizing socket this.socket.on('init', this.init) + this.socket.on('auth_failed', this.authFailed) // Stream Listeners this.socket.on('stream_open', this.streamOpen) @@ -571,6 +606,7 @@ export default { this.updateBodyClass() this.resize() this.$eventBus.$on('change-lang', this.changeLanguage) + this.$eventBus.$on('token_refreshed', this.tokenRefreshed) window.addEventListener('resize', this.resize) window.addEventListener('keydown', this.keyDown) @@ -594,6 +630,7 @@ export default { }, beforeDestroy() { this.$eventBus.$off('change-lang', this.changeLanguage) + this.$eventBus.$off('token_refreshed', this.tokenRefreshed) window.removeEventListener('resize', this.resize) window.removeEventListener('keydown', this.keyDown) } diff --git a/client/nuxt.config.js b/client/nuxt.config.js index f54d1cf4..7219c784 100644 --- a/client/nuxt.config.js +++ b/client/nuxt.config.js @@ -73,7 +73,8 @@ module.exports = { // Axios module configuration: https://go.nuxtjs.dev/config-axios axios: { - baseURL: routerBasePath + baseURL: routerBasePath, + progress: false }, // nuxt/pwa https://pwa.nuxtjs.org diff --git a/client/package-lock.json b/client/package-lock.json index 23ec14a9..406ef9db 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,12 +1,12 @@ { "name": "audiobookshelf-client", - "version": "2.24.0", + "version": "2.26.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "audiobookshelf-client", - "version": "2.24.0", + "version": "2.26.0", "license": "ISC", "dependencies": { "@nuxtjs/axios": "^5.13.6", diff --git a/client/package.json b/client/package.json index 7985cd78..5ebaab54 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf-client", - "version": "2.24.0", + "version": "2.26.0", "buildNumber": 1, "description": "Self-hosted audiobook and podcast client", "main": "index.js", diff --git a/client/pages/account.vue b/client/pages/account.vue index b157f570..e9b5da3c 100644 --- a/client/pages/account.vue +++ b/client/pages/account.vue @@ -182,18 +182,19 @@ export default { password: this.password, newPassword: this.newPassword }) - .then((res) => { - if (res.success) { - this.$toast.success(this.$strings.ToastUserPasswordChangeSuccess) - this.resetForm() - } else { - this.$toast.error(res.error || this.$strings.ToastUnknownError) - } - this.changingPassword = false + .then(() => { + this.$toast.success(this.$strings.ToastUserPasswordChangeSuccess) + this.resetForm() }) .catch((error) => { - console.error(error) - this.$toast.error(this.$strings.ToastUnknownError) + console.error('Failed to change password', error) + let errorMessage = this.$strings.ToastUnknownError + if (error.response?.data && typeof error.response.data === 'string') { + errorMessage = error.response.data + } + this.$toast.error(errorMessage) + }) + .finally(() => { this.changingPassword = false }) }, diff --git a/client/pages/audiobook/_id/manage.vue b/client/pages/audiobook/_id/manage.vue index 7afe12a9..40672af2 100644 --- a/client/pages/audiobook/_id/manage.vue +++ b/client/pages/audiobook/_id/manage.vue @@ -28,14 +28,14 @@
-
{{ $strings.LabelMetaTag }}
-
{{ $strings.LabelValue }}
+
{{ $strings.LabelMetaTag }}
+
{{ $strings.LabelValue }}