IMP: card three dot menu
This commit is contained in:
parent
d5489edf01
commit
a6736d571f
11 changed files with 2192 additions and 2792 deletions
|
|
@ -51,7 +51,7 @@ class ServerAPI {
|
|||
try {
|
||||
const response = await this.fetchWithRetry(`/album/${id}`);
|
||||
return await response.json();
|
||||
} catch (e) {
|
||||
} catch {
|
||||
const response = await this.fetchWithRetry(`/album?id=${id}`);
|
||||
return await response.json();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ class ServerAPI {
|
|||
try {
|
||||
const response = await this.fetchWithRetry(`/artist/${id}`);
|
||||
return await response.json();
|
||||
} catch (e) {
|
||||
} catch {
|
||||
const response = await this.fetchWithRetry(`/artist?id=${id}`);
|
||||
return await response.json();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ class ServerAPI {
|
|||
try {
|
||||
const response = await this.fetchWithRetry(`/playlist/${id}`);
|
||||
return await response.json();
|
||||
} catch (e) {
|
||||
} catch {
|
||||
// Fallback to query param style
|
||||
const response = await this.fetchWithRetry(`/playlist?id=${id}`);
|
||||
return await response.json();
|
||||
|
|
|
|||
4577
index.html
4577
index.html
File diff suppressed because one or more lines are too long
|
|
@ -334,7 +334,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||
initializeUIInteractions(player, api, ui);
|
||||
initializeKeyboardShortcuts(player, audioPlayer);
|
||||
|
||||
initTracker(player, ui);
|
||||
initTracker(player);
|
||||
|
||||
const castBtn = document.getElementById('cast-btn');
|
||||
initializeCasting(audioPlayer, castBtn);
|
||||
|
|
|
|||
273
js/events.js
273
js/events.js
|
|
@ -10,7 +10,12 @@ import {
|
|||
SVG_BIN,
|
||||
} from './utils.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 { updateTabTitle, navigate } from './router.js';
|
||||
import { db } from './db.js';
|
||||
|
|
@ -116,6 +121,12 @@ export function initializePlayerEvents(player, audioPlayer, scrobbler, ui) {
|
|||
audioPlayer.addEventListener('error', (e) => {
|
||||
console.error('Audio playback error:', e);
|
||||
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());
|
||||
|
|
@ -548,6 +559,123 @@ export async function handleTrackAction(
|
|||
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') {
|
||||
player.addToQueue(item);
|
||||
if (window.renderQueueFunction) window.renderQueueFunction();
|
||||
|
|
@ -556,53 +684,20 @@ export async function handleTrackAction(
|
|||
player.addNextToQueue(item);
|
||||
if (window.renderQueueFunction) window.renderQueueFunction();
|
||||
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') {
|
||||
try {
|
||||
let tracks = [];
|
||||
if (type === 'album') {
|
||||
const data = await api.getAlbum(item.id);
|
||||
tracks = data.tracks;
|
||||
} else if (type === 'playlist') {
|
||||
const data = await api.getPlaylist(item.uuid);
|
||||
tracks = data.tracks;
|
||||
} 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}`);
|
||||
player.setQueue([item], 0);
|
||||
player.playAtIndex(0);
|
||||
showNotification(`Playing track: ${item.title}`);
|
||||
} else if (action === 'start-mix') {
|
||||
if (item.mixes?.TRACK_MIX) {
|
||||
navigate(`/mix/${item.mixes.TRACK_MIX}`);
|
||||
} else {
|
||||
showNotification('No mix available for this track');
|
||||
}
|
||||
} else if (action === 'download') {
|
||||
await downloadTrackWithMetadata(item, downloadQualitySettings.getQuality(), api, lyricsManager);
|
||||
} else if (action === 'toggle-like') {
|
||||
}
|
||||
else if (action === 'toggle-like') {
|
||||
const added = await db.toggleFavorite(type, item);
|
||||
syncManager.syncLibraryItem(type, item, added);
|
||||
|
||||
|
|
@ -714,10 +809,9 @@ export async function handleTrackAction(
|
|||
return `
|
||||
<div class="modal-option ${alreadyContains ? 'already-contains' : ''}" data-id="${p.id}">
|
||||
<span>${p.name}</span>
|
||||
${
|
||||
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>`
|
||||
: ''
|
||||
${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>`
|
||||
: ''
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -802,6 +896,25 @@ async function updateContextMenuLikeState(contextMenu, contextTrack) {
|
|||
const hasMix = contextTrack.mixes && contextTrack.mixes.TRACK_MIX;
|
||||
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) {
|
||||
|
|
@ -850,6 +963,30 @@ export function initializeTrackInteractions(player, api, mainContent, contextMen
|
|||
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');
|
||||
if (menuBtn) {
|
||||
e.stopPropagation();
|
||||
|
|
@ -940,12 +1077,48 @@ export function initializeTrackInteractions(player, api, mainContent, contextMen
|
|||
});
|
||||
|
||||
contextMenu._contextTrack = contextTrack;
|
||||
contextMenu._contextType = 'track';
|
||||
await updateContextMenuLikeState(contextMenu, contextTrack);
|
||||
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', () => {
|
||||
contextMenu.style.display = 'none';
|
||||
});
|
||||
|
|
@ -957,10 +1130,12 @@ export function initializeTrackInteractions(player, api, mainContent, contextMen
|
|||
|
||||
const action = target.dataset.action;
|
||||
const track = contextMenu._contextTrack || contextTrack;
|
||||
const type = contextMenu._contextType || '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._contextType = null;
|
||||
});
|
||||
|
||||
// Now playing bar interactions
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { getExtensionForQuality, getCoverBlob } from './utils.js';
|
||||
import { getCoverBlob } from './utils.js';
|
||||
|
||||
const VENDOR_STRING = 'Monochrome';
|
||||
const DEFAULT_TITLE = 'Unknown Title';
|
||||
|
|
@ -14,8 +14,6 @@ const DEFAULT_ALBUM = 'Unknown Album';
|
|||
* @returns {Promise<Blob>} - Audio blob with embedded metadata
|
||||
*/
|
||||
export async function addMetadataToAudio(audioBlob, track, api, quality) {
|
||||
const extension = getExtensionForQuality(quality);
|
||||
|
||||
if (quality === 'HI_RES_LOSSLESS') {
|
||||
return await addFlacMetadata(audioBlob, track, api);
|
||||
}
|
||||
|
|
|
|||
44
js/player.js
44
js/player.js
|
|
@ -244,7 +244,7 @@ export class Player {
|
|||
// Warm connection/cache
|
||||
// For Blob URLs (DASH), this head request is not needed and can cause errors.
|
||||
if (!streamUrl.startsWith('blob:')) {
|
||||
fetch(streamUrl, { method: 'HEAD', signal: this.preloadAbortController.signal }).catch(() => {});
|
||||
fetch(streamUrl, { method: 'HEAD', signal: this.preloadAbortController.signal }).catch(() => { });
|
||||
}
|
||||
} catch (error) {
|
||||
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;
|
||||
if (this.currentQueueIndex < 0 || this.currentQueueIndex >= currentQueue.length) {
|
||||
return;
|
||||
|
|
@ -306,6 +306,8 @@ export class Player {
|
|||
document.title = `${trackTitle} • ${getTrackArtists(track)}`;
|
||||
|
||||
this.updatePlayingTrackIndicator();
|
||||
this.updateMediaSession(track);
|
||||
this.updateMediaSessionPlaybackState();
|
||||
|
||||
try {
|
||||
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();
|
||||
} catch (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;
|
||||
if (index >= 0 && index < currentQueue.length) {
|
||||
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) {
|
||||
this.playTrackFromQueue();
|
||||
this.playTrackFromQueue(0, recursiveCount);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -465,7 +468,7 @@ export class Player {
|
|||
return;
|
||||
}
|
||||
|
||||
this.playTrackFromQueue();
|
||||
this.playTrackFromQueue(0, recursiveCount);
|
||||
}
|
||||
|
||||
playPrev(recursiveCount = 0) {
|
||||
|
|
@ -486,14 +489,14 @@ export class Player {
|
|||
if (currentQueue[this.currentQueueIndex].isUnavailable) {
|
||||
return this.playPrev(recursiveCount + 1);
|
||||
}
|
||||
this.playTrackFromQueue();
|
||||
this.playTrackFromQueue(0, recursiveCount);
|
||||
}
|
||||
}
|
||||
|
||||
handlePlayPause() {
|
||||
if (!this.audio.src || this.audio.error) {
|
||||
if (this.currentTrack) {
|
||||
this.playTrackFromQueue();
|
||||
this.playTrackFromQueue(0, 0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
@ -503,7 +506,7 @@ export class Player {
|
|||
if (e.name === 'NotAllowedError' || e.name === 'AbortError') return;
|
||||
console.error('Play failed, reloading track:', e);
|
||||
if (this.currentTrack) {
|
||||
this.playTrackFromQueue();
|
||||
this.playTrackFromQueue(0, 0);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
|
|
@ -571,30 +574,29 @@ export class Player {
|
|||
this.saveQueueState();
|
||||
}
|
||||
|
||||
addToQueue(track) {
|
||||
this.queue.push(track);
|
||||
addToQueue(trackOrTracks) {
|
||||
const tracks = Array.isArray(trackOrTracks) ? trackOrTracks : [trackOrTracks];
|
||||
this.queue.push(...tracks);
|
||||
|
||||
if (!this.currentTrack || this.currentQueueIndex === -1) {
|
||||
this.currentQueueIndex = this.queue.length - 1;
|
||||
this.playTrackFromQueue();
|
||||
this.currentQueueIndex = this.queue.length - tracks.length;
|
||||
this.playTrackFromQueue(0, 0);
|
||||
}
|
||||
this.saveQueueState();
|
||||
}
|
||||
|
||||
addNextToQueue(track) {
|
||||
addNextToQueue(trackOrTracks) {
|
||||
const tracks = Array.isArray(trackOrTracks) ? trackOrTracks : [trackOrTracks];
|
||||
const currentQueue = this.shuffleActive ? this.shuffledQueue : this.queue;
|
||||
const insertIndex = this.currentQueueIndex + 1;
|
||||
|
||||
// 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,
|
||||
// though syncing that is tricky. The standard logic often just appends to the active queue view.
|
||||
if (this.shuffleActive) {
|
||||
this.originalQueueBeforeShuffle.push(track); // Just append to end of main list? Or logic needed.
|
||||
// Simplest is to just modify the active playing queue.
|
||||
} else {
|
||||
// In linear mode, `currentQueue` IS `this.queue`
|
||||
this.originalQueueBeforeShuffle.push(...tracks); // Sync original queue
|
||||
}
|
||||
|
||||
this.saveQueueState();
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import { escapeHtml, SVG_DOWNLOAD } from './utils.js';
|
|||
|
||||
let artistsData = [];
|
||||
let globalPlayer = null;
|
||||
let globalUi = null;
|
||||
|
||||
async function loadArtistsData() {
|
||||
try {
|
||||
|
|
@ -17,7 +16,7 @@ async function loadArtistsData() {
|
|||
.map((line) => {
|
||||
try {
|
||||
return JSON.parse(line);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
|
|
@ -487,9 +486,8 @@ function showEraSongs(era, artistName) {
|
|||
closeBtn.onclick = closeModal;
|
||||
}
|
||||
|
||||
export async function initTracker(player, ui) {
|
||||
export async function initTracker(player) {
|
||||
globalPlayer = player;
|
||||
globalUi = ui;
|
||||
await loadArtistsData();
|
||||
|
||||
const checkAndRenderTracker = async () => {
|
||||
|
|
|
|||
37
js/ui.js
37
js/ui.js
|
|
@ -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">
|
||||
${SVG_PLAY}
|
||||
</button>
|
||||
<button class="card-menu-btn" data-action="card-menu" data-type="${type}" data-id="${id}" title="Menu">
|
||||
${SVG_MENU}
|
||||
</button>
|
||||
`
|
||||
: '';
|
||||
|
||||
const cardContent =
|
||||
type === 'artist'
|
||||
? `<h4 class="card-title">${title}</h4>`
|
||||
: `<div class="card-info">
|
||||
<h4 class="card-title">${title}</h4>
|
||||
<p class="card-subtitle">${subtitle}</p>
|
||||
</div>`;
|
||||
const cardContent = `
|
||||
<div class="card-info">
|
||||
<h4 class="card-title">${title}</h4>
|
||||
${subtitle ? `<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
|
||||
const buttonsInWrapper = !isCompact ? playBtnHTML : '';
|
||||
|
|
@ -1532,10 +1533,10 @@ export class UIRenderer {
|
|||
dateDisplay =
|
||||
window.innerWidth > 768
|
||||
? releaseDate.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})
|
||||
: year;
|
||||
}
|
||||
}
|
||||
|
|
@ -2233,6 +2234,14 @@ export class UIRenderer {
|
|||
if (similar && similar.length > 0) {
|
||||
similarContainer.innerHTML = similar.map((a) => this.createArtistCardHTML(a)).join('');
|
||||
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 {
|
||||
similarSection.style.display = 'none';
|
||||
}
|
||||
|
|
@ -2259,9 +2268,9 @@ export class UIRenderer {
|
|||
<span>${artist.popularity}% popularity</span>
|
||||
<div class="artist-tags">
|
||||
${(artist.artistRoles || [])
|
||||
.filter((role) => role.category)
|
||||
.map((role) => `<span class="artist-tag">${role.category}</span>`)
|
||||
.join('')}
|
||||
.filter((role) => role.category)
|
||||
.map((role) => `<span class="artist-tag">${role.category}</span>`)
|
||||
.join('')}
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
|
|
|||
35
styles.css
35
styles.css
|
|
@ -735,9 +735,19 @@ body.has-page-background .track-item:hover {
|
|||
|
||||
.card-like-btn {
|
||||
position: absolute;
|
||||
top: 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);
|
||||
border-radius: 50% !important;
|
||||
width: 32px !important;
|
||||
|
|
@ -752,15 +762,18 @@ body.has-page-background .track-item:hover {
|
|||
z-index: 10;
|
||||
color: white !important;
|
||||
border: none !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.card:hover .card-like-btn,
|
||||
.card:hover .card-menu-btn,
|
||||
.card-like-btn.active {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
.card-like-btn:hover {
|
||||
.card-like-btn:hover,
|
||||
.card-menu-btn:hover {
|
||||
background: rgb(0, 0, 0, 0.7) !important;
|
||||
transform: scale(1.1) !important;
|
||||
}
|
||||
|
|
@ -991,6 +1004,13 @@ body.has-page-background .track-item:hover {
|
|||
/* 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 {
|
||||
font-size: 1.4rem;
|
||||
margin-bottom: var(--spacing-md);
|
||||
|
|
@ -2929,15 +2949,6 @@ input:checked + .slider::before {
|
|||
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-line {
|
||||
padding: 0.5rem 0;
|
||||
|
|
|
|||
Loading…
Reference in a new issue