Sort videos within playlist (#4921)

* Implement playlist sorting

* Hide sort menu for remote playlists

* Remove 'Custom (descending)' sort order

I don't see the need for this particular sort order.

* Adjust sort order & align dropdown with 'More Options' button

* Make 'Latest added first' default option instead of custom

* Remove unlikely-to-be-implemented 'Date published' sorting options

Context from absidue: 'I don't think we should even attempt to support it, due to all of the situations where it wouldn't be possible.'

* Update to use sortOrder as main variable throughout

* Hide sort menu for playlists of length <2
This commit is contained in:
Jason 2024-04-16 22:26:43 +00:00 committed by GitHub
parent ea35a13614
commit 9815ed3da2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 111 additions and 8 deletions

View File

@ -107,7 +107,9 @@ export default defineComponent({
newPlaylistDefaultProperties: function () {
return this.$store.getters.getNewPlaylistDefaultProperties
},
locale: function () {
return this.$i18n.locale.replace('_', '-')
},
processedQuery: function() {
return this.query.trim().toLowerCase()
},

View File

@ -280,6 +280,7 @@ const state = {
thumbnailPreference: '',
blurThumbnails: false,
useProxy: false,
userPlaylistSortOrder: 'date_added_descending',
useRssFeeds: false,
useSponsorBlock: false,
videoVolumeMouseScroll: false,

View File

@ -7,6 +7,7 @@ import PlaylistInfo from '../../components/playlist-info/playlist-info.vue'
import FtListVideoNumbered from '../../components/ft-list-video-numbered/ft-list-video-numbered.vue'
import FtFlexBox from '../../components/ft-flex-box/ft-flex-box.vue'
import FtButton from '../../components/ft-button/ft-button.vue'
import FtSelect from '../../components/ft-select/ft-select.vue'
import FtAutoLoadNextPageWrapper from '../../components/ft-auto-load-next-page-wrapper/ft-auto-load-next-page-wrapper.vue'
import {
getLocalPlaylist,
@ -16,6 +17,16 @@ import {
import { extractNumberFromString, setPublishedTimestampsInvidious, showToast } from '../../helpers/utils'
import { invidiousGetPlaylistInfo, youtubeImageUrlToInvidious } from '../../helpers/api/invidious'
const SORT_BY_VALUES = {
DateAddedNewest: 'date_added_descending',
DateAddedOldest: 'date_added_ascending',
AuthorAscending: 'author_ascending',
AuthorDescending: 'author_descending',
VideoTitleAscending: 'video_title_ascending',
VideoTitleDescending: 'video_title_descending',
Custom: 'custom',
}
export default defineComponent({
name: 'Playlist',
components: {
@ -25,6 +36,7 @@ export default defineComponent({
'ft-list-video-numbered': FtListVideoNumbered,
'ft-flex-box': FtFlexBox,
'ft-button': FtButton,
'ft-select': FtSelect,
'ft-auto-load-next-page-wrapper': FtAutoLoadNextPageWrapper,
},
beforeRouteLeave(to, from, next) {
@ -77,6 +89,12 @@ export default defineComponent({
currentInvidiousInstance: function () {
return this.$store.getters.getCurrentInvidiousInstance
},
userPlaylistSortOrder: function () {
return this.$store.getters.getUserPlaylistSortOrder
},
sortOrder: function () {
return this.isUserPlaylistRequested ? this.userPlaylistSortOrder : SORT_BY_VALUES.Custom
},
currentLocale: function () {
return this.$i18n.locale.replace('_', '-')
},
@ -138,10 +156,10 @@ export default defineComponent({
},
sometimesFilteredUserPlaylistItems() {
if (!this.isUserPlaylistRequested) { return this.playlistItems }
if (this.processedVideoSearchQuery === '') { return this.playlistItems }
if (!this.isUserPlaylistRequested) { return this.sortedPlaylistItems }
if (this.processedVideoSearchQuery === '') { return this.sortedPlaylistItems }
return this.playlistItems.filter((v) => {
return this.sortedPlaylistItems.filter((v) => {
if (typeof (v.title) === 'string' && v.title.toLowerCase().includes(this.processedVideoSearchQuery)) {
return true
} else if (typeof (v.author) === 'string' && v.author.toLowerCase().includes(this.processedVideoSearchQuery)) {
@ -151,10 +169,41 @@ export default defineComponent({
return false
})
},
sortByValues() {
return Object.values(SORT_BY_VALUES)
},
isSortOrderCustom() {
return this.sortOrder === SORT_BY_VALUES.Custom
},
sortedPlaylistItems: function () {
if (this.sortOrder === SORT_BY_VALUES.Custom) {
return this.playlistItems
}
return this.playlistItems.toSorted((a, b) => {
switch (this.sortOrder) {
case SORT_BY_VALUES.DateAddedNewest:
return b.timeAdded - a.timeAdded
case SORT_BY_VALUES.DateAddedOldest:
return a.timeAdded - b.timeAdded
case SORT_BY_VALUES.VideoTitleAscending:
return a.title.localeCompare(b.title, this.currentLocale)
case SORT_BY_VALUES.VideoTitleDescending:
return b.title.localeCompare(a.title, this.currentLocale)
case SORT_BY_VALUES.AuthorAscending:
return a.author.localeCompare(b.author, this.currentLocale)
case SORT_BY_VALUES.AuthorDescending:
return b.author.localeCompare(a.author, this.currentLocale)
default:
console.error(`Unknown sortOrder: ${this.sortOrder}`)
return 0
}
})
},
visiblePlaylistItems: function () {
if (!this.isUserPlaylistRequested) {
// No filtering for non user playlists yet
return this.playlistItems
return this.sortedPlaylistItems
}
if (this.userPlaylistVisibleLimit < this.sometimesFilteredUserPlaylistItems.length) {
@ -166,6 +215,32 @@ export default defineComponent({
processedVideoSearchQuery() {
return this.videoSearchQuery.trim().toLowerCase()
},
sortBySelectNames() {
return this.sortByValues.map((k) => {
switch (k) {
case SORT_BY_VALUES.Custom:
return this.$t('Playlist.Sort By.Custom')
case SORT_BY_VALUES.DateAddedNewest:
return this.$t('Playlist.Sort By.DateAddedNewest')
case SORT_BY_VALUES.DateAddedOldest:
return this.$t('Playlist.Sort By.DateAddedOldest')
case SORT_BY_VALUES.VideoTitleAscending:
return this.$t('Playlist.Sort By.VideoTitleAscending')
case SORT_BY_VALUES.VideoTitleDescending:
return this.$t('Playlist.Sort By.VideoTitleDescending')
case SORT_BY_VALUES.AuthorAscending:
return this.$t('Playlist.Sort By.AuthorAscending')
case SORT_BY_VALUES.AuthorDescending:
return this.$t('Playlist.Sort By.AuthorDescending')
default:
console.error(`Unknown sort: ${k}`)
return k
}
})
},
sortBySelectValues() {
return this.sortByValues
},
},
watch: {
$route () {
@ -485,6 +560,7 @@ export default defineComponent({
...mapActions([
'updateSubscriptionDetails',
'updatePlaylist',
'updateUserPlaylistSortOrder',
'removeVideo',
]),

View File

@ -66,6 +66,13 @@
max-block-size: 7vh;
}
.sortSelect {
/* Put it on the right */
margin-inline-start: auto;
/* Align with 'More Options' dropdown button */
margin-inline-end: 20px;
}
:deep(.videoThumbnail) {
margin-block-start: auto;
margin-block-end: auto;

View File

@ -43,6 +43,15 @@
<template
v-if="playlistItems.length > 0"
>
<ft-select
v-if="isUserPlaylistRequested && playlistItems.length > 1"
class="sortSelect"
:value="sortOrder"
:select-names="sortBySelectNames"
:select-values="sortBySelectValues"
:placeholder="$t('Playlist.Sort By.Sort By')"
@change="updateUserPlaylistSortOrder"
/>
<template
v-if="visiblePlaylistItems.length > 0"
>
@ -62,8 +71,8 @@
appearance="result"
:always-show-add-to-playlist-button="true"
:quick-bookmark-button-enabled="quickBookmarkButtonEnabled"
:can-move-video-up="index > 0 && !playlistInVideoSearchMode"
:can-move-video-down="index < playlistItems.length - 1 && !playlistInVideoSearchMode"
:can-move-video-up="index > 0 && !playlistInVideoSearchMode && isSortOrderCustom"
:can-move-video-down="index < playlistItems.length - 1 && !playlistInVideoSearchMode && isSortOrderCustom"
:can-remove-from-playlist="true"
:video-index="playlistInVideoSearchMode ? playlistItems.findIndex(i => i === item) : index"
:initial-visible-state="index < 10"

View File

@ -185,7 +185,6 @@ User Playlists:
LatestPlayedFirst: 'Recently Played'
EarliestPlayedFirst: 'Earliest Played'
SinglePlaylistView:
Search for Videos: Search for Videos
@ -872,6 +871,15 @@ Playlist:
View: View
Views: Views
Last Updated On: Last Updated On
Sort By:
Sort By: Sort By
DateAddedNewest: Latest added first
DateAddedOldest: Earliest added first
AuthorAscending: Author (A-Z)
AuthorDescending: Author (Z-A)
VideoTitleAscending: Title (A-Z)
VideoTitleDescending: Title (Z-A)
Custom: Custom
# On Video Watch Page
#* Published