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 = `
# Title @@ -2141,11 +2177,11 @@ export class UIRenderer { Menu
`; - 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 = ` -
- # - Title - Duration - Menu -
- `; + const originalTracks = [...tracks]; + let currentTracks = [...tracks]; + let currentSort = 'custom'; - this.renderListWithTracks(tracklistContainer, tracks, true, true); + const renderTracks = () => { + tracklistContainer.innerHTML = ` +
+ # + Title + Duration + Menu +
+ `; + 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;