diff --git a/client/components/app/BookShelfToolbar.vue b/client/components/app/BookShelfToolbar.vue index b7ecff624..70149eef7 100644 --- a/client/components/app/BookShelfToolbar.vue +++ b/client/components/app/BookShelfToolbar.vue @@ -131,6 +131,10 @@ export default { { text: this.isSeriesFinished ? this.$strings.MessageMarkAsNotFinished : this.$strings.MessageMarkAsFinished, action: 'mark-series-finished' + }, + { + text: !this.isSeriesHidden ? this.$strings.LabelHideSeries : this.$strings.LabelUnhideSeries, + action: 'hide-series' } ] @@ -301,6 +305,10 @@ export default { if (!this.seriesId) return false return this.$store.getters['user/getIsSeriesRemovedFromContinueListening'](this.seriesId) }, + isSeriesHidden() { + if (!this.seriesId) return false + return this.$store.getters['user/getIsSeriesHidden'](this.seriesId) + }, filterBy() { return this.$store.getters['user/getUserSetting']('filterBy') }, @@ -439,6 +447,12 @@ export default { return } this.markSeriesFinished() + } else if (action === 'hide-series') { + if (this.processingSeries) { + console.warn('Already processing series') + return + } + this.markSeriesHidden() } else if (this.handleSubtitlesAction(action)) { return } else if (this.handleCollapseSubSeriesAction(action)) { @@ -566,6 +580,33 @@ export default { } this.$store.commit('globals/setConfirmPrompt', payload) }, + markSeriesHidden() { + const newIsHidden = !this.isSeriesHidden; + + const payload = { + message: newIsHidden ? this.$strings.MessageConfirmHideSeries : this.$strings.MessageConfirmUnhideSeries, + callback: (confirmed) => { + if (confirmed) { + this.processingSeries = true + const endpoint = newIsHidden ? 'hide' : 'unhide' + this.$axios + .$get(`/api/me/series/${this.seriesId}/${endpoint}`) + .then(() => { + this.$toast.success(this.$strings.ToastSeriesUpdateSuccess) + }) + .catch((error) => { + this.$toast.error(this.$strings.ToastSeriesUpdateFailed) + console.error('Failed to batch update read/not read', error) + }) + .finally(() => { + this.processingSeries = false + }) + } + }, + type: 'yesNo' + } + this.$store.commit('globals/setConfirmPrompt', payload) + }, updateOrder() { this.saveSettings() }, diff --git a/client/store/user.js b/client/store/user.js index 96e79d12f..8a42e5996 100644 --- a/client/store/user.js +++ b/client/store/user.js @@ -76,6 +76,10 @@ export const getters = { if (!state.user || !state.user.seriesHideFromContinueListening || !state.user.seriesHideFromContinueListening.length) return false return state.user.seriesHideFromContinueListening.includes(seriesId) }, + getIsSeriesHidden: (state) => (seriesId) => { + if (!state.user || !state.user.hiddenSeries || !state.user.hiddenSeries.length) return false + return state.user.hiddenSeries.includes(seriesId) + }, getSizeMultiplier: (state) => { return state.settings.bookshelfCoverSize / 120 } diff --git a/client/strings/en-us.json b/client/strings/en-us.json index 83acb5a69..1fa8ce62e 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -394,6 +394,7 @@ "LabelHardDeleteFile": "Hard delete file", "LabelHasEbook": "Has ebook", "LabelHasSupplementaryEbook": "Has supplementary ebook", + "LabelHideSeries": "Hide Series", "LabelHideSubtitles": "Hide Subtitles", "LabelHighestPriority": "Highest priority", "LabelHost": "Host", @@ -699,6 +700,7 @@ "LabelType": "Type", "LabelUnabridged": "Unabridged", "LabelUndo": "Undo", + "LabelUnhideSeries": "Unhide Series", "LabelUnknown": "Unknown", "LabelUnknownPublishDate": "Unknown publish date", "LabelUpdateCover": "Update Cover", @@ -774,6 +776,7 @@ "MessageConfirmDeleteSession": "Are you sure you want to delete this session?", "MessageConfirmEmbedMetadataInAudioFiles": "Are you sure you want to embed metadata in {0} audio files?", "MessageConfirmForceReScan": "Are you sure you want to force re-scan?", + "MessageConfirmHideSeries": "Are you sure you want to hide this series?", "MessageConfirmMarkAllEpisodesFinished": "Are you sure you want to mark all episodes as finished?", "MessageConfirmMarkAllEpisodesNotFinished": "Are you sure you want to mark all episodes as not finished?", "MessageConfirmMarkItemFinished": "Are you sure you want to mark \"{0}\" as finished?", @@ -804,6 +807,7 @@ "MessageConfirmRenameTagWarning": "Warning! A similar tag with a different casing already exists \"{0}\".", "MessageConfirmResetProgress": "Are you sure you want to reset your progress?", "MessageConfirmSendEbookToDevice": "Are you sure you want to send {0} ebook \"{1}\" to device \"{2}\"?", + "MessageConfirmUnhideSeries": "Are you sure you want to unhide this series?", "MessageConfirmUnlinkOpenId": "Are you sure you want to unlink this user from OpenID?", "MessageDaysListenedInTheLastYear": "{0} days listened in the last year", "MessageDownloadingEpisode": "Downloading episode", diff --git a/server/controllers/MeController.js b/server/controllers/MeController.js index 51773a5ad..a2328bc8c 100644 --- a/server/controllers/MeController.js +++ b/server/controllers/MeController.js @@ -381,6 +381,44 @@ class MeController { res.json(req.user.toOldJSONForBrowser()) } + /** + * GET: /api/me/series/:id/hide + * + * @param {RequestWithUser} req + * @param {Response} res + */ + async hideSeries(req, res) { + if (!(await Database.seriesModel.checkExistsById(req.params.id))) { + Logger.error(`[MeController] hideSeries: Series ${req.params.id} not found`) + return res.sendStatus(404) + } + + const hasUpdated = await req.user.hideSeries(req.params.id) + if (hasUpdated) { + SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toOldJSONForBrowser()) + } + res.json(req.user.toOldJSONForBrowser()) + } + + /** + * GET: /api/me/series/:id/unhide + * + * @param {RequestWithUser} req + * @param {Response} res + */ + async unhideSeries(req, res) { + if (!(await Database.seriesModel.checkExistsById(req.params.id))) { + Logger.error(`[MeController] unhideSeries: Series ${req.params.id} not found`) + return res.sendStatus(404) + } + + const hasUpdated = await req.user.unhideSeries(req.params.id) + if (hasUpdated) { + SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toOldJSONForBrowser()) + } + res.json(req.user.toOldJSONForBrowser()) + } + /** * GET: api/me/progress/:id/remove-from-continue-listening * diff --git a/server/controllers/UserController.js b/server/controllers/UserController.js index 3ec10539e..54e6500a9 100644 --- a/server/controllers/UserController.js +++ b/server/controllers/UserController.js @@ -178,6 +178,7 @@ class UserController { permissions, bookmarks: [], extraData: { + hiddenSeries: [], seriesHideFromContinueListening: [] } } diff --git a/server/models/User.js b/server/models/User.js index 36b2eca98..c4d0e8d48 100644 --- a/server/models/User.js +++ b/server/models/User.js @@ -603,6 +603,7 @@ class User extends Model { */ toOldJSONForBrowser(hideRootToken = false, minimal = false) { const seriesHideFromContinueListening = this.extraData?.seriesHideFromContinueListening || [] + const hiddenSeries = this.extraData?.hiddenSeries || [] const librariesAccessible = this.permissions?.librariesAccessible || [] const itemTagsSelected = this.permissions?.itemTagsSelected || [] const permissions = { ...this.permissions } @@ -621,6 +622,7 @@ class User extends Model { isOldToken: this.isOldToken, mediaProgress: this.mediaProgresses?.map((mp) => mp.getOldMediaProgress()) || [], seriesHideFromContinueListening: [...seriesHideFromContinueListening], + hiddenSeries: [...hiddenSeries], bookmarks: this.bookmarks?.map((b) => ({ ...b })) || [], isActive: this.isActive, isLocked: this.isLocked, @@ -938,6 +940,38 @@ class User extends Model { return true } + /** + * + * @param {string} seriesId + * @returns {Promise} + */ + async hideSeries(seriesId) { + if (!this.extraData) this.extraData = {} + const hiddenSeries = this.extraData.hiddenSeries || [] + if (hiddenSeries.includes(seriesId)) return false + hiddenSeries.push(seriesId) + this.extraData.hiddenSeries = hiddenSeries + this.changed('extraData', true) + await this.save() + return true + } + + /** + * + * @param {string} seriesId + * @returns {Promise} + */ + async unhideSeries(seriesId) { + if (!this.extraData) this.extraData = {} + let hiddenSeries = this.extraData.hiddenSeries || [] + if (!hiddenSeries.includes(seriesId)) return false + hiddenSeries = hiddenSeries.filter((sid) => sid !== seriesId) + this.extraData.hiddenSeries = hiddenSeries + this.changed('extraData', true) + await this.save() + return true + } + /** * Update user permissions from external JSON * diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index db04bf5ec..4b644d920 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -186,6 +186,8 @@ class ApiRouter { this.router.get('/me/items-in-progress', MeController.getAllLibraryItemsInProgress.bind(this)) this.router.get('/me/series/:id/remove-from-continue-listening', MeController.removeSeriesFromContinueListening.bind(this)) this.router.get('/me/series/:id/readd-to-continue-listening', MeController.readdSeriesFromContinueListening.bind(this)) + this.router.get('/me/series/:id/hide', MeController.hideSeries.bind(this)) + this.router.get('/me/series/:id/unhide', MeController.unhideSeries.bind(this)) this.router.get('/me/stats/year/:year', MeController.getStatsForYear.bind(this)) this.router.post('/me/ereader-devices', MeController.updateUserEReaderDevices.bind(this)) diff --git a/server/utils/queries/seriesFilters.js b/server/utils/queries/seriesFilters.js index ed71e5b3f..24cbb5bc5 100644 --- a/server/utils/queries/seriesFilters.js +++ b/server/utils/queries/seriesFilters.js @@ -106,6 +106,15 @@ module.exports = { } } + // Don't return hidden series + if (user.extraData.hiddenSeries) { + seriesWhere.push({ + id: { + [Sequelize.Op.notIn]: user.extraData.hiddenSeries + } + }) + } + if (attrQuery) { seriesWhere.push( Sequelize.where(Sequelize.literal(`(${attrQuery})`), {