diff --git a/index.html b/index.html
index 45112b8..b03f375 100644
--- a/index.html
+++ b/index.html
@@ -53,10 +53,12 @@
diff --git a/js/ui.js b/js/ui.js
index 124e206..9dd2016 100644
--- a/js/ui.js
+++ b/js/ui.js
@@ -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;
@@ -2130,10 +2161,15 @@ export class UIRenderer {
descEl.textContent = playlistData.description || '';
const originalTracks = [...tracks];
- let currentTracks = [...tracks];
+ // Default sort: first available option (Playlist Order if no addedAt, else Date Added Newest)
+ const hasAddedDate = tracks.some((t) => t.addedAt);
+ currentSort = hasAddedDate ? 'added-newest' : 'custom';
+ let currentTracks = sortTracks(originalTracks, currentSort);
const renderTracks = () => {
- tracklistContainer.innerHTML = `
+ // Re-fetch container each time because enableTrackReordering clones it
+ const container = document.getElementById('playlist-detail-tracklist');
+ container.innerHTML = `
`;
- 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,35 +2196,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);
- });
- }
+ currentTracks = sortTracks(originalTracks, sortType);
renderTracks();
};
@@ -2205,13 +2225,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 = () => {
@@ -2276,16 +2297,34 @@ export class UIRenderer {
metaEl.textContent = `${playlist.numberOfTracks} tracks • ${formatDuration(totalDuration)}`;
descEl.textContent = playlist.description || '';
- tracklistContainer.innerHTML = `
-
- `;
+ const originalTracks = [...tracks];
+ let currentTracks = [...tracks];
+ let currentSort = 'custom';
- this.renderListWithTracks(tracklistContainer, tracks, true, true);
+ const renderTracks = () => {
+ tracklistContainer.innerHTML = `
+
+ `;
+ this.renderListWithTracks(tracklistContainer, currentTracks, true, true);
+ };
+
+ const applySort = (sortType) => {
+ currentSort = sortType;
+ currentTracks = sortTracks(originalTracks, sortType);
+ renderTracks();
+ };
+
+ renderTracks();
+
+ playBtn.onclick = () => {
+ this.player.setQueue(currentTracks, 0);
+ this.player.playTrackFromQueue();
+ };
// Update header like button
const playlistLikeBtn = document.getElementById('like-playlist-btn');
@@ -2308,8 +2347,8 @@ export class UIRenderer {
recommendedSection.style.display = 'none';
}
- // Render Actions (Shuffle + Share)
- this.updatePlaylistHeaderActions(playlist, false, tracks, false);
+ // Render Actions (Shuffle + Sort + Share)
+ this.updatePlaylistHeaderActions(playlist, false, currentTracks, false, applySort, () => currentSort);
recentActivityManager.addPlaylist(playlist);
document.title = playlist.title || 'Artist Mix';
@@ -2817,7 +2856,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
@@ -2845,10 +2884,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 =
@@ -2858,6 +2898,21 @@ export class UIRenderer {
e.stopPropagation();
const menu = document.getElementById('sort-menu');
+ // Show "Date Added" if tracks have addedAt, otherwise show "Playlist Order"
+ const hasAddedDate = tracks.some((t) => t.addedAt);
+ 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`;
menu.style.left = `${rect.left}px`;
@@ -2880,7 +2935,6 @@ export class UIRenderer {
setTimeout(() => document.addEventListener('click', closeMenu), 0);
};
- fragment.appendChild(sortBtn);
}
// Edit/Delete (Owned Only)
@@ -2915,8 +2969,10 @@ 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.
@@ -2942,12 +2998,24 @@ export class UIRenderer {
// 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);
- // Append the rest
+ // Insert Sort after Download, before Like
+ if (sortBtn && likeBtn) {
+ actionsDiv.insertBefore(sortBtn, likeBtn);
+ } else if (sortBtn) {
+ 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);
+ }
}
}
diff --git a/styles.css b/styles.css
index 61f1968..c0c8bcd 100644
--- a/styles.css
+++ b/styles.css
@@ -2179,7 +2179,8 @@ input:checked + .slider::before {
pointer-events: none;
}
-#context-menu {
+#context-menu,
+#sort-menu {
display: none;
position: fixed;
background-color: var(--card);
@@ -2189,15 +2190,19 @@ input:checked + .slider::before {
box-shadow: var(--shadow-lg);
z-index: 3000;
min-width: 160px;
+ transform-origin: top left;
+ animation: scale-in var(--transition-fast) var(--ease-out-back);
}
-#context-menu ul {
+#context-menu ul,
+#sort-menu ul {
list-style: none;
}
#context-menu li,
#sort-menu li {
padding: 0.5rem 0.75rem;
+ margin-right: 8px;
cursor: pointer;
border-radius: 4px;
transition:
@@ -2212,6 +2217,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);
@@ -2221,12 +2230,6 @@ input:checked + .slider::before {
color: var(--foreground);
}
-#context-menu,
-#sort-menu {
- transform-origin: top left;
- animation: scale-in var(--transition-fast) var(--ease-out-back);
-}
-
#queue-modal-overlay {
display: none;
position: fixed;