Add ability to hide and unhide series

List of hidden series IDs is stored in user.extraData and hidden series
are filtered out of the query on the server side.
This commit is contained in:
DoctorDalek1963 2025-11-03 22:05:13 +00:00
parent 0c7b738b7c
commit 8318ac33e9
No known key found for this signature in database
8 changed files with 133 additions and 0 deletions

View File

@ -131,6 +131,10 @@ export default {
{ {
text: this.isSeriesFinished ? this.$strings.MessageMarkAsNotFinished : this.$strings.MessageMarkAsFinished, text: this.isSeriesFinished ? this.$strings.MessageMarkAsNotFinished : this.$strings.MessageMarkAsFinished,
action: 'mark-series-finished' 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 if (!this.seriesId) return false
return this.$store.getters['user/getIsSeriesRemovedFromContinueListening'](this.seriesId) return this.$store.getters['user/getIsSeriesRemovedFromContinueListening'](this.seriesId)
}, },
isSeriesHidden() {
if (!this.seriesId) return false
return this.$store.getters['user/getIsSeriesHidden'](this.seriesId)
},
filterBy() { filterBy() {
return this.$store.getters['user/getUserSetting']('filterBy') return this.$store.getters['user/getUserSetting']('filterBy')
}, },
@ -439,6 +447,12 @@ export default {
return return
} }
this.markSeriesFinished() this.markSeriesFinished()
} else if (action === 'hide-series') {
if (this.processingSeries) {
console.warn('Already processing series')
return
}
this.markSeriesHidden()
} else if (this.handleSubtitlesAction(action)) { } else if (this.handleSubtitlesAction(action)) {
return return
} else if (this.handleCollapseSubSeriesAction(action)) { } else if (this.handleCollapseSubSeriesAction(action)) {
@ -566,6 +580,33 @@ export default {
} }
this.$store.commit('globals/setConfirmPrompt', payload) 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() { updateOrder() {
this.saveSettings() this.saveSettings()
}, },

View File

@ -76,6 +76,10 @@ export const getters = {
if (!state.user || !state.user.seriesHideFromContinueListening || !state.user.seriesHideFromContinueListening.length) return false if (!state.user || !state.user.seriesHideFromContinueListening || !state.user.seriesHideFromContinueListening.length) return false
return state.user.seriesHideFromContinueListening.includes(seriesId) 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) => { getSizeMultiplier: (state) => {
return state.settings.bookshelfCoverSize / 120 return state.settings.bookshelfCoverSize / 120
} }

View File

@ -394,6 +394,7 @@
"LabelHardDeleteFile": "Hard delete file", "LabelHardDeleteFile": "Hard delete file",
"LabelHasEbook": "Has ebook", "LabelHasEbook": "Has ebook",
"LabelHasSupplementaryEbook": "Has supplementary ebook", "LabelHasSupplementaryEbook": "Has supplementary ebook",
"LabelHideSeries": "Hide Series",
"LabelHideSubtitles": "Hide Subtitles", "LabelHideSubtitles": "Hide Subtitles",
"LabelHighestPriority": "Highest priority", "LabelHighestPriority": "Highest priority",
"LabelHost": "Host", "LabelHost": "Host",
@ -699,6 +700,7 @@
"LabelType": "Type", "LabelType": "Type",
"LabelUnabridged": "Unabridged", "LabelUnabridged": "Unabridged",
"LabelUndo": "Undo", "LabelUndo": "Undo",
"LabelUnhideSeries": "Unhide Series",
"LabelUnknown": "Unknown", "LabelUnknown": "Unknown",
"LabelUnknownPublishDate": "Unknown publish date", "LabelUnknownPublishDate": "Unknown publish date",
"LabelUpdateCover": "Update Cover", "LabelUpdateCover": "Update Cover",
@ -774,6 +776,7 @@
"MessageConfirmDeleteSession": "Are you sure you want to delete this session?", "MessageConfirmDeleteSession": "Are you sure you want to delete this session?",
"MessageConfirmEmbedMetadataInAudioFiles": "Are you sure you want to embed metadata in {0} audio files?", "MessageConfirmEmbedMetadataInAudioFiles": "Are you sure you want to embed metadata in {0} audio files?",
"MessageConfirmForceReScan": "Are you sure you want to force re-scan?", "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?", "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?", "MessageConfirmMarkAllEpisodesNotFinished": "Are you sure you want to mark all episodes as not finished?",
"MessageConfirmMarkItemFinished": "Are you sure you want to mark \"{0}\" as 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}\".", "MessageConfirmRenameTagWarning": "Warning! A similar tag with a different casing already exists \"{0}\".",
"MessageConfirmResetProgress": "Are you sure you want to reset your progress?", "MessageConfirmResetProgress": "Are you sure you want to reset your progress?",
"MessageConfirmSendEbookToDevice": "Are you sure you want to send {0} ebook \"{1}\" to device \"{2}\"?", "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?", "MessageConfirmUnlinkOpenId": "Are you sure you want to unlink this user from OpenID?",
"MessageDaysListenedInTheLastYear": "{0} days listened in the last year", "MessageDaysListenedInTheLastYear": "{0} days listened in the last year",
"MessageDownloadingEpisode": "Downloading episode", "MessageDownloadingEpisode": "Downloading episode",

View File

@ -381,6 +381,44 @@ class MeController {
res.json(req.user.toOldJSONForBrowser()) 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 * GET: api/me/progress/:id/remove-from-continue-listening
* *

View File

@ -178,6 +178,7 @@ class UserController {
permissions, permissions,
bookmarks: [], bookmarks: [],
extraData: { extraData: {
hiddenSeries: [],
seriesHideFromContinueListening: [] seriesHideFromContinueListening: []
} }
} }

View File

@ -603,6 +603,7 @@ class User extends Model {
*/ */
toOldJSONForBrowser(hideRootToken = false, minimal = false) { toOldJSONForBrowser(hideRootToken = false, minimal = false) {
const seriesHideFromContinueListening = this.extraData?.seriesHideFromContinueListening || [] const seriesHideFromContinueListening = this.extraData?.seriesHideFromContinueListening || []
const hiddenSeries = this.extraData?.hiddenSeries || []
const librariesAccessible = this.permissions?.librariesAccessible || [] const librariesAccessible = this.permissions?.librariesAccessible || []
const itemTagsSelected = this.permissions?.itemTagsSelected || [] const itemTagsSelected = this.permissions?.itemTagsSelected || []
const permissions = { ...this.permissions } const permissions = { ...this.permissions }
@ -621,6 +622,7 @@ class User extends Model {
isOldToken: this.isOldToken, isOldToken: this.isOldToken,
mediaProgress: this.mediaProgresses?.map((mp) => mp.getOldMediaProgress()) || [], mediaProgress: this.mediaProgresses?.map((mp) => mp.getOldMediaProgress()) || [],
seriesHideFromContinueListening: [...seriesHideFromContinueListening], seriesHideFromContinueListening: [...seriesHideFromContinueListening],
hiddenSeries: [...hiddenSeries],
bookmarks: this.bookmarks?.map((b) => ({ ...b })) || [], bookmarks: this.bookmarks?.map((b) => ({ ...b })) || [],
isActive: this.isActive, isActive: this.isActive,
isLocked: this.isLocked, isLocked: this.isLocked,
@ -938,6 +940,38 @@ class User extends Model {
return true return true
} }
/**
*
* @param {string} seriesId
* @returns {Promise<boolean>}
*/
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<boolean>}
*/
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 * Update user permissions from external JSON
* *

View File

@ -186,6 +186,8 @@ class ApiRouter {
this.router.get('/me/items-in-progress', MeController.getAllLibraryItemsInProgress.bind(this)) 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/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/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.get('/me/stats/year/:year', MeController.getStatsForYear.bind(this))
this.router.post('/me/ereader-devices', MeController.updateUserEReaderDevices.bind(this)) this.router.post('/me/ereader-devices', MeController.updateUserEReaderDevices.bind(this))

View File

@ -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) { if (attrQuery) {
seriesWhere.push( seriesWhere.push(
Sequelize.where(Sequelize.literal(`(${attrQuery})`), { Sequelize.where(Sequelize.literal(`(${attrQuery})`), {