IMP: card three dot menu

This commit is contained in:
Julien Maille 2026-01-25 21:43:09 +01:00
parent d5489edf01
commit a6736d571f
11 changed files with 2192 additions and 2792 deletions

View file

@ -51,7 +51,7 @@ class ServerAPI {
try { try {
const response = await this.fetchWithRetry(`/album/${id}`); const response = await this.fetchWithRetry(`/album/${id}`);
return await response.json(); return await response.json();
} catch (e) { } catch {
const response = await this.fetchWithRetry(`/album?id=${id}`); const response = await this.fetchWithRetry(`/album?id=${id}`);
return await response.json(); return await response.json();
} }

View file

@ -51,7 +51,7 @@ class ServerAPI {
try { try {
const response = await this.fetchWithRetry(`/artist/${id}`); const response = await this.fetchWithRetry(`/artist/${id}`);
return await response.json(); return await response.json();
} catch (e) { } catch {
const response = await this.fetchWithRetry(`/artist?id=${id}`); const response = await this.fetchWithRetry(`/artist?id=${id}`);
return await response.json(); return await response.json();
} }

View file

@ -51,7 +51,7 @@ class ServerAPI {
try { try {
const response = await this.fetchWithRetry(`/playlist/${id}`); const response = await this.fetchWithRetry(`/playlist/${id}`);
return await response.json(); return await response.json();
} catch (e) { } catch {
// Fallback to query param style // Fallback to query param style
const response = await this.fetchWithRetry(`/playlist?id=${id}`); const response = await this.fetchWithRetry(`/playlist?id=${id}`);
return await response.json(); return await response.json();

4577
index.html

File diff suppressed because one or more lines are too long

View file

@ -334,7 +334,7 @@ document.addEventListener('DOMContentLoaded', async () => {
initializeUIInteractions(player, api, ui); initializeUIInteractions(player, api, ui);
initializeKeyboardShortcuts(player, audioPlayer); initializeKeyboardShortcuts(player, audioPlayer);
initTracker(player, ui); initTracker(player);
const castBtn = document.getElementById('cast-btn'); const castBtn = document.getElementById('cast-btn');
initializeCasting(audioPlayer, castBtn); initializeCasting(audioPlayer, castBtn);

View file

@ -10,7 +10,12 @@ import {
SVG_BIN, SVG_BIN,
} from './utils.js'; } from './utils.js';
import { lastFMStorage, waveformSettings } from './storage.js'; import { lastFMStorage, waveformSettings } from './storage.js';
import { showNotification, downloadTrackWithMetadata } from './downloads.js'; import {
showNotification,
downloadTrackWithMetadata,
downloadAlbumAsZip,
downloadPlaylistAsZip,
} from './downloads.js';
import { downloadQualitySettings } from './storage.js'; import { downloadQualitySettings } from './storage.js';
import { updateTabTitle, navigate } from './router.js'; import { updateTabTitle, navigate } from './router.js';
import { db } from './db.js'; import { db } from './db.js';
@ -116,6 +121,12 @@ export function initializePlayerEvents(player, audioPlayer, scrobbler, ui) {
audioPlayer.addEventListener('error', (e) => { audioPlayer.addEventListener('error', (e) => {
console.error('Audio playback error:', e); console.error('Audio playback error:', e);
playPauseBtn.innerHTML = SVG_PLAY; playPauseBtn.innerHTML = SVG_PLAY;
// Skip to next track on error to prevent queue stalling
if (player.currentTrack) {
console.warn('Skipping to next track due to playback error');
setTimeout(() => player.playNext(), 1000); // Small delay to avoid rapid skipping
}
}); });
playPauseBtn.addEventListener('click', () => player.handlePlayPause()); playPauseBtn.addEventListener('click', () => player.handlePlayPause());
@ -548,6 +559,123 @@ export async function handleTrackAction(
return; return;
} }
if (action === 'track-mix' && type === 'track') {
if (item.mixes && item.mixes.TRACK_MIX) {
navigate(`/mix/${item.mixes.TRACK_MIX}`);
}
return;
}
// Collection Actions (Album, Playlist, Mix)
const isCollection = ['album', 'playlist', 'user-playlist', 'mix'].includes(type);
const collectionActions = [
'play-card',
'shuffle-play-card',
'add-to-queue',
'play-next',
'download',
'start-mix',
];
if (isCollection && collectionActions.includes(action)) {
try {
let tracks = [];
let collectionItem = item;
if (type === 'album') {
const data = await api.getAlbum(item.id);
tracks = data.tracks;
collectionItem = data.album || item;
} else if (type === 'playlist') {
const data = await api.getPlaylist(item.uuid);
tracks = data.tracks;
collectionItem = data.playlist || item;
} else if (type === 'user-playlist') {
let playlist = await db.getPlaylist(item.id);
if (!playlist) {
try {
playlist = await syncManager.getPublicPlaylist(item.id);
} catch {
/* ignore */
}
}
tracks = playlist ? playlist.tracks : item.tracks || [];
collectionItem = playlist || item;
} else if (type === 'mix') {
const data = await api.getMix(item.id);
tracks = data.tracks;
collectionItem = data.mix || item;
}
if (tracks.length === 0 && action !== 'start-mix') {
showNotification(`No tracks found in this ${type}`);
return;
}
if (action === 'download') {
if (type === 'album') {
await downloadAlbumAsZip(collectionItem, tracks, api, downloadQualitySettings.getQuality(), lyricsManager);
} else {
await downloadPlaylistAsZip(collectionItem, tracks, api, downloadQualitySettings.getQuality(), lyricsManager);
}
return;
}
if (action === 'add-to-queue') {
player.addToQueue(tracks);
if (window.renderQueueFunction) window.renderQueueFunction();
showNotification(`Added ${tracks.length} tracks to queue`);
return;
}
if (action === 'play-next') {
player.addNextToQueue(tracks);
if (window.renderQueueFunction) window.renderQueueFunction();
showNotification(`Playing next: ${tracks.length} tracks`);
return;
}
if (action === 'start-mix') {
if (type === 'album' && collectionItem.artist?.id) {
const artistData = await api.getArtist(collectionItem.artist.id);
if (artistData.mixes?.ARTIST_MIX) {
navigate(`/mix/${artistData.mixes.ARTIST_MIX}`);
return;
}
}
// Fallback to item's own page or first track's mix
if (tracks.length > 0 && tracks[0].mixes?.TRACK_MIX) {
navigate(`/mix/${tracks[0].mixes.TRACK_MIX}`);
} else {
navigate(`/${type.replace('user-', '')}/${item.id || item.uuid}`);
}
return;
}
// play-card and shuffle-play-card
if (action === 'shuffle-play-card') {
player.shuffleActive = true;
const tracksToShuffle = [...tracks];
tracksToShuffle.sort(() => Math.random() - 0.5);
player.setQueue(tracksToShuffle, 0);
const shuffleBtn = document.getElementById('shuffle-btn');
if (shuffleBtn) shuffleBtn.classList.add('active');
} else {
player.setQueue(tracks, 0);
const shuffleBtn = document.getElementById('shuffle-btn');
if (shuffleBtn) shuffleBtn.classList.remove('active');
}
player.playAtIndex(0);
const name = type === 'user-playlist' ? collectionItem.name : collectionItem.title;
showNotification(`Playing ${type.replace('user-', '')}: ${name}`);
} catch (error) {
console.error('Failed to handle collection action:', error);
showNotification(`Failed to process ${type} action`);
}
return;
}
// Individual Track Actions
if (action === 'add-to-queue') { if (action === 'add-to-queue') {
player.addToQueue(item); player.addToQueue(item);
if (window.renderQueueFunction) window.renderQueueFunction(); if (window.renderQueueFunction) window.renderQueueFunction();
@ -556,53 +684,20 @@ export async function handleTrackAction(
player.addNextToQueue(item); player.addNextToQueue(item);
if (window.renderQueueFunction) window.renderQueueFunction(); if (window.renderQueueFunction) window.renderQueueFunction();
showNotification(`Playing next: ${item.title}`); showNotification(`Playing next: ${item.title}`);
} else if (action === 'track-mix') {
if (item.mixes && item.mixes.TRACK_MIX) {
navigate(`/mix/${item.mixes.TRACK_MIX}`);
}
} else if (action === 'play-card') { } else if (action === 'play-card') {
try { player.setQueue([item], 0);
let tracks = []; player.playAtIndex(0);
if (type === 'album') { showNotification(`Playing track: ${item.title}`);
const data = await api.getAlbum(item.id); } else if (action === 'start-mix') {
tracks = data.tracks; if (item.mixes?.TRACK_MIX) {
} else if (type === 'playlist') { navigate(`/mix/${item.mixes.TRACK_MIX}`);
const data = await api.getPlaylist(item.uuid); } else {
tracks = data.tracks; showNotification('No mix available for this track');
} else if (type === 'user-playlist') {
let playlist = await db.getPlaylist(item.id);
if (!playlist) {
try {
playlist = await syncManager.getPublicPlaylist(item.id);
} catch {
// Ignore
}
}
tracks = playlist ? playlist.tracks : item.tracks || [];
if (playlist) item.name = playlist.name;
} else if (type === 'mix') {
const data = await api.getMix(item.id);
tracks = data.tracks;
if (data.mix) item.title = data.mix.title;
}
if (tracks.length > 0) {
player.setQueue(tracks, 0);
const shuffleBtn = document.getElementById('shuffle-btn');
if (shuffleBtn) shuffleBtn.classList.remove('active');
player.playAtIndex(0);
const name = type === 'user-playlist' ? item.name : item.title;
showNotification(`Playing ${type.replace('user-', '')}: ${name}`);
} else {
showNotification(`No tracks found in this ${type}`);
}
} catch (error) {
console.error('Failed to play card:', error);
showNotification(`Failed to play ${type}`);
} }
} else if (action === 'download') { } else if (action === 'download') {
await downloadTrackWithMetadata(item, downloadQualitySettings.getQuality(), api, lyricsManager); await downloadTrackWithMetadata(item, downloadQualitySettings.getQuality(), api, lyricsManager);
} else if (action === 'toggle-like') { }
else if (action === 'toggle-like') {
const added = await db.toggleFavorite(type, item); const added = await db.toggleFavorite(type, item);
syncManager.syncLibraryItem(type, item, added); syncManager.syncLibraryItem(type, item, added);
@ -714,10 +809,9 @@ export async function handleTrackAction(
return ` return `
<div class="modal-option ${alreadyContains ? 'already-contains' : ''}" data-id="${p.id}"> <div class="modal-option ${alreadyContains ? 'already-contains' : ''}" data-id="${p.id}">
<span>${p.name}</span> <span>${p.name}</span>
${ ${alreadyContains
alreadyContains ? `<button class="remove-from-playlist-btn-modal" title="Remove from playlist" style="background: transparent; border: none; color: inherit; cursor: pointer; padding: 4px; display: flex; align-items: center;">${SVG_BIN}</button>`
? `<button class="remove-from-playlist-btn-modal" title="Remove from playlist" style="background: transparent; border: none; color: inherit; cursor: pointer; padding: 4px; display: flex; align-items: center;">${SVG_BIN}</button>` : ''
: ''
} }
</div> </div>
`; `;
@ -802,6 +896,25 @@ async function updateContextMenuLikeState(contextMenu, contextTrack) {
const hasMix = contextTrack.mixes && contextTrack.mixes.TRACK_MIX; const hasMix = contextTrack.mixes && contextTrack.mixes.TRACK_MIX;
trackMixItem.style.display = hasMix ? 'block' : 'none'; trackMixItem.style.display = hasMix ? 'block' : 'none';
} }
// Filter items based on type
const type = contextMenu._contextType || 'track';
contextMenu.querySelectorAll('li[data-action]').forEach((item) => {
const filter = item.dataset.typeFilter;
if (filter) {
const types = filter.split(',');
item.style.display = types.includes(type) ? 'block' : 'none';
} else {
item.style.display = 'block';
}
// Update labels for Like/Save
if (item.dataset.action === 'toggle-like') {
const labelKey = `label${type.charAt(0).toUpperCase() + type.slice(1).replace('User-playlist', 'Playlist')}`;
const label = item.dataset[labelKey] || item.dataset.labelTrack || 'Like';
item.textContent = label;
}
});
} }
export function initializeTrackInteractions(player, api, mainContent, contextMenu, lyricsManager, ui, scrobbler) { export function initializeTrackInteractions(player, api, mainContent, contextMenu, lyricsManager, ui, scrobbler) {
@ -850,6 +963,30 @@ export function initializeTrackInteractions(player, api, mainContent, contextMen
return; return;
} }
const cardMenuBtn = e.target.closest('.card-menu-btn');
if (cardMenuBtn) {
e.stopPropagation();
const card = cardMenuBtn.closest('.card');
const type = cardMenuBtn.dataset.type;
const id = cardMenuBtn.dataset.id;
let item = card ? trackDataStore.get(card) : null;
if (!item) {
// Fallback: create a shell item
item = { id, uuid: id, title: card.querySelector('.card-title')?.textContent || 'Item' };
}
contextTrack = item;
contextMenu._contextTrack = item;
contextMenu._contextType = type;
await updateContextMenuLikeState(contextMenu, item);
const rect = cardMenuBtn.getBoundingClientRect();
positionMenu(contextMenu, rect.left, rect.bottom + 5, rect);
return;
}
const menuBtn = e.target.closest('.track-menu-btn'); const menuBtn = e.target.closest('.track-menu-btn');
if (menuBtn) { if (menuBtn) {
e.stopPropagation(); e.stopPropagation();
@ -940,12 +1077,48 @@ export function initializeTrackInteractions(player, api, mainContent, contextMen
}); });
contextMenu._contextTrack = contextTrack; contextMenu._contextTrack = contextTrack;
contextMenu._contextType = 'track';
await updateContextMenuLikeState(contextMenu, contextTrack); await updateContextMenuLikeState(contextMenu, contextTrack);
positionMenu(contextMenu, e.pageX, e.pageY); positionMenu(contextMenu, e.pageX, e.pageY);
} }
} }
}); });
mainContent.addEventListener('contextmenu', async (e) => {
const trackItem = e.target.closest('.track-item, .queue-track-item');
const card = e.target.closest('.card');
if (trackItem) {
e.preventDefault();
if (trackItem.classList.contains('queue-track-item')) {
const queueIndex = parseInt(trackItem.dataset.queueIndex);
contextTrack = player.getCurrentQueue()[queueIndex];
} else {
contextTrack = trackDataStore.get(trackItem);
}
if (contextTrack) {
if (contextTrack.isLocal) return;
contextMenu._contextTrack = contextTrack;
contextMenu._contextType = 'track';
await updateContextMenuLikeState(contextMenu, contextTrack);
positionMenu(contextMenu, e.pageX, e.pageY);
}
} else if (card) {
e.preventDefault();
const type = card.dataset.albumId ? 'album' : card.dataset.playlistId ? 'playlist' : card.dataset.mixId ? 'mix' : card.dataset.href ? card.dataset.href.split('/')[1] : 'item';
const id = card.dataset.albumId || card.dataset.playlistId || card.dataset.mixId;
const item = trackDataStore.get(card) || { id, uuid: id, title: card.querySelector('.card-title')?.textContent };
contextTrack = item;
contextMenu._contextTrack = item;
contextMenu._contextType = type.replace('userplaylist', 'user-playlist');
await updateContextMenuLikeState(contextMenu, item);
positionMenu(contextMenu, e.pageX, e.pageY);
}
});
document.addEventListener('click', () => { document.addEventListener('click', () => {
contextMenu.style.display = 'none'; contextMenu.style.display = 'none';
}); });
@ -957,10 +1130,12 @@ export function initializeTrackInteractions(player, api, mainContent, contextMen
const action = target.dataset.action; const action = target.dataset.action;
const track = contextMenu._contextTrack || contextTrack; const track = contextMenu._contextTrack || contextTrack;
const type = contextMenu._contextType || 'track';
if (action && track) { if (action && track) {
await handleTrackAction(action, track, player, api, lyricsManager, 'track', ui, scrobbler); await handleTrackAction(action, track, player, api, lyricsManager, type, ui, scrobbler);
} }
contextMenu.style.display = 'none'; contextMenu.style.display = 'none';
contextMenu._contextType = null;
}); });
// Now playing bar interactions // Now playing bar interactions

View file

@ -1,4 +1,4 @@
import { getExtensionForQuality, getCoverBlob } from './utils.js'; import { getCoverBlob } from './utils.js';
const VENDOR_STRING = 'Monochrome'; const VENDOR_STRING = 'Monochrome';
const DEFAULT_TITLE = 'Unknown Title'; const DEFAULT_TITLE = 'Unknown Title';
@ -14,8 +14,6 @@ const DEFAULT_ALBUM = 'Unknown Album';
* @returns {Promise<Blob>} - Audio blob with embedded metadata * @returns {Promise<Blob>} - Audio blob with embedded metadata
*/ */
export async function addMetadataToAudio(audioBlob, track, api, quality) { export async function addMetadataToAudio(audioBlob, track, api, quality) {
const extension = getExtensionForQuality(quality);
if (quality === 'HI_RES_LOSSLESS') { if (quality === 'HI_RES_LOSSLESS') {
return await addFlacMetadata(audioBlob, track, api); return await addFlacMetadata(audioBlob, track, api);
} }

View file

@ -244,7 +244,7 @@ export class Player {
// Warm connection/cache // Warm connection/cache
// For Blob URLs (DASH), this head request is not needed and can cause errors. // For Blob URLs (DASH), this head request is not needed and can cause errors.
if (!streamUrl.startsWith('blob:')) { if (!streamUrl.startsWith('blob:')) {
fetch(streamUrl, { method: 'HEAD', signal: this.preloadAbortController.signal }).catch(() => {}); fetch(streamUrl, { method: 'HEAD', signal: this.preloadAbortController.signal }).catch(() => { });
} }
} catch (error) { } catch (error) {
if (error.name !== 'AbortError') { if (error.name !== 'AbortError') {
@ -254,7 +254,7 @@ export class Player {
} }
} }
async playTrackFromQueue(startTime = 0) { async playTrackFromQueue(startTime = 0, recursiveCount = 0) {
const currentQueue = this.shuffleActive ? this.shuffledQueue : this.queue; const currentQueue = this.shuffleActive ? this.shuffledQueue : this.queue;
if (this.currentQueueIndex < 0 || this.currentQueueIndex >= currentQueue.length) { if (this.currentQueueIndex < 0 || this.currentQueueIndex >= currentQueue.length) {
return; return;
@ -306,6 +306,8 @@ export class Player {
document.title = `${trackTitle}${getTrackArtists(track)}`; document.title = `${trackTitle}${getTrackArtists(track)}`;
this.updatePlayingTrackIndicator(); this.updatePlayingTrackIndicator();
this.updateMediaSession(track);
this.updateMediaSessionPlaybackState();
try { try {
let streamUrl; let streamUrl;
@ -417,12 +419,13 @@ export class Player {
} }
} }
// Update Media Session AFTER play starts to ensure metadata is captured
this.updateMediaSession(track);
this.updateMediaSessionPlaybackState();
this.preloadNextTracks(); this.preloadNextTracks();
} catch (error) { } catch (error) {
console.error(`Could not play track: ${trackTitle}`, error); console.error(`Could not play track: ${trackTitle}`, error);
// Skip to next track on unexpected error
if (recursiveCount < currentQueue.length) {
setTimeout(() => this.playNext(recursiveCount + 1), 1000);
}
} }
} }
@ -430,7 +433,7 @@ export class Player {
const currentQueue = this.shuffleActive ? this.shuffledQueue : this.queue; const currentQueue = this.shuffleActive ? this.shuffledQueue : this.queue;
if (index >= 0 && index < currentQueue.length) { if (index >= 0 && index < currentQueue.length) {
this.currentQueueIndex = index; this.currentQueueIndex = index;
this.playTrackFromQueue(); this.playTrackFromQueue(0, 0);
} }
} }
@ -445,7 +448,7 @@ export class Player {
} }
if (this.repeatMode === REPEAT_MODE.ONE && !currentQueue[this.currentQueueIndex]?.isUnavailable) { if (this.repeatMode === REPEAT_MODE.ONE && !currentQueue[this.currentQueueIndex]?.isUnavailable) {
this.playTrackFromQueue(); this.playTrackFromQueue(0, recursiveCount);
return; return;
} }
@ -465,7 +468,7 @@ export class Player {
return; return;
} }
this.playTrackFromQueue(); this.playTrackFromQueue(0, recursiveCount);
} }
playPrev(recursiveCount = 0) { playPrev(recursiveCount = 0) {
@ -486,14 +489,14 @@ export class Player {
if (currentQueue[this.currentQueueIndex].isUnavailable) { if (currentQueue[this.currentQueueIndex].isUnavailable) {
return this.playPrev(recursiveCount + 1); return this.playPrev(recursiveCount + 1);
} }
this.playTrackFromQueue(); this.playTrackFromQueue(0, recursiveCount);
} }
} }
handlePlayPause() { handlePlayPause() {
if (!this.audio.src || this.audio.error) { if (!this.audio.src || this.audio.error) {
if (this.currentTrack) { if (this.currentTrack) {
this.playTrackFromQueue(); this.playTrackFromQueue(0, 0);
} }
return; return;
} }
@ -503,7 +506,7 @@ export class Player {
if (e.name === 'NotAllowedError' || e.name === 'AbortError') return; if (e.name === 'NotAllowedError' || e.name === 'AbortError') return;
console.error('Play failed, reloading track:', e); console.error('Play failed, reloading track:', e);
if (this.currentTrack) { if (this.currentTrack) {
this.playTrackFromQueue(); this.playTrackFromQueue(0, 0);
} }
}); });
} else { } else {
@ -571,30 +574,29 @@ export class Player {
this.saveQueueState(); this.saveQueueState();
} }
addToQueue(track) { addToQueue(trackOrTracks) {
this.queue.push(track); const tracks = Array.isArray(trackOrTracks) ? trackOrTracks : [trackOrTracks];
this.queue.push(...tracks);
if (!this.currentTrack || this.currentQueueIndex === -1) { if (!this.currentTrack || this.currentQueueIndex === -1) {
this.currentQueueIndex = this.queue.length - 1; this.currentQueueIndex = this.queue.length - tracks.length;
this.playTrackFromQueue(); this.playTrackFromQueue(0, 0);
} }
this.saveQueueState(); this.saveQueueState();
} }
addNextToQueue(track) { addNextToQueue(trackOrTracks) {
const tracks = Array.isArray(trackOrTracks) ? trackOrTracks : [trackOrTracks];
const currentQueue = this.shuffleActive ? this.shuffledQueue : this.queue; const currentQueue = this.shuffleActive ? this.shuffledQueue : this.queue;
const insertIndex = this.currentQueueIndex + 1; const insertIndex = this.currentQueueIndex + 1;
// Insert after current track // Insert after current track
currentQueue.splice(insertIndex, 0, track); currentQueue.splice(insertIndex, 0, ...tracks);
// If we are shuffling, we might want to also add it to the original queue for consistency, // If we are shuffling, we might want to also add it to the original queue for consistency,
// though syncing that is tricky. The standard logic often just appends to the active queue view. // though syncing that is tricky. The standard logic often just appends to the active queue view.
if (this.shuffleActive) { if (this.shuffleActive) {
this.originalQueueBeforeShuffle.push(track); // Just append to end of main list? Or logic needed. this.originalQueueBeforeShuffle.push(...tracks); // Sync original queue
// Simplest is to just modify the active playing queue.
} else {
// In linear mode, `currentQueue` IS `this.queue`
} }
this.saveQueueState(); this.saveQueueState();

View file

@ -3,7 +3,6 @@ import { escapeHtml, SVG_DOWNLOAD } from './utils.js';
let artistsData = []; let artistsData = [];
let globalPlayer = null; let globalPlayer = null;
let globalUi = null;
async function loadArtistsData() { async function loadArtistsData() {
try { try {
@ -17,7 +16,7 @@ async function loadArtistsData() {
.map((line) => { .map((line) => {
try { try {
return JSON.parse(line); return JSON.parse(line);
} catch (e) { } catch {
return null; return null;
} }
}) })
@ -487,9 +486,8 @@ function showEraSongs(era, artistName) {
closeBtn.onclick = closeModal; closeBtn.onclick = closeModal;
} }
export async function initTracker(player, ui) { export async function initTracker(player) {
globalPlayer = player; globalPlayer = player;
globalUi = ui;
await loadArtistsData(); await loadArtistsData();
const checkAndRenderTracker = async () => { const checkAndRenderTracker = async () => {

View file

@ -302,16 +302,17 @@ export class UIRenderer {
<button class="play-btn card-play-btn" data-action="play-card" data-type="${type}" data-id="${id}" title="Play"> <button class="play-btn card-play-btn" data-action="play-card" data-type="${type}" data-id="${id}" title="Play">
${SVG_PLAY} ${SVG_PLAY}
</button> </button>
<button class="card-menu-btn" data-action="card-menu" data-type="${type}" data-id="${id}" title="Menu">
${SVG_MENU}
</button>
` `
: ''; : '';
const cardContent = const cardContent = `
type === 'artist' <div class="card-info">
? `<h4 class="card-title">${title}</h4>` <h4 class="card-title">${title}</h4>
: `<div class="card-info"> ${subtitle ? `<p class="card-subtitle">${subtitle}</p>` : ''}
<h4 class="card-title">${title}</h4> </div>`;
<p class="card-subtitle">${subtitle}</p>
</div>`;
// In compact mode, move the play button outside the wrapper to position it on the right side of the card // In compact mode, move the play button outside the wrapper to position it on the right side of the card
const buttonsInWrapper = !isCompact ? playBtnHTML : ''; const buttonsInWrapper = !isCompact ? playBtnHTML : '';
@ -1532,10 +1533,10 @@ export class UIRenderer {
dateDisplay = dateDisplay =
window.innerWidth > 768 window.innerWidth > 768
? releaseDate.toLocaleDateString('en-US', { ? releaseDate.toLocaleDateString('en-US', {
year: 'numeric', year: 'numeric',
month: 'long', month: 'long',
day: 'numeric', day: 'numeric',
}) })
: year; : year;
} }
} }
@ -2233,6 +2234,14 @@ export class UIRenderer {
if (similar && similar.length > 0) { if (similar && similar.length > 0) {
similarContainer.innerHTML = similar.map((a) => this.createArtistCardHTML(a)).join(''); similarContainer.innerHTML = similar.map((a) => this.createArtistCardHTML(a)).join('');
similarSection.style.display = 'block'; similarSection.style.display = 'block';
similar.forEach((a) => {
const el = similarContainer.querySelector(`[data-artist-id="${a.id}"]`);
if (el) {
trackDataStore.set(el, a);
this.updateLikeState(el, 'artist', a.id);
}
});
} else { } else {
similarSection.style.display = 'none'; similarSection.style.display = 'none';
} }
@ -2259,9 +2268,9 @@ export class UIRenderer {
<span>${artist.popularity}% popularity</span> <span>${artist.popularity}% popularity</span>
<div class="artist-tags"> <div class="artist-tags">
${(artist.artistRoles || []) ${(artist.artistRoles || [])
.filter((role) => role.category) .filter((role) => role.category)
.map((role) => `<span class="artist-tag">${role.category}</span>`) .map((role) => `<span class="artist-tag">${role.category}</span>`)
.join('')} .join('')}
</div> </div>
`; `;

View file

@ -735,9 +735,19 @@ body.has-page-background .track-item:hover {
.card-like-btn { .card-like-btn {
position: absolute; position: absolute;
top: 2%;
right: 2%; right: 2%;
background: rgb(0, 0, 0, 0.25) !important; top: 2%;
}
.card-menu-btn {
position: absolute;
left: 2%;
top: 2%;
}
.card-like-btn,
.card-menu-btn {
background: rgb(0, 0, 0, 0.4) !important;
backdrop-filter: blur(8px); backdrop-filter: blur(8px);
border-radius: 50% !important; border-radius: 50% !important;
width: 32px !important; width: 32px !important;
@ -752,15 +762,18 @@ body.has-page-background .track-item:hover {
z-index: 10; z-index: 10;
color: white !important; color: white !important;
border: none !important; border: none !important;
cursor: pointer;
} }
.card:hover .card-like-btn, .card:hover .card-like-btn,
.card:hover .card-menu-btn,
.card-like-btn.active { .card-like-btn.active {
opacity: 1; opacity: 1;
transform: scale(1); transform: scale(1);
} }
.card-like-btn:hover { .card-like-btn:hover,
.card-menu-btn:hover {
background: rgb(0, 0, 0, 0.7) !important; background: rgb(0, 0, 0, 0.7) !important;
transform: scale(1.1) !important; transform: scale(1.1) !important;
} }
@ -991,6 +1004,13 @@ body.has-page-background .track-item:hover {
/* Reset in case inherited */ /* Reset in case inherited */
} }
.album-content-layout .content-section .card-grid .card.compact {
display: inline-flex;
vertical-align: top;
width: 220px;
margin-right: var(--spacing-sm);
}
.album-content-layout .section-title { .album-content-layout .section-title {
font-size: 1.4rem; font-size: 1.4rem;
margin-bottom: var(--spacing-md); margin-bottom: var(--spacing-md);
@ -2929,15 +2949,6 @@ input:checked + .slider::before {
color: var(--foreground); color: var(--foreground);
} }
/* Specific Panel Overrides if needed */
.lyrics-panel {
/* Inherits side-panel */
}
.queue-panel {
/* Inherits side-panel */
}
/* Synced lyrics styling with Apple Music animations */ /* Synced lyrics styling with Apple Music animations */
.synced-line { .synced-line {
padding: 0.5rem 0; padding: 0.5rem 0;