mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 07:15:47 +00:00
feat(playlist): sort playlist tracks by date added (#1191)
* feat(sort-dropdown): support a right-aligned popover via align prop * feat(playlist): sort playlist tracks by date added * docs(changelog): playlist sort by date added (#1191) * docs(credits): playlist sort by date added (#1191)
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import type { SubsonicSong } from '../../api/subsonicTypes';
|
||||
import {
|
||||
getDisplayedSongs,
|
||||
type DisplayedSongsOptions,
|
||||
type PlaylistSortKey,
|
||||
type PlaylistSortDir,
|
||||
} from './playlistDisplayedSongs';
|
||||
|
||||
const song = (id: string, title = id, artist = ''): SubsonicSong =>
|
||||
({ id, title, artist }) as SubsonicSong;
|
||||
|
||||
const opts = (over: Partial<DisplayedSongsOptions> = {}): DisplayedSongsOptions => ({
|
||||
filterText: '',
|
||||
sortKey: 'natural',
|
||||
sortDir: 'asc',
|
||||
ratings: {},
|
||||
userRatingOverrides: {},
|
||||
starredOverrides: {},
|
||||
starredSongs: new Set<string>(),
|
||||
...over,
|
||||
});
|
||||
|
||||
const ids = (songs: SubsonicSong[]) => songs.map(s => s.id);
|
||||
|
||||
describe('getDisplayedSongs — position (date added)', () => {
|
||||
// Playlist load order is oldest→newest (servers append new tracks at the end).
|
||||
const list = [song('a'), song('b'), song('c'), song('d')];
|
||||
|
||||
it('ascending keeps the playlist load order (oldest → newest)', () => {
|
||||
expect(ids(getDisplayedSongs(list, opts({ sortKey: 'position', sortDir: 'asc' })))).toEqual([
|
||||
'a',
|
||||
'b',
|
||||
'c',
|
||||
'd',
|
||||
]);
|
||||
});
|
||||
|
||||
it('descending reverses it (newest added first) — the requested behaviour', () => {
|
||||
expect(ids(getDisplayedSongs(list, opts({ sortKey: 'position', sortDir: 'desc' })))).toEqual([
|
||||
'd',
|
||||
'c',
|
||||
'b',
|
||||
'a',
|
||||
]);
|
||||
});
|
||||
|
||||
it('never mutates the input array', () => {
|
||||
const input = [song('a'), song('b'), song('c')];
|
||||
getDisplayedSongs(input, opts({ sortKey: 'position', sortDir: 'desc' }));
|
||||
expect(ids(input)).toEqual(['a', 'b', 'c']);
|
||||
});
|
||||
|
||||
it('filters first, then reverses the surviving rows', () => {
|
||||
const mixed = [song('1', 'alpha'), song('2', 'beta'), song('3', 'alphabet')];
|
||||
const out = getDisplayedSongs(
|
||||
mixed,
|
||||
opts({ sortKey: 'position', sortDir: 'desc', filterText: 'alpha' }),
|
||||
);
|
||||
expect(ids(out)).toEqual(['3', '1']);
|
||||
});
|
||||
|
||||
it("natural still ignores sortDir (it is the reset state, not a position sort)", () => {
|
||||
const k: PlaylistSortKey = 'natural';
|
||||
const d: PlaylistSortDir = 'desc';
|
||||
expect(ids(getDisplayedSongs(list, opts({ sortKey: k, sortDir: d })))).toEqual([
|
||||
'a',
|
||||
'b',
|
||||
'c',
|
||||
'd',
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { SubsonicSong } from '../../api/subsonicTypes';
|
||||
|
||||
export type PlaylistSortKey = 'natural' | 'title' | 'artist' | 'album' | 'favorite' | 'rating' | 'duration' | 'playCount' | 'lastPlayed' | 'bpm';
|
||||
export type PlaylistSortKey = 'natural' | 'position' | 'title' | 'artist' | 'album' | 'favorite' | 'rating' | 'duration' | 'playCount' | 'lastPlayed' | 'bpm';
|
||||
export type PlaylistSortDir = 'asc' | 'desc';
|
||||
|
||||
export interface DisplayedSongsOptions {
|
||||
@@ -18,6 +18,13 @@ export function getDisplayedSongs(songs: SubsonicSong[], opts: DisplayedSongsOpt
|
||||
if (!q && opts.sortKey === 'natural') return songs;
|
||||
let result = [...songs];
|
||||
if (q) result = result.filter(s => s.title.toLowerCase().includes(q) || (s.artist ?? '').toLowerCase().includes(q));
|
||||
if (opts.sortKey === 'position') {
|
||||
// Playlist position is the "date added" proxy: servers append new tracks at
|
||||
// the end, so ascending = oldest→newest (load order) and descending =
|
||||
// newest→oldest. Reverse rather than compare — stable and O(n), and the
|
||||
// Subsonic playlist response carries no per-entry timestamp to compare on.
|
||||
return opts.sortDir === 'desc' ? result.reverse() : result;
|
||||
}
|
||||
if (opts.sortKey !== 'natural') {
|
||||
result.sort((a, b) => {
|
||||
let av: string | number;
|
||||
|
||||
Reference in New Issue
Block a user