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,
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()
},

View File

@ -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
}

View File

@ -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",

View File

@ -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
*

View File

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

View File

@ -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<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
*

View File

@ -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))

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