From 18ad23d01627cf33bb7636f937ef22841d41e533 Mon Sep 17 00:00:00 2001 From: John Date: Sun, 24 Aug 2025 16:54:38 -0500 Subject: [PATCH] Issue 4540 New SortBy Options: Started Date & Finished Date (#4575) --------- Co-authored-by: advplyr --- client/components/cards/LazyBookCard.vue | 16 ++++++++++++++++ client/components/controls/LibrarySortSelect.vue | 10 +++++++++- client/strings/en-us.json | 4 ++++ server/utils/queries/libraryItemsBookFilters.js | 12 ++++++++---- 4 files changed, 37 insertions(+), 5 deletions(-) diff --git a/client/components/cards/LazyBookCard.vue b/client/components/cards/LazyBookCard.vue index 955e18d92..fbb50bb14 100644 --- a/client/components/cards/LazyBookCard.vue +++ b/client/components/cards/LazyBookCard.vue @@ -353,6 +353,14 @@ export default { if (!this.userProgressLastUpdated) return '\u00A0' return this.$getString('LabelLastProgressDate', [this.$formatDatetime(this.userProgressLastUpdated, this.dateFormat, this.timeFormat)]) } + if (this.orderBy === 'progress.createdAt') { + if (!this.userProgressStartedDate) return '\u00A0' + return this.$getString('LabelStartedDate', [this.$formatDatetime(this.userProgressStartedDate, this.dateFormat, this.timeFormat)]) + } + if (this.orderBy === 'progress.finishedAt') { + if (!this.userProgressFinishedDate) return '\u00A0' + return this.$getString('LabelFinishedDate', [this.$formatDatetime(this.userProgressFinishedDate, this.dateFormat, this.timeFormat)]) + } return null }, episodeProgress() { @@ -389,6 +397,14 @@ export default { if (!this.userProgress) return null return this.userProgress.lastUpdate }, + userProgressStartedDate() { + if (!this.userProgress) return null + return this.userProgress.startedAt + }, + userProgressFinishedDate() { + if (!this.userProgress) return null + return this.userProgress.finishedAt + }, itemIsFinished() { if (this.booksInSeries) return this.seriesIsFinished return this.userProgress ? !!this.userProgress.isFinished : false diff --git a/client/components/controls/LibrarySortSelect.vue b/client/components/controls/LibrarySortSelect.vue index 536c93f12..a0734d6a4 100644 --- a/client/components/controls/LibrarySortSelect.vue +++ b/client/components/controls/LibrarySortSelect.vue @@ -134,6 +134,14 @@ export default { text: this.$strings.LabelLibrarySortByProgress, value: 'progress' }, + { + text: this.$strings.LabelLibrarySortByProgressStarted, + value: 'progress.createdAt' + }, + { + text: this.$strings.LabelLibrarySortByProgressFinished, + value: 'progress.finishedAt' + }, { text: this.$strings.LabelRandomly, value: 'random' @@ -200,4 +208,4 @@ export default { .librarySortMenu { max-height: calc(100vh - 125px); } - \ No newline at end of file + diff --git a/client/strings/en-us.json b/client/strings/en-us.json index 77e9e07a6..6fc55d6f0 100644 --- a/client/strings/en-us.json +++ b/client/strings/en-us.json @@ -378,6 +378,7 @@ "LabelFilterByUser": "Filter by User", "LabelFindEpisodes": "Find Episodes", "LabelFinished": "Finished", + "LabelFinishedDate": "Finished {0}", "LabelFolder": "Folder", "LabelFolders": "Folders", "LabelFontBold": "Bold", @@ -436,6 +437,8 @@ "LabelLibraryItem": "Library Item", "LabelLibraryName": "Library Name", "LabelLibrarySortByProgress": "Progress Updated", + "LabelLibrarySortByProgressFinished": "Finished Date", + "LabelLibrarySortByProgressStarted": "Started Date", "LabelLimit": "Limit", "LabelLineSpacing": "Line spacing", "LabelListenAgain": "Listen Again", @@ -635,6 +638,7 @@ "LabelStartTime": "Start Time", "LabelStarted": "Started", "LabelStartedAt": "Started At", + "LabelStartedDate": "Started {0}", "LabelStatsAudioTracks": "Audio Tracks", "LabelStatsAuthors": "Authors", "LabelStatsBestDay": "Best Day", diff --git a/server/utils/queries/libraryItemsBookFilters.js b/server/utils/queries/libraryItemsBookFilters.js index 85d7f3877..494a9564f 100644 --- a/server/utils/queries/libraryItemsBookFilters.js +++ b/server/utils/queries/libraryItemsBookFilters.js @@ -289,7 +289,11 @@ module.exports = { const nullDir = sortDesc ? 'DESC NULLS FIRST' : 'ASC NULLS LAST' return [[Sequelize.literal(`CAST(\`series.bookSeries.sequence\` AS FLOAT) ${nullDir}`)]] } else if (sortBy === 'progress') { - return [[Sequelize.literal('mediaProgresses.updatedAt'), dir]] + return [[Sequelize.literal(`mediaProgresses.updatedAt ${dir} NULLS LAST`)]] + } else if (sortBy === 'progress.createdAt') { + return [[Sequelize.literal(`mediaProgresses.createdAt ${dir} NULLS LAST`)]] + } else if (sortBy === 'progress.finishedAt') { + return [[Sequelize.literal(`mediaProgresses.finishedAt ${dir} NULLS LAST`)]] } else if (sortBy === 'random') { return [Database.sequelize.random()] } @@ -519,7 +523,7 @@ module.exports = { } bookIncludes.push({ model: Database.mediaProgressModel, - attributes: ['id', 'isFinished', 'currentTime', 'ebookProgress', 'updatedAt'], + attributes: ['id', 'isFinished', 'currentTime', 'ebookProgress', 'updatedAt', 'createdAt', 'finishedAt'], where: mediaProgressWhere, required: false }) @@ -530,10 +534,10 @@ module.exports = { } // When sorting by progress but not filtering by progress, include media progresses - if (filterGroup !== 'progress' && sortBy === 'progress') { + if (filterGroup !== 'progress' && ['progress.createdAt', 'progress.finishedAt', 'progress'].includes(sortBy)) { bookIncludes.push({ model: Database.mediaProgressModel, - attributes: ['id', 'isFinished', 'currentTime', 'ebookProgress', 'updatedAt'], + attributes: ['id', 'isFinished', 'currentTime', 'ebookProgress', 'updatedAt', 'createdAt', 'finishedAt'], where: { userId: user.id },