From fc325a409567447f2e3e0abc9c5b2e1c68067343 Mon Sep 17 00:00:00 2001 From: Jens Bickel <235654990+JensBickel@users.noreply.github.com> Date: Mon, 9 Feb 2026 17:19:21 +0100 Subject: [PATCH] feat(client): add placeholder UX for series/upload flows, card styling/action guards, and localized strings --- client/components/app/BookShelfToolbar.vue | 32 ++++ client/components/app/LazyBookshelf.vue | 13 ++ client/components/cards/ItemUploadCard.vue | 40 ++++- client/components/cards/LazyBookCard.vue | 76 ++++++++- client/components/cards/LazySeriesCard.vue | 6 +- .../app/BookShelfToolbar.placeholder.cy.js | 147 ++++++++++++++++++ .../tests/components/cards/LazyBookCard.cy.js | 28 ++++ .../components/cards/LazySeriesCard.cy.js | 13 ++ .../components/pages/Upload.placeholder.cy.js | 122 +++++++++++++++ client/pages/upload/index.vue | 77 ++++++++- client/strings/en-us.json | 16 +- package-lock.json | 4 +- package.json | 7 +- server/controllers/FileSystemController.js | 16 +- server/controllers/LibraryController.js | 1 + server/controllers/MiscController.js | 79 ++++++++-- server/controllers/SeriesController.js | 17 +- server/utils/queries/libraryFilters.js | 4 +- .../utils/queries/libraryItemsBookFilters.js | 8 +- server/utils/queries/seriesFilters.js | 3 +- .../SeriesController.placeholders.test.js | 93 +++++++++++ .../LibraryScanner.placeholders.test.js | 9 +- ...braryItemsBookFilters.placeholders.test.js | 104 ++++++++++--- 23 files changed, 850 insertions(+), 65 deletions(-) create mode 100644 client/cypress/tests/components/app/BookShelfToolbar.placeholder.cy.js create mode 100644 client/cypress/tests/components/pages/Upload.placeholder.cy.js diff --git a/client/components/app/BookShelfToolbar.vue b/client/components/app/BookShelfToolbar.vue index b7ecff624..57054dce7 100644 --- a/client/components/app/BookShelfToolbar.vue +++ b/client/components/app/BookShelfToolbar.vue @@ -134,6 +134,13 @@ export default { } ] + if (this.userCanUpdate && this.isBookLibrary) { + items.push({ + text: this.$strings.ButtonAddPlaceholder, + action: 'add-placeholder' + }) + } + if (this.userIsAdminOrUp || this.selectedSeries.rssFeed) { items.push({ text: this.$strings.LabelOpenRSSFeed, @@ -427,6 +434,12 @@ export default { seriesContextMenuAction({ action }) { if (action === 'open-rss-feed') { this.showOpenSeriesRSSFeed() + } else if (action === 'add-placeholder') { + if (this.processingSeries) { + console.warn('Already processing series') + return + } + this.createSeriesPlaceholder() } else if (action === 're-add-to-continue-listening') { if (this.processingSeries) { console.warn('Already processing series') @@ -566,6 +579,25 @@ export default { } this.$store.commit('globals/setConfirmPrompt', payload) }, + async createSeriesPlaceholder() { + if (!this.seriesId) return + this.processingSeries = true + const payload = { + title: this.$strings.LabelPlaceholderDefaultTitle + } + try { + const libraryItem = await this.$axios.$post(`/api/libraries/${this.currentLibraryId}/series/${this.seriesId}/placeholders`, payload) + this.$toast.success(this.$strings.ToastPlaceholderCreated) + this.$store.commit('setBookshelfBookIds', []) + this.$store.commit('showEditModal', libraryItem) + this.$eventBus.$emit('series-bookshelf-refresh', { seriesId: this.seriesId }) + } catch (error) { + console.error('Failed to create placeholder', error) + this.$toast.error(this.$strings.ToastPlaceholderCreateFailed) + } finally { + this.processingSeries = false + } + }, updateOrder() { this.saveSettings() }, diff --git a/client/components/app/LazyBookshelf.vue b/client/components/app/LazyBookshelf.vue index 4c72d0d78..3baf90c03 100644 --- a/client/components/app/LazyBookshelf.vue +++ b/client/components/app/LazyBookshelf.vue @@ -566,8 +566,19 @@ export default { this.entityComponentRefs[indexOf].setEntity(libraryItem) } } + if (this.entityName === 'series-books' && this.seriesId) { + const seriesIds = libraryItem.media?.metadata?.series?.map((se) => se.id) || [] + if (!seriesIds.includes(this.seriesId)) { + this.resetEntities(this.currScrollTop) + } + } } }, + seriesBookshelfRefresh({ seriesId } = {}) { + if (this.entityName !== 'series-books') return + if (seriesId && this.seriesId && seriesId !== this.seriesId) return + this.resetEntities(this.currScrollTop) + }, routeToBookshelfIfLastIssueRemoved() { if (this.totalEntities === 0) { const currentRouteQuery = this.$route.query @@ -791,6 +802,7 @@ export default { this.$eventBus.$on('bookshelf_clear_selection', this.clearSelectedEntities) this.$eventBus.$on('user-settings', this.settingsUpdated) + this.$eventBus.$on('series-bookshelf-refresh', this.seriesBookshelfRefresh) if (this.$root.socket) { this.$root.socket.on('item_updated', this.libraryItemUpdated) @@ -822,6 +834,7 @@ export default { this.$eventBus.$off('bookshelf_clear_selection', this.clearSelectedEntities) this.$eventBus.$off('user-settings', this.settingsUpdated) + this.$eventBus.$off('series-bookshelf-refresh', this.seriesBookshelfRefresh) if (this.$root.socket) { this.$root.socket.off('item_updated', this.libraryItemUpdated) diff --git a/client/components/cards/ItemUploadCard.vue b/client/components/cards/ItemUploadCard.vue index adbf3adbe..7767e402e 100644 --- a/client/components/cards/ItemUploadCard.vue +++ b/client/components/cards/ItemUploadCard.vue @@ -40,7 +40,10 @@
-
+
+ +
+