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">
<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-oldest" class="requires-added-date">Date Added (Oldest)</li>
<li data-sort="title">Title (A-Z)</li>

188
js/ui.js
View file

@ -43,6 +43,37 @@ import {
createTrackFromSong,
} 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 {
constructor(api, player) {
this.api = api;
@ -2131,9 +2162,12 @@ export class UIRenderer {
const originalTracks = [...tracks];
let currentTracks = [...tracks];
currentSort = 'custom';
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">
<span style="width: 40px; text-align: center;">#</span>
<span>Title</span>
@ -2141,11 +2175,11 @@ export class UIRenderer {
<span style="display: flex; justify-content: flex-end; opacity: 0.8;">Menu</span>
</div>
`;
this.renderListWithTracks(tracklistContainer, currentTracks, true, true);
this.renderListWithTracks(container, currentTracks, true, true);
// Add remove buttons and enable reordering ONLY IF OWNED
if (ownedPlaylist) {
const trackItems = tracklistContainer.querySelectorAll('.track-item');
const trackItems = container.querySelectorAll('.track-item');
trackItems.forEach((item, index) => {
const actionsDiv = item.querySelector('.track-item-actions');
const removeBtn = document.createElement('button');
@ -2160,46 +2194,19 @@ export class UIRenderer {
});
if (currentSort === 'custom') {
tracklistContainer.classList.add('is-editable');
this.enableTrackReordering(tracklistContainer, currentTracks, playlistId, syncManager);
container.classList.add('is-editable');
this.enableTrackReordering(container, currentTracks, playlistId, syncManager);
} else {
tracklistContainer.classList.remove('is-editable');
container.classList.remove('is-editable');
}
} else {
tracklistContainer.classList.remove('is-editable');
container.classList.remove('is-editable');
}
};
const applySort = (sortType) => {
currentSort = sortType;
if (sortType === 'custom') {
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;
});
}
currentTracks = sortTracks(originalTracks, sortType);
renderTracks();
};
@ -2216,13 +2223,14 @@ export class UIRenderer {
this.loadRecommendedSongsForPlaylist(tracks);
}
// Render Actions (Shuffle, Edit, Delete, Share, Sort)
// Render Actions (Sort, Shuffle, Edit, Delete, Share)
this.updatePlaylistHeaderActions(
playlistData,
!!ownedPlaylist,
tracks,
currentTracks,
false,
ownedPlaylist ? applySort : null
applySort,
() => currentSort
);
playBtn.onclick = () => {
@ -2305,34 +2313,7 @@ export class UIRenderer {
const applySort = (sortType) => {
currentSort = sortType;
if (sortType === 'custom') {
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;
});
}
currentTracks = sortTracks(originalTracks, sortType);
renderTracks();
};
@ -2365,7 +2346,7 @@ export class UIRenderer {
}
// Render Actions (Shuffle + Sort + Share)
this.updatePlaylistHeaderActions(playlist, false, currentTracks, false, applySort);
this.updatePlaylistHeaderActions(playlist, false, currentTracks, false, applySort, () => currentSort);
recentActivityManager.addPlaylist(playlist);
document.title = playlist.title || 'Artist Mix';
@ -2873,7 +2854,7 @@ export class UIRenderer {
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');
// Cleanup existing dynamic buttons
@ -2901,11 +2882,11 @@ export class UIRenderer {
this.player.setQueue(shuffledTracks, 0);
this.player.playTrackFromQueue();
};
fragment.appendChild(shuffleBtn);
// Sort button (always available if onSort is provided)
let sortBtn = null;
if (onSort) {
const sortBtn = document.createElement('button');
sortBtn = document.createElement('button');
sortBtn.id = 'sort-playlist-btn';
sortBtn.className = 'btn-secondary';
sortBtn.innerHTML =
@ -2915,12 +2896,20 @@ export class UIRenderer {
e.stopPropagation();
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 dateOptions = menu.querySelectorAll('.requires-added-date');
dateOptions.forEach((opt) => {
menu.querySelectorAll('.requires-added-date').forEach((opt) => {
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();
menu.style.top = `${rect.bottom + 5}px`;
@ -2944,7 +2933,6 @@ export class UIRenderer {
setTimeout(() => document.addEventListener('click', closeMenu), 0);
};
fragment.appendChild(sortBtn);
}
// Edit/Delete (Owned Only)
@ -2979,39 +2967,39 @@ export class UIRenderer {
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 likeBtn = actionsDiv.querySelector('#like-playlist-btn');
if (dlBtn) {
// We want Shuffle first, then Edit/Delete/Share.
// 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.
// Insert Shuffle before Download
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) {
actionsDiv.appendChild(fragment.firstChild);
}
} 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;
}
#sort-menu li.sort-active {
font-weight: bold;
}
#context-menu li:hover,
#sort-menu li:hover {
background-color: var(--secondary);