fix sorting on monochrome's playlists

This commit is contained in:
BlackSigkill 2026-02-06 11:25:02 +01:00
parent cf499baeb4
commit 26acd4b225
3 changed files with 93 additions and 101 deletions

View file

@ -53,7 +53,7 @@
<div id="sort-menu" style="display: none"> <div id="sort-menu" style="display: none">
<ul> <ul>
<li data-sort="custom">Playlist Order</li> <li data-sort="custom" class="requires-custom-order">Playlist Order</li>
<li data-sort="added-newest" class="requires-added-date">Date Added (Newest)</li> <li data-sort="added-newest" class="requires-added-date">Date Added (Newest)</li>
<li data-sort="added-oldest" class="requires-added-date">Date Added (Oldest)</li> <li data-sort="added-oldest" class="requires-added-date">Date Added (Oldest)</li>
<li data-sort="title">Title (A-Z)</li> <li data-sort="title">Title (A-Z)</li>

188
js/ui.js
View file

@ -43,6 +43,37 @@ import {
createTrackFromSong, createTrackFromSong,
} from './tracker.js'; } from './tracker.js';
function sortTracks(tracks, sortType) {
if (sortType === 'custom') return [...tracks];
const sorted = [...tracks];
switch (sortType) {
case 'added-newest':
return sorted.sort((a, b) => (b.addedAt || 0) - (a.addedAt || 0));
case 'added-oldest':
return sorted.sort((a, b) => (a.addedAt || 0) - (b.addedAt || 0));
case 'title':
return sorted.sort((a, b) => (a.title || '').localeCompare(b.title || ''));
case 'artist':
return sorted.sort((a, b) => {
const artistA = a.artist?.name || a.artists?.[0]?.name || '';
const artistB = b.artist?.name || b.artists?.[0]?.name || '';
return artistA.localeCompare(artistB);
});
case 'album':
return sorted.sort((a, b) => {
const albumA = a.album?.title || '';
const albumB = b.album?.title || '';
const albumCompare = albumA.localeCompare(albumB);
if (albumCompare !== 0) return albumCompare;
const trackNumA = a.trackNumber || a.position || 0;
const trackNumB = b.trackNumber || b.position || 0;
return trackNumA - trackNumB;
});
default:
return sorted;
}
}
export class UIRenderer { export class UIRenderer {
constructor(api, player) { constructor(api, player) {
this.api = api; this.api = api;
@ -2131,9 +2162,12 @@ export class UIRenderer {
const originalTracks = [...tracks]; const originalTracks = [...tracks];
let currentTracks = [...tracks]; let currentTracks = [...tracks];
currentSort = 'custom';
const renderTracks = () => { const renderTracks = () => {
tracklistContainer.innerHTML = ` // Re-fetch container each time because enableTrackReordering clones it
const container = document.getElementById('playlist-detail-tracklist');
container.innerHTML = `
<div class="track-list-header"> <div class="track-list-header">
<span style="width: 40px; text-align: center;">#</span> <span style="width: 40px; text-align: center;">#</span>
<span>Title</span> <span>Title</span>
@ -2141,11 +2175,11 @@ export class UIRenderer {
<span style="display: flex; justify-content: flex-end; opacity: 0.8;">Menu</span> <span style="display: flex; justify-content: flex-end; opacity: 0.8;">Menu</span>
</div> </div>
`; `;
this.renderListWithTracks(tracklistContainer, currentTracks, true, true); this.renderListWithTracks(container, currentTracks, true, true);
// Add remove buttons and enable reordering ONLY IF OWNED // Add remove buttons and enable reordering ONLY IF OWNED
if (ownedPlaylist) { if (ownedPlaylist) {
const trackItems = tracklistContainer.querySelectorAll('.track-item'); const trackItems = container.querySelectorAll('.track-item');
trackItems.forEach((item, index) => { trackItems.forEach((item, index) => {
const actionsDiv = item.querySelector('.track-item-actions'); const actionsDiv = item.querySelector('.track-item-actions');
const removeBtn = document.createElement('button'); const removeBtn = document.createElement('button');
@ -2160,46 +2194,19 @@ export class UIRenderer {
}); });
if (currentSort === 'custom') { if (currentSort === 'custom') {
tracklistContainer.classList.add('is-editable'); container.classList.add('is-editable');
this.enableTrackReordering(tracklistContainer, currentTracks, playlistId, syncManager); this.enableTrackReordering(container, currentTracks, playlistId, syncManager);
} else { } else {
tracklistContainer.classList.remove('is-editable'); container.classList.remove('is-editable');
} }
} else { } else {
tracklistContainer.classList.remove('is-editable'); container.classList.remove('is-editable');
} }
}; };
const applySort = (sortType) => { const applySort = (sortType) => {
currentSort = sortType; currentSort = sortType;
if (sortType === 'custom') { currentTracks = sortTracks(originalTracks, sortType);
currentTracks = [...originalTracks];
} else if (sortType === 'added-newest') {
currentTracks = [...originalTracks].sort((a, b) => (b.addedAt || 0) - (a.addedAt || 0));
} else if (sortType === 'added-oldest') {
currentTracks = [...originalTracks].sort((a, b) => (a.addedAt || 0) - (b.addedAt || 0));
} else if (sortType === 'title') {
currentTracks = [...originalTracks].sort((a, b) =>
(a.title || '').localeCompare(b.title || '')
);
} else if (sortType === 'artist') {
currentTracks = [...originalTracks].sort((a, b) => {
const artistA = a.artist?.name || a.artists?.[0]?.name || '';
const artistB = b.artist?.name || b.artists?.[0]?.name || '';
return artistA.localeCompare(artistB);
});
} else if (sortType === 'album') {
currentTracks = [...originalTracks].sort((a, b) => {
const albumA = a.album?.title || '';
const albumB = b.album?.title || '';
const albumCompare = albumA.localeCompare(albumB);
if (albumCompare !== 0) return albumCompare;
// If same album, sort by track number
const trackNumA = a.trackNumber || a.position || 0;
const trackNumB = b.trackNumber || b.position || 0;
return trackNumA - trackNumB;
});
}
renderTracks(); renderTracks();
}; };
@ -2216,13 +2223,14 @@ export class UIRenderer {
this.loadRecommendedSongsForPlaylist(tracks); this.loadRecommendedSongsForPlaylist(tracks);
} }
// Render Actions (Shuffle, Edit, Delete, Share, Sort) // Render Actions (Sort, Shuffle, Edit, Delete, Share)
this.updatePlaylistHeaderActions( this.updatePlaylistHeaderActions(
playlistData, playlistData,
!!ownedPlaylist, !!ownedPlaylist,
tracks, currentTracks,
false, false,
ownedPlaylist ? applySort : null applySort,
() => currentSort
); );
playBtn.onclick = () => { playBtn.onclick = () => {
@ -2305,34 +2313,7 @@ export class UIRenderer {
const applySort = (sortType) => { const applySort = (sortType) => {
currentSort = sortType; currentSort = sortType;
if (sortType === 'custom') { currentTracks = sortTracks(originalTracks, sortType);
currentTracks = [...originalTracks];
} else if (sortType === 'added-newest') {
currentTracks = [...originalTracks].sort((a, b) => (b.addedAt || 0) - (a.addedAt || 0));
} else if (sortType === 'added-oldest') {
currentTracks = [...originalTracks].sort((a, b) => (a.addedAt || 0) - (b.addedAt || 0));
} else if (sortType === 'title') {
currentTracks = [...originalTracks].sort((a, b) =>
(a.title || '').localeCompare(b.title || '')
);
} else if (sortType === 'artist') {
currentTracks = [...originalTracks].sort((a, b) => {
const artistA = a.artist?.name || a.artists?.[0]?.name || '';
const artistB = b.artist?.name || b.artists?.[0]?.name || '';
return artistA.localeCompare(artistB);
});
} else if (sortType === 'album') {
currentTracks = [...originalTracks].sort((a, b) => {
const albumA = a.album?.title || '';
const albumB = b.album?.title || '';
const albumCompare = albumA.localeCompare(albumB);
if (albumCompare !== 0) return albumCompare;
// If same album, sort by track number
const trackNumA = a.trackNumber || a.position || 0;
const trackNumB = b.trackNumber || b.position || 0;
return trackNumA - trackNumB;
});
}
renderTracks(); renderTracks();
}; };
@ -2365,7 +2346,7 @@ export class UIRenderer {
} }
// Render Actions (Shuffle + Sort + Share) // Render Actions (Shuffle + Sort + Share)
this.updatePlaylistHeaderActions(playlist, false, currentTracks, false, applySort); this.updatePlaylistHeaderActions(playlist, false, currentTracks, false, applySort, () => currentSort);
recentActivityManager.addPlaylist(playlist); recentActivityManager.addPlaylist(playlist);
document.title = playlist.title || 'Artist Mix'; document.title = playlist.title || 'Artist Mix';
@ -2873,7 +2854,7 @@ export class UIRenderer {
await renderTrackerTrackContent(trackId, container, this); await renderTrackerTrackContent(trackId, container, this);
} }
updatePlaylistHeaderActions(playlist, isOwned, tracks, showShare = false, onSort = null) { updatePlaylistHeaderActions(playlist, isOwned, tracks, showShare = false, onSort = null, getCurrentSort = null) {
const actionsDiv = document.getElementById('page-playlist').querySelector('.detail-header-actions'); const actionsDiv = document.getElementById('page-playlist').querySelector('.detail-header-actions');
// Cleanup existing dynamic buttons // Cleanup existing dynamic buttons
@ -2901,11 +2882,11 @@ export class UIRenderer {
this.player.setQueue(shuffledTracks, 0); this.player.setQueue(shuffledTracks, 0);
this.player.playTrackFromQueue(); this.player.playTrackFromQueue();
}; };
fragment.appendChild(shuffleBtn);
// Sort button (always available if onSort is provided) // Sort button (always available if onSort is provided)
let sortBtn = null;
if (onSort) { if (onSort) {
const sortBtn = document.createElement('button'); sortBtn = document.createElement('button');
sortBtn.id = 'sort-playlist-btn'; sortBtn.id = 'sort-playlist-btn';
sortBtn.className = 'btn-secondary'; sortBtn.className = 'btn-secondary';
sortBtn.innerHTML = sortBtn.innerHTML =
@ -2915,12 +2896,20 @@ export class UIRenderer {
e.stopPropagation(); e.stopPropagation();
const menu = document.getElementById('sort-menu'); const menu = document.getElementById('sort-menu');
// Check if any track has addedAt data // Show "Date Added" if tracks have addedAt, otherwise show "Playlist Order"
const hasAddedDate = tracks.some((t) => t.addedAt); const hasAddedDate = tracks.some((t) => t.addedAt);
const dateOptions = menu.querySelectorAll('.requires-added-date'); menu.querySelectorAll('.requires-added-date').forEach((opt) => {
dateOptions.forEach((opt) => {
opt.style.display = hasAddedDate ? '' : 'none'; opt.style.display = hasAddedDate ? '' : 'none';
}); });
menu.querySelectorAll('.requires-custom-order').forEach((opt) => {
opt.style.display = hasAddedDate ? 'none' : '';
});
// Highlight current sort option
const currentSortType = getCurrentSort ? getCurrentSort() : 'custom';
menu.querySelectorAll('li').forEach((opt) => {
opt.classList.toggle('sort-active', opt.dataset.sort === currentSortType);
});
const rect = sortBtn.getBoundingClientRect(); const rect = sortBtn.getBoundingClientRect();
menu.style.top = `${rect.bottom + 5}px`; menu.style.top = `${rect.bottom + 5}px`;
@ -2944,7 +2933,6 @@ export class UIRenderer {
setTimeout(() => document.addEventListener('click', closeMenu), 0); setTimeout(() => document.addEventListener('click', closeMenu), 0);
}; };
fragment.appendChild(sortBtn);
} }
// Edit/Delete (Owned Only) // Edit/Delete (Owned Only)
@ -2979,39 +2967,39 @@ export class UIRenderer {
fragment.appendChild(shareBtn); fragment.appendChild(shareBtn);
} }
// Insert before Download button if possible, else append // Insert buttons in the correct order: Play, Shuffle, Download, Sort, Like, Edit/Delete/Share
const dlBtn = actionsDiv.querySelector('#download-playlist-btn'); const dlBtn = actionsDiv.querySelector('#download-playlist-btn');
const likeBtn = actionsDiv.querySelector('#like-playlist-btn');
if (dlBtn) { if (dlBtn) {
// We want Shuffle first, then Edit/Delete/Share. // Insert Shuffle before Download
// But Download is usually first or second.
// In renderPlaylistPage: Play, Download, Like.
// We want Shuffle after Play? Or after Download?
// Previous code: actionsDiv.insertBefore(shuffleBtn, dlBtn); => Shuffle before Download.
// Then appended others.
// Let's just append everything for now to keep it simple, or insert Shuffle specifically.
// The Play button is static. Download is static.
// If we want Shuffle before Download:
// fragment has Shuffle, Edit, Delete, Share.
// If we insert fragment before Download, all go before Download.
// That might change the order.
// Previous order: Shuffle (before Download), then Edit/Delete/Share (appended = after Like).
// Let's split fragment?
// Or just use append for all.
// The user didn't complain about order, but consistency is good.
// "Fix popup buttons" was the request.
// Let's stick to appending for now to minimize visual layout shifts from previous (where Edit/Delete were appended).
// Shuffle was inserted before Download.
actionsDiv.insertBefore(shuffleBtn, dlBtn); actionsDiv.insertBefore(shuffleBtn, dlBtn);
// Append the rest
// Insert Sort after Download, before Like
if (sortBtn) {
if (likeBtn) {
actionsDiv.insertBefore(sortBtn, likeBtn);
} else {
// If no Like button, insert Sort after Download
if (dlBtn.nextSibling) {
actionsDiv.insertBefore(sortBtn, dlBtn.nextSibling);
} else {
actionsDiv.appendChild(sortBtn);
}
}
}
// Append Edit/Delete/Share buttons after Like
while (fragment.firstChild) { while (fragment.firstChild) {
actionsDiv.appendChild(fragment.firstChild); actionsDiv.appendChild(fragment.firstChild);
} }
} else { } else {
actionsDiv.appendChild(fragment); // If no Download button, just append everything
actionsDiv.appendChild(shuffleBtn);
if (sortBtn) actionsDiv.appendChild(sortBtn);
while (fragment.firstChild) {
actionsDiv.appendChild(fragment.firstChild);
}
} }
} }

View file

@ -2214,6 +2214,10 @@ input:checked + .slider::before {
align-items: center; align-items: center;
} }
#sort-menu li.sort-active {
font-weight: bold;
}
#context-menu li:hover, #context-menu li:hover,
#sort-menu li:hover { #sort-menu li:hover {
background-color: var(--secondary); background-color: var(--secondary);