From 8be0fa944be45630bf1efaf6817aa5835e09a97e Mon Sep 17 00:00:00 2001 From: BlackSigkill Date: Fri, 20 Feb 2026 10:49:56 +0100 Subject: [PATCH] add sub context menu for artists + add links to artists --- js/app.js | 9 ---- js/events.js | 121 ++++++++++++++++++++++++++++++++++++++------------- js/ui.js | 3 +- styles.css | 10 +++++ 4 files changed, 102 insertions(+), 41 deletions(-) diff --git a/js/app.js b/js/app.js index 796542d..e9d37b3 100644 --- a/js/app.js +++ b/js/app.js @@ -2302,15 +2302,6 @@ document.addEventListener('DOMContentLoaded', async () => { albumItem.textContent = `Go to ${label}`; albumItem.style.display = track.album ? 'block' : 'none'; } - if (artistItem) { - const artists = track.artists || (track.artist ? [track.artist] : []); - if (artists.length === 1) { - artistItem.style.display = 'block'; - artistItem.textContent = `Go to ${artists[0].name || 'artist'}`; - } else { - artistItem.style.display = 'none'; - } - } } } } diff --git a/js/events.js b/js/events.js index 5223b6f..6d1c4bc 100644 --- a/js/events.js +++ b/js/events.js @@ -1508,9 +1508,6 @@ async function updateContextMenuLikeState(contextMenu, contextTrack) { // Handle multiple artists for "Go to artist" const artistItem = contextMenu.querySelector('li[data-action="go-to-artist"]'); if (artistItem) { - // Remove any previously added multiple artist items - contextMenu.querySelectorAll('.dynamic-artist-item').forEach((i) => i.remove()); - const artists = Array.isArray(contextTrack.artists) ? contextTrack.artists : contextTrack.artist @@ -1519,35 +1516,14 @@ async function updateContextMenuLikeState(contextMenu, contextTrack) { const canShowArtist = type === 'track' || type === 'album'; if (artists.length > 1 && canShowArtist) { - artistItem.style.display = 'none'; - // Sort artists by name to be consistent - [...artists] - .sort((a, b) => (a.name || '').localeCompare(b.name || '')) - .forEach((artist) => { - const li = document.createElement('li'); - li.classList.add('dynamic-artist-item'); - li.dataset.action = 'go-to-artist'; - li.dataset.artistId = artist.id || ''; - - // Handle tracker/unreleased tracks - const isTracker = - contextTrack.isTracker || (contextTrack.id && String(contextTrack.id).startsWith('tracker-')); - if (isTracker && contextTrack.trackerInfo?.sheetId) { - li.dataset.trackerSheetId = contextTrack.trackerInfo.sheetId; - } - - li.textContent = `Go to ${artist.name || 'Unknown Artist'}`; - artistItem.parentNode.insertBefore(li, artistItem.nextSibling); - }); + artistItem.style.display = 'block'; + artistItem.textContent = 'Go to artists'; + artistItem.dataset.hasMultipleArtists = 'true'; } else { - // Restore default behavior for single artist const hasArtist = artists.length > 0; artistItem.style.display = hasArtist && canShowArtist ? 'block' : 'none'; - if (hasArtist) { - artistItem.textContent = `Go to ${artists[0].name || 'artist'}`; - } else { - artistItem.textContent = 'Go to artist'; - } + artistItem.dataset.hasMultipleArtists = 'false'; + artistItem.textContent = artists.length > 1 ? 'Go to artists' : 'Go to artist'; delete artistItem.dataset.artistId; delete artistItem.dataset.trackerSheetId; } @@ -1614,6 +1590,11 @@ export function initializeTrackInteractions(player, api, mainContent, contextMen item = { id, uuid: id, title: card.querySelector('.card-title')?.textContent || 'Item' }; } + if (contextMenu._originalHTML) { + contextMenu.innerHTML = contextMenu._originalHTML; + contextMenu._originalHTML = null; + } + contextTrack = item; contextMenu._contextTrack = item; contextMenu._contextType = type; @@ -1639,12 +1620,21 @@ export function initializeTrackInteractions(player, api, mainContent, contextMen clickedTrack && contextTrack.id === clickedTrack.id ) { + if (contextMenu._originalHTML) { + contextMenu.innerHTML = contextMenu._originalHTML; + } contextMenu.style.display = 'none'; + contextMenu._contextType = null; + contextMenu._originalHTML = null; return; } contextTrack = clickedTrack; if (contextTrack) { + if (contextMenu._originalHTML) { + contextMenu.innerHTML = contextMenu._originalHTML; + contextMenu._originalHTML = null; + } contextMenu._contextTrack = contextTrack; contextMenu._contextType = 'track'; await updateContextMenuLikeState(contextMenu, contextTrack); @@ -1659,7 +1649,7 @@ export function initializeTrackInteractions(player, api, mainContent, contextMen if (trackItem && (trackItem.classList.contains('unavailable') || trackItem.classList.contains('blocked'))) { return; } - if (trackItem && !trackItem.dataset.queueIndex && !e.target.closest('.remove-from-playlist-btn')) { + if (trackItem && !trackItem.dataset.queueIndex && !e.target.closest('.remove-from-playlist-btn') && !e.target.closest('.artist-link')) { const parentList = trackItem.closest('.track-list'); const allTrackElements = Array.from(parentList.querySelectorAll('.track-item')); const trackList = allTrackElements.map((el) => trackDataStore.get(el)).filter(Boolean); @@ -1674,6 +1664,20 @@ export function initializeTrackInteractions(player, api, mainContent, contextMen } } + // Handle artist link clicks in track lists + const artistLink = e.target.closest('.artist-link'); + if (artistLink) { + e.stopPropagation(); + const artistId = artistLink.dataset.artistId; + const trackerSheetId = artistLink.dataset.trackerSheetId; + if (trackerSheetId) { + navigate(`/unreleased/${trackerSheetId}`); + } else if (artistId) { + navigate(`/artist/${artistId}`); + } + return; + } + const card = e.target.closest('.card'); if (card) { // Don't navigate if card is blocked (unless clicking menu button) @@ -1714,6 +1718,11 @@ export function initializeTrackInteractions(player, api, mainContent, contextMen if (contextTrack) { if (contextTrack.isLocal) return; + if (contextMenu._originalHTML) { + contextMenu.innerHTML = contextMenu._originalHTML; + contextMenu._originalHTML = null; + } + // Hide actions for unavailable tracks const unavailableActions = ['play-next', 'add-to-queue', 'download', 'track-mix']; contextMenu.querySelectorAll('[data-action]').forEach((btn) => { @@ -1745,6 +1754,12 @@ export function initializeTrackInteractions(player, api, mainContent, contextMen uuid: id, title: card.querySelector('.card-title')?.textContent, }; + + if (contextMenu._originalHTML) { + contextMenu.innerHTML = contextMenu._originalHTML; + contextMenu._originalHTML = null; + } + contextTrack = item; contextMenu._contextTrack = item; contextMenu._contextType = type.replace('userplaylist', 'user-playlist'); @@ -1756,7 +1771,14 @@ export function initializeTrackInteractions(player, api, mainContent, contextMen }); document.addEventListener('click', () => { - contextMenu.style.display = 'none'; + if (contextMenu.style.display === 'block') { + if (contextMenu._originalHTML) { + contextMenu.innerHTML = contextMenu._originalHTML; + } + contextMenu.style.display = 'none'; + contextMenu._contextType = null; + contextMenu._originalHTML = null; + } }); contextMenu.addEventListener('click', async (e) => { @@ -1767,11 +1789,48 @@ export function initializeTrackInteractions(player, api, mainContent, contextMen const action = target.dataset.action; const track = contextMenu._contextTrack || contextTrack; const type = contextMenu._contextType || 'track'; + + if (action === 'go-to-artists' || (action === 'go-to-artist' && target.dataset.hasMultipleArtists === 'true')) { + const artists = Array.isArray(track.artists) ? track.artists : track.artist ? [track.artist] : []; + if (artists.length > 1) { + // Save original HTML if not already saved + if (!contextMenu._originalHTML) { + contextMenu._originalHTML = contextMenu.innerHTML; + } + + // Render sub-menu + let subMenuHTML = '
  • ← Back
  • '; + [...artists] + .sort((a, b) => (a.name || '').localeCompare(b.name || '')) + .forEach((artist) => { + subMenuHTML += `
  • ${escapeHtml(artist.name || 'Unknown Artist')}
  • `; + }); + contextMenu.innerHTML = ``; + return; + } + } + + if (action === 'back-to-main-menu') { + if (contextMenu._originalHTML) { + contextMenu.innerHTML = contextMenu._originalHTML; + contextMenu._originalHTML = null; + // Re-update like state since we replaced the HTML + await updateContextMenuLikeState(contextMenu, track); + } + return; + } + if (action && track) { // Track context menu action trackContextMenuAction(action, type, track); await handleTrackAction(action, track, player, api, lyricsManager, type, ui, scrobbler, target.dataset); } + + // Reset menu state before closing + if (contextMenu._originalHTML) { + contextMenu.innerHTML = contextMenu._originalHTML; + contextMenu._originalHTML = null; + } contextMenu.style.display = 'none'; contextMenu._contextType = null; }); diff --git a/js/ui.js b/js/ui.js index b2dac27..1931cc7 100644 --- a/js/ui.js +++ b/js/ui.js @@ -12,6 +12,7 @@ import { trackDataStore, hasExplicitContent, getTrackArtists, + getTrackArtistsHTML, getTrackTitle, getTrackYearDisplay, createQualityBadgeHTML, @@ -371,7 +372,7 @@ export class UIRenderer { ${explicitBadge} ${qualityBadge} -
    ${escapeHtml(trackArtists)}${yearDisplay}
    +
    ${getTrackArtistsHTML(track)}${yearDisplay}
    ${isUnavailable || isBlocked ? '--:--' : track.duration ? formatTime(track.duration) : '--:--'}
    diff --git a/styles.css b/styles.css index 85f7624..02441b0 100644 --- a/styles.css +++ b/styles.css @@ -4866,6 +4866,16 @@ input:checked + .slider::before { text-decoration: underline; } +.track-item .artist .artist-link { + cursor: pointer; + transition: color var(--transition); +} + +.track-item .artist .artist-link:hover { + color: var(--highlight); + text-decoration: underline; +} + /* Updated Volume Controls Layout */ .player-actions-row {