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/app/SideRail.vue b/client/components/app/SideRail.vue index 2b05ef36..5f364201 100644 --- a/client/components/app/SideRail.vue +++ b/client/components/app/SideRail.vue @@ -116,7 +116,7 @@
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/BookMatchCard.vue b/client/components/cards/BookMatchCard.vue index 87aa0a71..09b963c5 100644 --- a/client/components/cards/BookMatchCard.vue +++ b/client/components/cards/BookMatchCard.vue @@ -13,9 +13,17 @@{{ book.publishedYear }}
-{{ $getString('LabelByAuthor', [book.author]) }}
-{{ $strings.LabelNarrators }}: {{ book.narrator }}
-{{ $strings.LabelDuration }}: {{ $elapsedPrettyExtended(bookDuration, false) }} {{ bookDurationComparison }}
+ +{{ $getString('LabelByAuthor', [book.author]) }}
+{{ $strings.LabelNarrators }}: {{ book.narrator }}
+{{ $strings.LabelDuration }}: {{ $elapsedPrettyExtended(bookDuration, false) }} {{ bookDurationComparison }}
+diff --git a/client/components/cards/LazyBookCard.vue b/client/components/cards/LazyBookCard.vue index 41b73310..955e18d9 100644 --- a/client/components/cards/LazyBookCard.vue +++ b/client/components/cards/LazyBookCard.vue @@ -101,7 +101,8 @@
- Episode #{{ recentEpisodeNumber }} + Episode + #{{ recentEpisodeNumber }}
{{ title }}
+{{ title }}
+{{ $strings.LabelUser }}
-{{ _session.userId }}
+{{ username }}
{{ $strings.LabelMediaPlayer }}
{{ playMethodName }}
@@ -132,6 +132,9 @@ export default { _session() { return this.session || {} }, + username() { + return this._session.user?.username || this._session.userId || '' + }, deviceInfo() { return this._session.deviceInfo || {} }, 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/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/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..a4c51539 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,27 +94,37 @@ 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' - const fontColor = isDark ? '#fff' : '#000' - const backgroundColor = isDark ? 'rgb(35 35 35)' : 'rgb(255, 255, 255)' + const theme = this.ereaderSettings.theme + const isDark = theme === 'dark' + const isSepia = theme === 'sepia' + + const fontColor = isDark + ? '#fff' + : isSepia + ? '#5b4636' + : '#000' + + const backgroundColor = isDark + ? 'rgb(35 35 35)' + : isSepia + ? 'rgb(244, 236, 216)' + : 'rgb(255, 255, 255)' const lineSpacing = this.ereaderSettings.lineSpacing / 100 - - const fontScale = this.ereaderSettings.fontScale / 100 - - const textStroke = this.ereaderSettings.textStroke / 100 + const fontScale = this.ereaderSettings.fontScale / 100 + const textStroke = this.ereaderSettings.textStroke / 100 return { '*': { color: `${fontColor}!important`, 'background-color': `${backgroundColor}!important`, - 'line-height': lineSpacing * fontScale + 'rem!important', - '-webkit-text-stroke': textStroke + 'px ' + fontColor + '!important' + 'line-height': `${lineSpacing * fontScale}rem!important`, + '-webkit-text-stroke': `${textStroke}px ${fontColor}!important` }, a: { color: `${fontColor}!important` @@ -309,14 +316,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 +354,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..9063479d 100644 --- a/client/components/readers/Reader.vue +++ b/client/components/readers/Reader.vue @@ -1,5 +1,5 @@ -