//js/events.js import { REPEAT_MODE, trackDataStore, formatTime, getTrackArtists, positionMenu, getShareUrl, escapeHtml, } from './utils.js'; import { lastFMStorage, libreFmSettings, listenBrainzSettings, waveformSettings, keyboardShortcuts, } from './storage.js'; import { showNotification, downloadTrackWithMetadata, downloadAlbum, downloadPlaylist } from './downloads.js'; import { downloadQualitySettings } from './storage.js'; import { updateTabTitle, navigate } from './router.js'; import { db } from './db.js'; import { syncManager } from './accounts/pocketbase.js'; import { waveformGenerator } from './waveform.js'; import { audioContextManager } from './audio-context.js'; import { hapticLongPress, hapticMedium, hapticLight } from './haptics.js'; import { trackPlayTrack, trackPauseTrack, trackSkipTrack, trackToggleShuffle, trackToggleRepeat, trackAddToQueue, trackPlayNext, trackLikeTrack, trackUnlikeTrack, trackLikeAlbum, trackUnlikeAlbum, trackLikeArtist, trackUnlikeArtist, trackLikePlaylist, trackUnlikePlaylist, trackDownloadTrack, trackContextMenuAction, trackBlockTrack, trackUnblockTrack, trackBlockAlbum, trackUnblockAlbum, trackBlockArtist, trackUnblockArtist, trackCopyLink, trackOpenInNewTab, trackSetSleepTimer, trackCancelSleepTimer, trackStartMix, trackEvent, } from './analytics.js'; import { SVG_BIN, SVG_MUTE, SVG_PAUSE, SVG_PLAY, SVG_VOLUME, SVG_CHECKBOX, SVG_CHECKBOX_CHECKED } from './icons.js'; import { partyManager } from './listening-party.js'; import { MusicAPI } from './music-api.js'; import { LyricsManager } from './lyrics.js'; import { Player } from './player.js'; let currentTrackIdForWaveform = null; const trackSelection = { selectedIds: new Set(), lastClickedId: null, isSelecting: false, }; let longPressTimer = null; let isLongPress = false; let longPressTrackItem = null; const LONG_PRESS_DURATION = 500; function handleTrackTouchStart(e) { if (!('ontouchstart' in window)) return; const trackItem = e.target.closest('.track-item'); if (!trackItem || trackItem.classList.contains('unavailable')) return; isLongPress = false; longPressTrackItem = trackItem; longPressTimer = setTimeout(async () => { isLongPress = true; toggleTrackSelection(trackItem, true, false); await hapticLongPress(); }, LONG_PRESS_DURATION); } function handleTrackTouchMove(_e) { if (longPressTimer) { clearTimeout(longPressTimer); longPressTimer = null; } } function handleTrackTouchEnd(_e) { if (longPressTimer) { clearTimeout(longPressTimer); longPressTimer = null; } setTimeout(() => { isLongPress = false; longPressTrackItem = null; }, 100); } function isMultiSelectToggle(e) { const shortcut = keyboardShortcuts.getShortcutForAction('multiSelectToggle'); if (!shortcut) return e.ctrlKey || e.metaKey; const key = e.key?.toLowerCase(); const shortcutKey = shortcut.key?.toLowerCase(); if (['control', 'shift', 'alt', 'meta'].includes(shortcutKey)) { if (shortcut.ctrl && !(e.ctrlKey || e.metaKey)) return false; if (shortcut.shift && !e.shiftKey) return false; if (shortcut.alt && !e.altKey) return false; return true; } return ( (shortcut.ctrl ? e.ctrlKey || e.metaKey : !e.ctrlKey && !e.metaKey) && (shortcut.shift ? e.shiftKey : !e.shiftKey) && (shortcut.alt ? e.altKey : !e.altKey) && key === shortcutKey ); } function isMultiSelectRange(e) { const shortcut = keyboardShortcuts.getShortcutForAction('multiSelectRange'); if (!shortcut) return e.shiftKey; const key = e.key?.toLowerCase(); const shortcutKey = shortcut.key?.toLowerCase(); if (['control', 'shift', 'alt', 'meta'].includes(shortcutKey)) { if (shortcut.ctrl && !(e.ctrlKey || e.metaKey)) return false; if (shortcut.shift && !e.shiftKey) return false; if (shortcut.alt && !e.altKey) return false; return true; } return ( (shortcut.ctrl ? e.ctrlKey || e.metaKey : !e.ctrlKey && !e.metaKey) && (shortcut.shift ? e.shiftKey : !e.shiftKey) && (shortcut.alt ? e.altKey : !e.altKey) && key === shortcutKey ); } function getSelectedTracks() { return Array.from(trackSelection.selectedIds); } function updateCheckbox(checkbox, checked) { if (checkbox) { checkbox.innerHTML = checked ? SVG_CHECKBOX_CHECKED(18) : SVG_CHECKBOX(18); checkbox.classList.toggle('checked', checked); } } function toggleTrackSelection(trackItem, ctrlHeld, shiftHeld) { const trackId = trackItem.dataset.trackId; const isSelected = trackSelection.selectedIds.has(trackId); if (ctrlHeld) { if (isSelected) { trackSelection.selectedIds.delete(trackId); trackItem.classList.remove('selected'); updateCheckbox(trackItem.querySelector('.track-checkbox'), false); } else { trackSelection.selectedIds.add(trackId); trackItem.classList.add('selected'); updateCheckbox(trackItem.querySelector('.track-checkbox'), true); } trackSelection.lastClickedId = trackId; } else if (shiftHeld && trackSelection.lastClickedId && trackSelection.lastClickedId !== trackId) { const parentList = trackItem.closest('.track-list') || trackItem.closest('#main-content'); const allTrackElements = Array.from(parentList.querySelectorAll('.track-item')); const lastIndex = allTrackElements.findIndex((el) => el.dataset.trackId === trackSelection.lastClickedId); const currentIndex = allTrackElements.findIndex((el) => el.dataset.trackId === trackId); if (lastIndex !== -1 && currentIndex !== -1) { const start = Math.min(lastIndex, currentIndex); const end = Math.max(lastIndex, currentIndex); for (let i = start; i <= end; i++) { const el = allTrackElements[i]; trackSelection.selectedIds.add(el.dataset.trackId); el.classList.add('selected'); updateCheckbox(el.querySelector('.track-checkbox'), true); } } } else { if (!isSelected) { trackSelection.selectedIds.add(trackId); trackItem.classList.add('selected'); updateCheckbox(trackItem.querySelector('.track-checkbox'), true); } else { trackSelection.selectedIds.delete(trackId); trackItem.classList.remove('selected'); updateCheckbox(trackItem.querySelector('.track-checkbox'), false); } trackSelection.lastClickedId = trackId; } trackSelection.isSelecting = trackSelection.selectedIds.size > 0; document.body.classList.toggle('multi-select-mode', trackSelection.isSelecting); } async function showMultiSelectPlaylistModal(tracks) { const modal = document.createElement('div'); modal.className = 'modal-overlay'; modal.style.cssText = 'position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.8); display: flex; align-items: center; justify-content: center; z-index: 10000;'; modal.innerHTML = ` `; const closeModal = () => { modal.remove(); document.body.style.overflow = ''; }; modal.querySelector('.modal-close').addEventListener('click', closeModal); modal.addEventListener('click', (e) => { if (e.target === modal) closeModal(); }); document.body.appendChild(modal); document.body.style.overflow = 'hidden'; await db.getPlaylists(true).then((playlists) => { const listEl = modal.querySelector('.playlist-list'); if (playlists.length === 0) { listEl.innerHTML = '
No playlists yet
'; } else { listEl.innerHTML = playlists .map( (p) => `
${escapeHtml(p.name)} ${p.tracks?.length || 0} tracks
` ) .join(''); } listEl.querySelectorAll('.playlist-item').forEach((item) => { item.addEventListener('click', async () => { const playlistId = item.dataset.playlistId; for (const track of tracks) { await db.addTrackToPlaylist(playlistId, track); } await syncManager.syncUserPlaylist(await db.getPlaylist(playlistId), 'update'); showNotification(`Added ${tracks.length} tracks to playlist`); closeModal(); }); }); }); modal.querySelector('.create-new-playlist').addEventListener('click', async () => { const name = prompt('Playlist name:'); if (name) { await db.createPlaylist(name, tracks).then((_playlist) => { showNotification(`Created playlist "${name}" with ${tracks.length} tracks`); closeModal(); }); } }); } const playPauseBtn = document.querySelector('.now-playing-bar .play-pause-btn'); const nextBtn = document.getElementById('next-btn'); const prevBtn = document.getElementById('prev-btn'); const shuffleBtn = document.getElementById('shuffle-btn'); const repeatBtn = document.getElementById('repeat-btn'); const homeStartRadioBtn = document.getElementById('home-start-infinite-radio-btn'); const sleepTimerBtnDesktop = document.getElementById('sleep-timer-btn-desktop'); const _volumeBar = document.getElementById('volume-bar'); const volumeFill = document.getElementById('volume-fill'); const volumeBtn = document.getElementById('volume-btn'); const updateVolumeUI = () => { const activeEl = Player.instance.activeElement; const { muted } = activeEl; const volume = Player.instance.userVolume; volumeBtn.innerHTML = muted || volume === 0 ? SVG_MUTE(20) : SVG_VOLUME(20); const effectiveVolume = muted ? 0 : volume * 100; volumeFill.style.setProperty('--volume-level', `${effectiveVolume}%`); volumeFill.style.width = `${effectiveVolume}%`; }; function clearSelection() { trackSelection.selectedIds.clear(); trackSelection.lastClickedId = null; trackSelection.isSelecting = false; document.body.classList.remove('multi-select-mode'); document.querySelectorAll('.track-item.selected').forEach((el) => { el.classList.remove('selected'); }); document.querySelectorAll('.track-checkbox').forEach((checkbox) => { checkbox.innerHTML = SVG_CHECKBOX(18); checkbox.classList.remove('checked'); }); updateSelectionBar(); } function updateSelectionBar() { let bar = document.getElementById('selection-bar'); if (!bar) { bar = document.createElement('div'); bar.id = 'selection-bar'; bar.className = 'selection-bar'; bar.innerHTML = ` 0 selected
`; document.body.appendChild(bar); bar.querySelectorAll('button').forEach((btn) => { btn.addEventListener('click', () => handleSelectionAction(btn.dataset.action)); }); } const count = trackSelection.selectedIds.size; bar.querySelector('.selection-count').textContent = `${count} selected`; bar.classList.toggle('visible', count > 0); } async function handleSelectionAction(action) { const selectedIds = getSelectedTracks(); if (selectedIds.length === 0) return; const mainContent = document.getElementById('main-content'); const selectedTracks = []; mainContent.querySelectorAll('.track-item').forEach((item) => { if (trackSelection.selectedIds.has(item.dataset.trackId)) { const track = trackDataStore.get(item); if (track) selectedTracks.push(track); } }); switch (action) { case 'play-selected': if (selectedTracks.length > 0) { Player.instance.setQueue(selectedTracks, 0); document.getElementById('shuffle-btn').classList.remove('active'); Player.instance.playTrackFromQueue(); } break; case 'add-to-queue-selected': if (selectedTracks.length > 0) { Player.instance.addToQueue(selectedTracks); if (window.renderQueueFunction) await window.renderQueueFunction(); showNotification(`Added ${selectedTracks.length} tracks to queue`); } break; case 'add-to-playlist-selected': if (selectedTracks.length > 0) { await showMultiSelectPlaylistModal(selectedTracks); } break; case 'download-selected': if (selectedTracks.length > 0) { showNotification(`Downloading ${selectedTracks.length} tracks`); for (const track of selectedTracks) { await downloadTrackWithMetadata( track, downloadQualitySettings.getQuality(), MusicAPI.instance.tidalAPI, LyricsManager.instance ); } } break; case 'like-selected': for (const track of selectedTracks) { const added = await db.toggleFavorite('track', track); await syncManager.syncLibraryItem('track', track, added); } showNotification(`Liked ${selectedTracks.length} tracks`); break; case 'clear-selection': clearSelection(); break; } } export function initializePlayerEvents(player, audioPlayer, scrobbler, ui) { if (homeStartRadioBtn) { homeStartRadioBtn.addEventListener('click', async () => { await player.enableRadio(); }); } const sleepTimerBtnMobile = document.getElementById('sleep-timer-btn'); // History tracking let historyLoggedTrackId = null; const setupMediaListeners = (element) => { element.addEventListener('loadstart', () => { if (player.activeElement === element) { historyLoggedTrackId = null; } }); element.addEventListener('play', async () => { if (player.activeElement !== element) return; // Initialize audio context manager for EQ (only once) if (!audioContextManager.isReady()) { audioContextManager.init(element); } await audioContextManager.resume(); if (player.currentTrack) { // Track play event trackPlayTrack(player.currentTrack); // Scrobble if (scrobbler.isAuthenticated()) { scrobbler.updateNowPlaying(player.currentTrack); } await updateWaveform(); } playPauseBtn.innerHTML = SVG_PAUSE(20); player.updateMediaSessionPlaybackState(); player.updateMediaSessionPositionState(); updateTabTitle(player); }); element.addEventListener('playing', () => { if (player.activeElement !== element) return; player.updateMediaSessionPlaybackState(); player.updateMediaSessionPositionState(); }); element.addEventListener('pause', () => { if (player.activeElement !== element) return; if (player.currentTrack) { trackPauseTrack(player.currentTrack); } playPauseBtn.innerHTML = SVG_PLAY(20); player.updateMediaSessionPlaybackState(); player.updateMediaSessionPositionState(); }); element.addEventListener('ended', () => { if (player.activeElement !== element) return; player.playNext(); }); element.addEventListener('timeupdate', async () => { if (player.activeElement !== element) return; const { currentTime, duration } = element; if (duration) { const progressFill = document.getElementById('progress-fill'); const currentTimeEl = document.getElementById('current-time'); progressFill.style.width = `${(currentTime / duration) * 100}%`; currentTimeEl.textContent = formatTime(currentTime); // Log to history after 10 seconds of playback if (currentTime >= 10 && player.currentTrack && player.currentTrack.id !== historyLoggedTrackId) { historyLoggedTrackId = player.currentTrack.id; const historyEntry = await db.addToHistory(player.currentTrack); await syncManager.syncHistoryItem(historyEntry); if (window.location.hash === '#recent') { ui.renderRecentPage(); } } } }); element.addEventListener('loadedmetadata', () => { if (player.activeElement !== element) return; const totalDurationEl = document.getElementById('total-duration'); totalDurationEl.textContent = formatTime(element.duration); player.updateMediaSessionPositionState(); }); element.addEventListener('error', (e) => { if (player.activeElement !== element) return; if (!element.src) return; const error = element.error; let errorMsg = 'Unknown error'; if (error) { switch (error.code) { case 1: errorMsg = 'Playback aborted'; break; case 2: errorMsg = 'Network error'; break; case 3: errorMsg = 'Decoding error'; break; case 4: errorMsg = 'Source not supported'; break; } if (error.message) errorMsg += `: ${error.message}`; } console.error(`Media playback error (${element.id}):`, errorMsg, e); playPauseBtn.innerHTML = SVG_PLAY(20); const canFallback = player.quality === 'HI_RES_LOSSLESS' && errorMsg.includes('Source not supported') && errorMsg.includes('0x80004005') && !player.isFallbackRetry; if (canFallback) { console.warn('Hi-Res failed due to DASH.js Error (FUCK DASH)'); } if (player.currentTrack && error && error.code !== 1) { if (player.isFallbackInProgress || canFallback) { return; } console.warn('Skipping to next track due to playback error'); setTimeout(() => player.playNext(), 1000); } }); element.addEventListener('volumechange', () => { if (player.activeElement === element) { updateVolumeUI(); } }); }; setupMediaListeners(audioPlayer); if (player.video) { setupMediaListeners(player.video); } playPauseBtn.addEventListener('click', async () => { await hapticMedium(); player.handlePlayPause(); }); nextBtn.addEventListener('click', async () => { await hapticMedium(); trackSkipTrack(player.currentTrack, 'next'); player.playNext(); }); prevBtn.addEventListener('click', async () => { await hapticMedium(); trackSkipTrack(player.currentTrack, 'previous'); player.playPrev(); }); shuffleBtn.addEventListener('click', async () => { await hapticLight(); player.toggleShuffle(); trackToggleShuffle(player.shuffleActive); shuffleBtn.classList.toggle('active', player.shuffleActive); if (window.renderQueueFunction) await window.renderQueueFunction(); }); repeatBtn.addEventListener('click', async () => { await hapticLight(); const mode = player.toggleRepeat(); trackToggleRepeat(mode === REPEAT_MODE.OFF ? 'off' : mode === REPEAT_MODE.ALL ? 'all' : 'one'); repeatBtn.classList.toggle('active', mode !== REPEAT_MODE.OFF); repeatBtn.classList.toggle('repeat-one', mode === REPEAT_MODE.ONE); repeatBtn.title = mode === REPEAT_MODE.OFF ? 'Repeat' : mode === REPEAT_MODE.ALL ? 'Repeat Queue' : 'Repeat One'; }); window.addEventListener('radio-state-changed', (e) => { if (e.detail && e.detail.enabled) { showNotification('Infinite Radio Enabled'); } }); // Sleep Timer for desktop if (sleepTimerBtnDesktop) { sleepTimerBtnDesktop.addEventListener('click', () => { if (player.isSleepTimerActive()) { player.clearSleepTimer(); trackCancelSleepTimer(); showNotification('Sleep timer cancelled'); } else { showSleepTimerModal(player); } }); } // Sleep Timer for mobile if (sleepTimerBtnMobile) { sleepTimerBtnMobile.addEventListener('click', () => { if (player.isSleepTimerActive()) { player.clearSleepTimer(); trackCancelSleepTimer(); showNotification('Sleep timer cancelled'); } else { showSleepTimerModal(player); } }); } // Waveform Masking Logic const updateWaveform = async () => { const progressBar = document.getElementById('progress-bar'); const playerControls = document.querySelector('.player-controls'); const isTracker = player.currentTrack && (player.currentTrack.isTracker || (player.currentTrack.id && String(player.currentTrack.id).startsWith('tracker-'))); if (!waveformSettings.isEnabled() || !player.currentTrack || isTracker) { if (progressBar) { progressBar.style.webkitMaskImage = ''; progressBar.style.maskImage = ''; progressBar.classList.remove('has-waveform', 'waveform-loaded'); } if (playerControls) { playerControls.classList.remove('waveform-loaded'); } currentTrackIdForWaveform = null; return; } if (progressBar && currentTrackIdForWaveform !== player.currentTrack.id) { currentTrackIdForWaveform = player.currentTrack.id; progressBar.classList.add('has-waveform'); progressBar.classList.remove('waveform-loaded'); if (playerControls) { playerControls.classList.remove('waveform-loaded'); } // Clear current mask while loading progressBar.style.webkitMaskImage = ''; progressBar.style.maskImage = ''; try { const { url: streamUrl } = await player.api.getStreamUrl(player.currentTrack.id, 'LOW'); const waveformData = await waveformGenerator.getWaveform(streamUrl, player.currentTrack.id); if (waveformData && currentTrackIdForWaveform === player.currentTrack.id) { let { peaks, duration } = waveformData; const trackDuration = player.currentTrack.duration; // Padding logic for sync if (trackDuration && duration && duration < trackDuration) { const diff = trackDuration - duration; if (diff > 0.5) { // If difference is significant (> 500ms) // Calculate how many peaks represent the missing time // peaks.length represents 'duration' // X peaks represent 'diff' const peaksPerSecond = peaks.length / duration; const paddingPeaksCount = Math.floor(diff * peaksPerSecond); if (paddingPeaksCount > 0) { const newPeaks = new Float32Array(peaks.length + paddingPeaksCount); // Fill start with 0s (implied by new Float32Array) newPeaks.set(peaks, paddingPeaksCount); peaks = newPeaks; } } } // Create a temporary canvas to generate the mask const canvas = document.createElement('canvas'); const rect = progressBar.getBoundingClientRect(); canvas.width = rect.width || 500; canvas.height = 28; // Fixed height for mask generation waveformGenerator.drawWaveform(canvas, peaks); const dataUrl = canvas.toDataURL(); progressBar.style.webkitMaskImage = `url(${dataUrl})`; progressBar.style.webkitMaskSize = '100% 100%'; progressBar.style.webkitMaskRepeat = 'no-repeat'; progressBar.style.maskImage = `url(${dataUrl})`; progressBar.style.maskSize = '100% 100%'; progressBar.style.maskRepeat = 'no-repeat'; progressBar.classList.add('waveform-loaded'); if (playerControls) { playerControls.classList.add('waveform-loaded'); } } } catch (e) { console.error('Failed to load waveform mask:', e); } } }; window.addEventListener('waveform-toggle', async (e) => { if (!e.detail.enabled) { const progressBar = document.getElementById('progress-bar'); const playerControls = document.querySelector('.player-controls'); if (progressBar) { progressBar.style.webkitMaskImage = ''; progressBar.style.maskImage = ''; progressBar.classList.remove('has-waveform', 'waveform-loaded'); } if (playerControls) { playerControls.classList.remove('waveform-loaded'); } } await updateWaveform(); }); if (volumeBtn) { volumeBtn.addEventListener('click', () => { const activeEl = player.activeElement; activeEl.muted = !activeEl.muted; localStorage.setItem('muted', activeEl.muted); const inactiveEl = player.currentTrack?.type === 'video' ? player.audio : player.video; if (inactiveEl) inactiveEl.muted = activeEl.muted; updateVolumeUI(); }); } const isMuted = localStorage.getItem('muted') === 'true'; audioPlayer.muted = isMuted; if (player.video) player.video.muted = isMuted; updateVolumeUI(); initializeSmoothSliders(player); } function initializeSmoothSliders(player) { const progressBar = document.getElementById('progress-bar'); const progressFill = document.getElementById('progress-fill'); const currentTimeEl = document.getElementById('current-time'); const volumeBar = document.getElementById('volume-bar'); const volumeFill = document.getElementById('volume-fill'); const volumeBtn = document.getElementById('volume-btn'); let isSeeking = false; let wasPlaying = false; let isAdjustingVolume = false; let lastSeekPosition = 0; const seek = (bar, event, setter) => { const rect = bar.getBoundingClientRect(); const position = Math.max(0, Math.min(1, (event.clientX - rect.left) / rect.width)); setter(position); }; const updateSeekUI = (position) => { const activeEl = player.activeElement; if (!isNaN(activeEl.duration)) { progressFill.style.width = `${position * 100}%`; if (currentTimeEl) { currentTimeEl.textContent = formatTime(position * activeEl.duration); } } }; // Progress bar with smooth dragging progressBar.addEventListener('mousedown', (e) => { const activeEl = player.activeElement; isSeeking = true; wasPlaying = !activeEl.paused; if (wasPlaying) activeEl.pause(); seek(progressBar, e, (position) => { lastSeekPosition = position; updateSeekUI(position); }); }); // Touch events for mobile progressBar.addEventListener('touchstart', (e) => { const activeEl = player.activeElement; e.preventDefault(); isSeeking = true; wasPlaying = !activeEl.paused; if (wasPlaying) activeEl.pause(); const touch = e.touches[0]; const rect = progressBar.getBoundingClientRect(); const position = Math.max(0, Math.min(1, (touch.clientX - rect.left) / rect.width)); lastSeekPosition = position; updateSeekUI(position); }); document.addEventListener('mousemove', (e) => { if (isSeeking) { seek(progressBar, e, (position) => { lastSeekPosition = position; updateSeekUI(position); }); } if (isAdjustingVolume) { seek(volumeBar, e, (position) => { const activeEl = player.activeElement; if (activeEl.muted) { activeEl.muted = false; localStorage.setItem('muted', false); const inactiveEl = player.currentTrack?.type === 'video' ? player.audio : player.video; if (inactiveEl) inactiveEl.muted = false; } player.setVolume(position); volumeFill.style.width = `${position * 100}%`; volumeBar.style.setProperty('--volume-level', `${position * 100}%`); }); } }); document.addEventListener('touchmove', (e) => { if (isSeeking) { const touch = e.touches[0]; const rect = progressBar.getBoundingClientRect(); const position = Math.max(0, Math.min(1, (touch.clientX - rect.left) / rect.width)); lastSeekPosition = position; updateSeekUI(position); } if (isAdjustingVolume) { const touch = e.touches[0]; const rect = volumeBar.getBoundingClientRect(); const position = Math.max(0, Math.min(1, (touch.clientX - rect.left) / rect.width)); const activeEl = player.activeElement; if (activeEl.muted) { activeEl.muted = false; localStorage.setItem('muted', false); const inactiveEl = player.currentTrack?.type === 'video' ? player.audio : player.video; if (inactiveEl) inactiveEl.muted = false; } player.setVolume(position); volumeFill.style.width = `${position * 100}%`; volumeBar.style.setProperty('--volume-level', `${position * 100}%`); } }); document.addEventListener('mouseup', () => { if (isSeeking) { const activeEl = player.activeElement; // Commit the seek if (!isNaN(activeEl.duration)) { activeEl.currentTime = lastSeekPosition * activeEl.duration; player.updateMediaSessionPositionState(); if (wasPlaying) activeEl.play(); } isSeeking = false; } if (isAdjustingVolume) { isAdjustingVolume = false; } }); document.addEventListener('touchend', () => { if (isSeeking) { const activeEl = player.activeElement; if (!isNaN(activeEl.duration)) { activeEl.currentTime = lastSeekPosition * activeEl.duration; player.updateMediaSessionPositionState(); if (wasPlaying) activeEl.play(); } isSeeking = false; } if (isAdjustingVolume) { isAdjustingVolume = false; } }); progressBar.addEventListener('click', (e) => { if (!isSeeking) { const activeEl = player.activeElement; // Only handle click if not result of a drag release seek(progressBar, e, (position) => { if (!isNaN(activeEl.duration) && activeEl.duration > 0 && activeEl.duration !== Infinity) { activeEl.currentTime = position * activeEl.duration; player.updateMediaSessionPositionState(); } else if (player.currentTrack && player.currentTrack.duration) { const targetTime = position * player.currentTrack.duration; const progressFill = document.querySelector('.progress-fill'); if (progressFill) progressFill.style.width = `${position * 100}%`; player.playTrackFromQueue(targetTime); } }); } }); volumeBar.addEventListener('mousedown', (e) => { isAdjustingVolume = true; seek(volumeBar, e, (position) => { const activeEl = player.activeElement; if (activeEl.muted) { activeEl.muted = false; localStorage.setItem('muted', false); const inactiveEl = player.currentTrack?.type === 'video' ? player.audio : player.video; if (inactiveEl) inactiveEl.muted = false; } player.setVolume(position); volumeFill.style.width = `${position * 100}%`; volumeBar.style.setProperty('--volume-level', `${position * 100}%`); }); }); volumeBar.addEventListener('touchstart', (e) => { e.preventDefault(); isAdjustingVolume = true; const touch = e.touches[0]; const rect = volumeBar.getBoundingClientRect(); const position = Math.max(0, Math.min(1, (touch.clientX - rect.left) / rect.width)); const activeEl = player.activeElement; if (activeEl.muted) { activeEl.muted = false; localStorage.setItem('muted', false); const inactiveEl = player.currentTrack?.type === 'video' ? player.audio : player.video; if (inactiveEl) inactiveEl.muted = false; } player.setVolume(position); volumeFill.style.width = `${position * 100}%`; volumeBar.style.setProperty('--volume-level', `${position * 100}%`); }); volumeBar.addEventListener('click', (e) => { if (!isAdjustingVolume) { seek(volumeBar, e, (position) => { const activeEl = player.activeElement; if (activeEl.muted) { activeEl.muted = false; localStorage.setItem('muted', false); const inactiveEl = player.currentTrack?.type === 'video' ? player.audio : player.video; if (inactiveEl) inactiveEl.muted = false; } player.setVolume(position); volumeFill.style.width = `${position * 100}%`; volumeBar.style.setProperty('--volume-level', `${position * 100}%`); }); } }); volumeBar.addEventListener( 'wheel', (e) => { e.preventDefault(); const delta = e.deltaY > 0 ? -0.05 : 0.05; const newVolume = Math.max(0, Math.min(1, player.userVolume + delta)); const activeEl = player.activeElement; if (delta > 0 && activeEl.muted) { activeEl.muted = false; localStorage.setItem('muted', false); const inactiveEl = player.currentTrack?.type === 'video' ? player.audio : player.video; if (inactiveEl) inactiveEl.muted = false; } player.setVolume(newVolume); volumeFill.style.width = `${newVolume * 100}%`; volumeBar.style.setProperty('--volume-level', `${newVolume * 100}%`); }, { passive: false } ); volumeBtn?.addEventListener( 'wheel', (e) => { e.preventDefault(); const delta = e.deltaY > 0 ? -0.05 : 0.05; const newVolume = Math.max(0, Math.min(1, player.userVolume + delta)); const activeEl = player.activeElement; if (delta > 0 && activeEl.muted) { activeEl.muted = false; localStorage.setItem('muted', false); const inactiveEl = player.currentTrack?.type === 'video' ? player.audio : player.video; if (inactiveEl) inactiveEl.muted = false; } player.setVolume(newVolume); volumeFill.style.width = `${newVolume * 100}%`; volumeBar.style.setProperty('--volume-level', `${newVolume * 100}%`); }, { passive: false } ); } // Standalone function to show add to playlist modal export async function showAddToPlaylistModal(track) { const modal = document.getElementById('playlist-select-modal'); const list = document.getElementById('playlist-select-list'); const cancelBtn = document.getElementById('playlist-select-cancel'); const overlay = modal.querySelector('.modal-overlay'); const renderModal = async () => { const playlists = await db.getPlaylists(true); const trackId = track.id; const playlistsWithTrack = new Set(); for (const playlist of playlists) { if (playlist.tracks && playlist.tracks.some((t) => t.id == trackId)) { playlistsWithTrack.add(playlist.id); } } list.innerHTML = ` ` + playlists .map((p) => { const alreadyContains = playlistsWithTrack.has(p.id); return ` `; }) .join(''); return true; }; if (!(await renderModal())) return; const closeModal = () => { modal.classList.remove('active'); cleanup(); }; const handleOptionClick = async (e) => { const removeBtn = e.target.closest('.remove-from-playlist-btn-modal'); const option = e.target.closest('.modal-option'); if (!option) return; if (option.classList.contains('create-new-option')) { closeModal(); const createModal = document.getElementById('playlist-modal'); document.getElementById('playlist-modal-title').textContent = 'Create Playlist'; document.getElementById('playlist-name-input').value = ''; document.getElementById('playlist-cover-input').value = ''; document.getElementById('playlist-cover-file-input').value = ''; document.getElementById('playlist-description-input').value = ''; createModal.dataset.editingId = ''; document.getElementById('import-section').style.display = 'none'; // Reset cover upload state const coverUploadBtn = document.getElementById('playlist-cover-upload-btn'); const coverUrlInput = document.getElementById('playlist-cover-input'); const coverToggleUrlBtn = document.getElementById('playlist-cover-toggle-url-btn'); if (coverUploadBtn) { coverUploadBtn.style.flex = '1'; coverUploadBtn.style.display = 'flex'; } if (coverUrlInput) coverUrlInput.style.display = 'none'; if (coverToggleUrlBtn) { coverToggleUrlBtn.textContent = 'or URL'; coverToggleUrlBtn.title = 'Switch to URL input'; } // Pass track createModal._pendingTracks = [track]; createModal.classList.add('active'); document.getElementById('playlist-name-input').focus(); return; } const playlistId = option.dataset.id; if (removeBtn) { e.stopPropagation(); await db.removeTrackFromPlaylist(playlistId, track.id); const updatedPlaylist = await db.getPlaylist(playlistId); await syncManager.syncUserPlaylist(updatedPlaylist, 'update'); showNotification(`Removed from playlist: ${option.querySelector('span').textContent}`); await renderModal(); } else { if (option.classList.contains('already-contains')) return; await db.addTrackToPlaylist(playlistId, track); const updatedPlaylist = await db.getPlaylist(playlistId); await syncManager.syncUserPlaylist(updatedPlaylist, 'update'); showNotification(`Added to playlist: ${option.querySelector('span').textContent}`); closeModal(); } }; const cleanup = () => { cancelBtn.removeEventListener('click', closeModal); overlay.removeEventListener('click', closeModal); list.removeEventListener('click', handleOptionClick); }; cancelBtn.addEventListener('click', closeModal); overlay.addEventListener('click', closeModal); list.addEventListener('click', handleOptionClick); modal.classList.add('active'); } export async function handleTrackAction( action, item, player, api, lyricsManager, type = 'track', ui = null, scrobbler = null, extraData = null ) { if (!item) return; // Actions not allowed for unavailable tracks const forbiddenForUnavailable = [ 'add-to-queue', 'play-next', 'track-mix', 'download', 'start-radio', 'start-infinite-radio', ]; if (item.isUnavailable && forbiddenForUnavailable.includes(action)) { showNotification('This track is unavailable.'); return; } if (action === 'request-song') { if (partyManager.currentParty) { await partyManager.requestSong(item); } else { showNotification('You are not in a listening party'); } return; } if (action === 'start-radio' || action === 'start-infinite-radio') { let tracks = []; if (type === 'track') { tracks = [item]; } else if (item.tracks) { tracks = item.tracks; } else 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') { const playlist = await db.getPlaylist(item.id); tracks = playlist ? playlist.tracks : []; } if (tracks.length > 0) { player.setQueue(tracks, 0); player.playAtIndex(0); player.enableRadio(tracks); showNotification(`Started radio based on ${type}: ${item.title || item.name}`); } else { showNotification('Could not start infinite radio: No tracks found'); } 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 { // Check if album/artist is blocked const { contentBlockingSettings } = await import('./storage.js'); if (type === 'album' && contentBlockingSettings.shouldHideAlbum(item)) { showNotification('This album is blocked'); return; } 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 downloadAlbum( collectionItem, tracks, api, downloadQualitySettings.getQuality(), lyricsManager ); } else { await downloadPlaylist( collectionItem, tracks, api, downloadQualitySettings.getQuality(), lyricsManager ); } return; } // Filter blocked tracks from collections tracks = contentBlockingSettings.filterTracks(tracks); if (action === 'add-to-queue') { player.addToQueue(tracks); if (window.renderQueueFunction) await window.renderQueueFunction(); showNotification(`Added ${tracks.length} tracks to queue`); return; } if (action === 'play-next') { player.addNextToQueue(tracks); if (window.renderQueueFunction) await 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; } if (action === 'toggle-pin') { const pinned = await db.togglePinned(item, type); showNotification(pinned ? `Pinned to sidebar` : `Unpinned from sidebar`); if (ui && typeof ui.renderPinnedItems === 'function') { ui.renderPinnedItems(); } } // Individual Track Actions // Check if track/artist is blocked const { contentBlockingSettings } = await import('./storage.js'); const BLOCKED_PLAY_ACTIONS = new Set(['play-card', 'add-to-queue', 'play-next', 'start-mix']); if (type === 'track' && BLOCKED_PLAY_ACTIONS.has(action) && contentBlockingSettings.shouldHideTrack(item)) { showNotification('This track is blocked'); return; } if (action === 'add-to-queue') { trackAddToQueue(item, 'end'); player.addToQueue(item); if (window.renderQueueFunction) await window.renderQueueFunction(); showNotification(`Added to queue: ${item.title}`); } else if (action === 'play-next') { trackPlayNext(item); player.addNextToQueue(item); if (window.renderQueueFunction) await window.renderQueueFunction(); showNotification(`Playing next: ${item.title}`); } else if (action === 'play-card') { player.setQueue([item], 0); player.playAtIndex(0); showNotification(`Playing track: ${item.title}`); } else if (action === 'start-mix') { trackStartMix(type, item); if (item.mixes?.TRACK_MIX) { navigate(`/mix/${item.mixes.TRACK_MIX}`); } else { showNotification('No mix available for this track'); } } else if (action === 'download') { trackDownloadTrack(item, downloadQualitySettings.getQuality()); await downloadTrackWithMetadata(item, downloadQualitySettings.getQuality(), api, lyricsManager); } else if (action === 'toggle-like') { const added = await db.toggleFavorite(type, item); await syncManager.syncLibraryItem(type, item, added); // Track like/unlike if (added) { if (type === 'track') trackLikeTrack(item); else if (type === 'video') trackEvent('Like Video', { title: item.title }); else if (type === 'album') trackLikeAlbum(item); else if (type === 'artist') trackLikeArtist(item); else if (type === 'playlist' || type === 'user-playlist') trackLikePlaylist(item); } else { if (type === 'track') trackUnlikeTrack(item); else if (type === 'video') trackEvent('Unlike Video', { title: item.title }); else if (type === 'album') trackUnlikeAlbum(item); else if (type === 'artist') trackUnlikeArtist(item); else if (type === 'playlist' || type === 'user-playlist') trackUnlikePlaylist(item); } if (added && type === 'track' && scrobbler) { if (lastFMStorage.isEnabled() && lastFMStorage.shouldLoveOnLike()) { scrobbler.loveTrack(item); } if (libreFmSettings.isEnabled() && libreFmSettings.shouldLoveOnLike()) { scrobbler.loveTrack(item); } if (listenBrainzSettings.isEnabled() && listenBrainzSettings.shouldLoveOnLike()) { scrobbler.loveTrack(item); } } // Update all instances of this item's like button on the page const id = type === 'playlist' ? item.uuid : item.id; const selector = type === 'track' ? `[data-track-id="${id}"] .like-btn` : type === 'video' ? `.card[data-video-id="${id}"] .like-btn` : `.card[data-${type}-id="${id}"] .like-btn, .card[data-playlist-id="${id}"] .like-btn`; // Also check header buttons const headerBtn = document.getElementById(`like-${type}-btn`); const elementsToUpdate = [...document.querySelectorAll(selector)]; if (headerBtn) elementsToUpdate.push(headerBtn); const nowPlayingLikeBtn = document.getElementById('now-playing-like-btn'); if (nowPlayingLikeBtn && (type === 'track' || type === 'video') && player?.currentTrack?.id === item.id) { elementsToUpdate.push(nowPlayingLikeBtn); } const fsLikeBtn = document.getElementById('fs-like-btn'); if (fsLikeBtn && (type === 'track' || type === 'video') && player?.currentTrack?.id === item.id) { elementsToUpdate.push(fsLikeBtn); } elementsToUpdate.forEach((btn) => { const heartIcon = btn.querySelector('svg'); if (heartIcon) { heartIcon.classList.toggle('filled', added); if (heartIcon.hasAttribute('fill')) { heartIcon.setAttribute('fill', added ? 'currentColor' : 'none'); } } btn.classList.toggle('active', added); btn.title = added ? 'Remove from Favorites' : 'Add to Favorites'; }); // Handle Library Page Update if (window.location.pathname.split('/').filter(Boolean)[0] === 'library') { const itemSelector = type === 'track' ? `.track-item[data-track-id="${id}"], .card[data-track-id="${id}"]` : type === 'video' ? `.video-card[data-video-id="${id}"]` : `.card[data-${type}-id="${id}"], .card[data-playlist-id="${id}"]`; const itemEl = document.querySelector(itemSelector); if (!added && itemEl) { // Remove item const container = itemEl.parentElement; itemEl.remove(); if (container && container.children.length === 0) { const msg = type === 'track' ? 'No liked tracks yet.' : type === 'video' ? 'No liked videos yet.' : `No liked ${type}s yet.`; container.innerHTML = `
${msg}
`; } } else if (added && !itemEl && ui && (type === 'track' || type === 'video')) { // Add item if (type === 'track') { const tracksContainer = document.getElementById('library-tracks-container'); if (tracksContainer) { const placeholder = tracksContainer.querySelector('.placeholder-text'); if (placeholder) placeholder.remove(); const layout = localStorage.getItem('libraryLikedTracksView') || 'list'; const tempDiv = document.createElement('div'); if (layout === 'grid') { tracksContainer.classList.remove('track-list'); tracksContainer.classList.add('card-grid'); tempDiv.innerHTML = ui.createTrackCardHTML(item); } else { tracksContainer.classList.remove('card-grid'); tracksContainer.classList.add('track-list'); const index = tracksContainer.children.length; tempDiv.innerHTML = ui.createTrackItemHTML(item, index, true, false, false, true); } const newEl = tempDiv.firstElementChild; if (newEl) { tracksContainer.appendChild(newEl); trackDataStore.set(newEl, item); ui.updateLikeState(newEl, 'track', item.id); const likedToolbar = document.getElementById('library-liked-tracks-toolbar'); if (likedToolbar) likedToolbar.style.display = 'flex'; const shuffleBtn = document.getElementById('shuffle-liked-tracks-btn'); const downloadBtn = document.getElementById('download-liked-tracks-btn'); if (shuffleBtn) shuffleBtn.style.display = 'flex'; if (downloadBtn) downloadBtn.style.display = 'flex'; ui.setupLibraryLikedTracksSearch(tracksContainer); } } } else if (type === 'video') { const videosTabContent = document.getElementById('library-tab-videos'); if (videosTabContent) { const grid = videosTabContent.querySelector('.card-grid'); if (grid) { const placeholder = grid.querySelector('.placeholder-text'); if (placeholder) grid.innerHTML = ''; const videoHTML = ui.createVideoCardHTML(item); const tempDiv = document.createElement('div'); tempDiv.innerHTML = videoHTML; const newEl = tempDiv.firstElementChild; if (newEl) { grid.appendChild(newEl); trackDataStore.set(newEl, item); ui.updateLikeState(newEl, 'video', item.id); newEl.addEventListener('click', (e) => { if ( e.target.closest('.card-play-btn') || e.target.closest('.card-image-container') ) { e.stopPropagation(); player.playVideo(item); } }); } } } } } } } else if (action === 'add-to-playlist') { const modal = document.getElementById('playlist-select-modal'); const list = document.getElementById('playlist-select-list'); const cancelBtn = document.getElementById('playlist-select-cancel'); const overlay = modal.querySelector('.modal-overlay'); const renderModal = async () => { const playlists = await db.getPlaylists(true); // Removed empty check to allow creating new playlist const trackId = item.id; const trackType = item.type || 'track'; const playlistsWithTrack = new Set(); for (const playlist of playlists) { if ( playlist.tracks && playlist.tracks.some((t) => t.id == trackId && (t.type || 'track') === trackType) ) { playlistsWithTrack.add(playlist.id); } } list.innerHTML = ` ` + playlists .map((p) => { const alreadyContains = playlistsWithTrack.has(p.id); return ` `; }) .join(''); return true; }; if (!(await renderModal())) return; const closeModal = () => { modal.classList.remove('active'); cleanup(); }; const handleOptionClick = async (e) => { const removeBtn = e.target.closest('.remove-from-playlist-btn-modal'); const option = e.target.closest('.modal-option'); if (!option) return; if (option.classList.contains('create-new-option')) { closeModal(); const createModal = document.getElementById('playlist-modal'); document.getElementById('playlist-modal-title').textContent = 'Create Playlist'; document.getElementById('playlist-name-input').value = ''; document.getElementById('playlist-cover-input').value = ''; document.getElementById('playlist-cover-file-input').value = ''; document.getElementById('playlist-description-input').value = ''; createModal.dataset.editingId = ''; document.getElementById('import-section').style.display = 'none'; // Reset cover upload state const coverUploadBtn = document.getElementById('playlist-cover-upload-btn'); const coverUrlInput = document.getElementById('playlist-cover-input'); const coverToggleUrlBtn = document.getElementById('playlist-cover-toggle-url-btn'); if (coverUploadBtn) { coverUploadBtn.style.flex = '1'; coverUploadBtn.style.display = 'flex'; } if (coverUrlInput) coverUrlInput.style.display = 'none'; if (coverToggleUrlBtn) { coverToggleUrlBtn.textContent = 'or URL'; coverToggleUrlBtn.title = 'Switch to URL input'; } // Pass track createModal._pendingTracks = [item]; createModal.classList.add('active'); document.getElementById('playlist-name-input').focus(); return; } const playlistId = option.dataset.id; if (removeBtn) { e.stopPropagation(); await db.removeTrackFromPlaylist(playlistId, item.id); const updatedPlaylist = await db.getPlaylist(playlistId); await syncManager.syncUserPlaylist(updatedPlaylist, 'update'); showNotification(`Removed from playlist: ${option.querySelector('span').textContent}`); await renderModal(); } else { if (option.classList.contains('already-contains')) return; await db.addTrackToPlaylist(playlistId, item); const updatedPlaylist = await db.getPlaylist(playlistId); await syncManager.syncUserPlaylist(updatedPlaylist, 'update'); showNotification(`Added to playlist: ${option.querySelector('span').textContent}`); closeModal(); } }; const cleanup = () => { cancelBtn.removeEventListener('click', closeModal); overlay.removeEventListener('click', closeModal); list.removeEventListener('click', handleOptionClick); }; cancelBtn.addEventListener('click', closeModal); overlay.addEventListener('click', closeModal); list.addEventListener('click', handleOptionClick); modal.classList.add('active'); } else if (action === 'go-to-artist') { const artistId = extraData?.artistId || item.artist?.id || item.artists?.[0]?.id; const trackerSheetId = extraData?.trackerSheetId || (item.isTracker ? item.trackerInfo?.sheetId : null); if (trackerSheetId) { navigate(`/unreleased/${trackerSheetId}`); } else if (artistId) { navigate(`/artist/${artistId}`); } } else if (action === 'go-to-album') { if (item.album?.id) { navigate(`/album/${item.album.id}`); } } else if (action === 'copy-link' || action === 'share') { // Use stored href from card if available, otherwise construct URL const contextMenu = document.getElementById('context-menu'); const storedHref = contextMenu?._contextHref; const typeForUrl = type === 'user-playlist' ? 'userplaylist' : type; const url = getShareUrl(storedHref ? storedHref : `/${typeForUrl}/${item.id || item.uuid}`); trackCopyLink(type, item.id || item.uuid); await navigator.clipboard .writeText(url) .then(() => { showNotification('Link copied to clipboard!'); }) .catch(console.error); } else if (action === 'open-in-new-tab') { // Use stored href from card if available, otherwise construct URL const contextMenu = document.getElementById('context-menu'); const storedHref = contextMenu?._contextHref; const url = storedHref ? `${window.location.origin}${storedHref}` : `${window.location.origin}/${type}/${item.id || item.uuid}`; trackOpenInNewTab(type, item.id || item.uuid); window.open(url, '_blank'); } else if (action === 'open-in-harmony') { const albumId = item.id; const harmonyUrl = `https://harmony.pulsewidth.org.uk/release?url=${encodeURIComponent(`https://tidal.com/album/${albumId}`)}>in=®ion=&musicbrainz=&deezer=&itunes=&spotify=&tidal=&beatport=`; window.open(harmonyUrl, '_blank'); } else if (action === 'track-info') { // Show detailed track info modal const isTracker = item.isTracker; let infoHTML = ''; if (isTracker && item.trackerInfo) { // Detailed unreleased/tracker track info const releaseDate = item.trackerInfo.releaseDate || item.streamStartDate; const dateDisplay = releaseDate ? new Date(releaseDate).toLocaleDateString() : 'Unknown'; const addedDate = item.trackerInfo.addedDate ? new Date(item.trackerInfo.addedDate).toLocaleDateString() : 'Unknown'; infoHTML = `

${escapeHtml(item.title)}

Unreleased Track

${item.artists ? `

Artist: ${escapeHtml(Array.isArray(item.artists) ? item.artists.map((a) => a.name || a).join(', ') : item.artists)}

` : ''} ${item.trackerInfo.artist ? `

Tracked Artist: ${escapeHtml(item.trackerInfo.artist)}

` : ''} ${item.trackerInfo.project ? `

Project: ${escapeHtml(item.trackerInfo.project)}

` : ''} ${item.trackerInfo.era ? `

Era: ${escapeHtml(item.trackerInfo.era)}

` : ''} ${item.trackerInfo.timeline ? `

Timeline: ${escapeHtml(item.trackerInfo.timeline)}

` : ''} ${item.trackerInfo.category ? `

Category: ${escapeHtml(item.trackerInfo.category)}

` : ''} ${item.trackerInfo.trackNumber ? `

Track Number: ${escapeHtml(String(item.trackerInfo.trackNumber))}

` : ''}

Duration: ${escapeHtml(formatTime(item.duration))}

${releaseDate !== 'Unknown' ? `

Release Date: ${escapeHtml(dateDisplay)}

` : ''} ${item.trackerInfo.addedDate ? `

Added to Tracker: ${escapeHtml(addedDate)}

` : ''} ${item.trackerInfo.leakedDate ? `

Leak Date: ${escapeHtml(new Date(item.trackerInfo.leakedDate).toLocaleDateString())}

` : ''} ${item.trackerInfo.recordingDate ? `

Recording Date: ${escapeHtml(new Date(item.trackerInfo.recordingDate).toLocaleDateString())}

` : ''}
${ item.trackerInfo.description ? `

Description

${escapeHtml(item.trackerInfo.description)}

` : '' } ${ item.trackerInfo.notes ? `

Notes

${escapeHtml(item.trackerInfo.notes)}

` : '' } ${ item.trackerInfo.sourceUrl ? `

Source URL:

${escapeHtml(item.trackerInfo.sourceUrl)}
` : '' } ${item.id ? `

Track ID: ${escapeHtml(item.id)}

` : ''}
`; } else { // Detailed normal track info const releaseDate = item.album?.releaseDate || item.streamStartDate; const dateDisplay = releaseDate ? new Date(releaseDate).toLocaleDateString() : 'Unknown'; const quality = item.audioQuality || 'Unknown'; const bitrate = item.bitrate ? `${item.bitrate} kbps` : ''; infoHTML = `

${escapeHtml(item.title)}

Artist: ${escapeHtml(getTrackArtists(item))}

Album: ${escapeHtml(item.album?.title || 'Unknown')}

${item.album?.artist?.name ? `

Album Artist: ${escapeHtml(item.album.artist.name)}

` : ''}

Release Date: ${escapeHtml(dateDisplay)}

Duration: ${escapeHtml(formatTime(item.duration))}

${item.trackNumber ? `

Track Number: ${escapeHtml(String(item.trackNumber))}

` : ''} ${item.discNumber ? `

Disc Number: ${escapeHtml(String(item.discNumber))}

` : ''} ${item.version ? `

Version: ${escapeHtml(item.version)}

` : ''} ${item.explicit ? `

Explicit: Yes

` : ''}

Quality: ${escapeHtml(quality)} ${bitrate ? `(${escapeHtml(bitrate)})` : ''}

${ item.credits && item.credits.length > 0 ? `

Credits

${item.credits.map((c) => `

${escapeHtml(c.type)}: ${escapeHtml(c.name)}

`).join('')}
` : '' } ${ item.composers && item.composers.length > 0 ? `

Composers: ${escapeHtml(item.composers.map((c) => c.name).join(', '))}

` : '' } ${ item.lyrics?.text ? `

Has Lyrics

` : '' } ${item.id ? `

Track ID: ${escapeHtml(item.id)}

` : ''} ${item.album?.id ? `

Album ID: ${escapeHtml(item.album.id)}

` : ''}
`; } // Create and show modal const modal = document.createElement('div'); modal.className = 'modal-overlay'; modal.style.cssText = 'position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.8); display: flex; align-items: center; justify-content: center; z-index: 10000;'; modal.innerHTML = infoHTML; modal.onclick = (e) => { if (e.target === modal) modal.remove(); }; const closeBtn = modal.querySelector('.track-info-close-btn'); if (closeBtn) { closeBtn.onclick = () => modal.remove(); } document.body.appendChild(modal); } else if (action === 'open-original-url') { // Open the original source URL for the track let url = null; if (item.isTracker && item.trackerInfo && item.trackerInfo.sourceUrl) { url = item.trackerInfo.sourceUrl; } else if (item.remoteUrl) { url = item.remoteUrl; } if (url) { window.open(url, '_blank'); } else { showNotification('No original URL available for this track.'); } } else if (action === 'block-track') { const { contentBlockingSettings } = await import('./storage.js'); if (contentBlockingSettings.isTrackBlocked(item.id)) { contentBlockingSettings.unblockTrack(item.id); trackUnblockTrack(item); showNotification(`Unblocked track: ${item.title}`); } else { contentBlockingSettings.blockTrack(item); trackBlockTrack(item); showNotification(`Blocked track: ${item.title}`); } } else if (action === 'block-album') { const { contentBlockingSettings } = await import('./storage.js'); const albumId = type === 'album' ? item.id : item.album?.id; const albumTitle = type === 'album' ? item.title || item.name : item.album?.title || item.album?.name; const albumArtist = type === 'album' ? item.artist?.name || item.artist : item.album?.artist?.name || item.album?.artist; if (!albumId) { showNotification('No album information available'); return; } const albumObj = { id: albumId, title: albumTitle, artist: albumArtist }; if (contentBlockingSettings.isAlbumBlocked(albumId)) { contentBlockingSettings.unblockAlbum(albumId); trackUnblockAlbum(albumObj); showNotification(`Unblocked album: ${albumTitle || 'Unknown Album'}`); } else { contentBlockingSettings.blockAlbum(albumObj); trackBlockAlbum(albumObj); showNotification(`Blocked album: ${albumTitle || 'Unknown Album'}`); } } else if (action === 'block-artist') { const { contentBlockingSettings } = await import('./storage.js'); const artistId = item.artist?.id || item.artists?.[0]?.id; const artistName = item.artist?.name || item.artists?.[0]?.name || item.name; if (!artistId) { showNotification('No artist information available'); return; } const artistObj = { id: artistId, name: artistName }; if (contentBlockingSettings.isArtistBlocked(artistId)) { contentBlockingSettings.unblockArtist(artistId); trackUnblockArtist(artistObj); showNotification(`Unblocked artist: ${artistName || 'Unknown Artist'}`); } else { contentBlockingSettings.blockArtist(artistObj); trackBlockArtist(artistObj); showNotification(`Blocked artist: ${artistName || 'Unknown Artist'}`); } } } async function updateContextMenuLikeState(contextMenu, contextTrack) { if (!contextMenu || !contextTrack) return; const type = contextMenu._contextType || 'track'; const likeItem = contextMenu.querySelector('li[data-action="toggle-like"]'); let isLiked = false; if (likeItem) { const key = type === 'playlist' ? contextTrack.uuid : contextTrack.id; isLiked = await db.isFavorite(type, key); } const pinItem = contextMenu.querySelector('li[data-action="toggle-pin"]'); if (pinItem) { const isPinned = await db.isPinned(contextTrack.id || contextTrack.uuid); pinItem.textContent = isPinned ? 'Unpin' : 'Pin'; } const trackMixItem = contextMenu.querySelector('li[data-action="track-mix"]'); if (trackMixItem) { const hasMix = contextTrack.mixes && contextTrack.mixes.TRACK_MIX; trackMixItem.style.display = hasMix ? 'block' : 'none'; } // Show/hide "Open Original URL" only for unreleased/tracker tracks const openOriginalUrlItem = contextMenu.querySelector('li[data-action="open-original-url"]'); if (openOriginalUrlItem) { const isUnreleased = contextTrack.isTracker || (contextTrack.trackerInfo && contextTrack.trackerInfo.sourceUrl); openOriginalUrlItem.style.display = isUnreleased ? 'block' : 'none'; } // Update block/unblock labels const { contentBlockingSettings } = await import('./storage.js'); const blockTrackItem = contextMenu.querySelector('li[data-action="block-track"]'); if (blockTrackItem) { const isBlocked = contentBlockingSettings.isTrackBlocked(contextTrack.id); blockTrackItem.textContent = isBlocked ? blockTrackItem.dataset.labelUnblock || 'Unblock track' : blockTrackItem.dataset.labelBlock || 'Block track'; } const blockAlbumItem = contextMenu.querySelector('li[data-action="block-album"]'); if (blockAlbumItem) { const albumId = type === 'album' ? contextTrack.id : contextTrack.album?.id; const isBlocked = albumId ? contentBlockingSettings.isAlbumBlocked(albumId) : false; blockAlbumItem.textContent = isBlocked ? blockAlbumItem.dataset.labelUnblock || 'Unblock album' : blockAlbumItem.dataset.labelBlock || 'Block album'; } const blockArtistItem = contextMenu.querySelector('li[data-action="block-artist"]'); if (blockArtistItem) { const artistId = contextTrack.artist?.id || contextTrack.artists?.[0]?.id; const isBlocked = artistId ? contentBlockingSettings.isArtistBlocked(artistId) : false; blockArtistItem.textContent = isBlocked ? blockArtistItem.dataset.labelUnblock || 'Unblock artist' : blockArtistItem.dataset.labelBlock || 'Block artist'; } // Filter items based on type 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'; } if (item.dataset.action === 'request-song') { if (!partyManager.currentParty) { item.style.display = 'none'; } } // Update labels for Like/Save if (item.dataset.action === 'toggle-like') { const labelPrefix = isLiked ? 'labelUnlike' : 'label'; const labelKey = `${labelPrefix}${type.charAt(0).toUpperCase() + type.slice(1).replace('User-playlist', 'Playlist')}`; const fallbackKey = isLiked ? 'labelUnlikeTrack' : 'labelTrack'; const label = item.dataset[labelKey] || item.dataset[fallbackKey] || (isLiked ? 'Unlike' : 'Like'); item.textContent = label; } }); // Handle multiple artists for "Go to artist" const artistItem = contextMenu.querySelector('li[data-action="go-to-artist"]'); if (artistItem) { const artists = Array.isArray(contextTrack.artists) ? contextTrack.artists : contextTrack.artist ? [contextTrack.artist] : []; const canShowArtist = type === 'track' || type === 'album'; if (artists.length > 1 && canShowArtist) { artistItem.style.display = 'block'; artistItem.textContent = 'Go to artists'; artistItem.dataset.hasMultipleArtists = 'true'; } else { const hasArtist = artists.length > 0; artistItem.style.display = hasArtist && canShowArtist ? 'block' : 'none'; artistItem.dataset.hasMultipleArtists = 'false'; artistItem.textContent = artists.length > 1 ? 'Go to artists' : 'Go to artist'; delete artistItem.dataset.artistId; delete artistItem.dataset.trackerSheetId; } } } export function initializeTrackInteractions(player, api, mainContent, contextMenu, lyricsManager, ui, scrobbler) { let contextTrack = null; mainContent.addEventListener('touchstart', handleTrackTouchStart, { passive: true }); mainContent.addEventListener('touchmove', handleTrackTouchMove, { passive: true }); mainContent.addEventListener('touchend', handleTrackTouchEnd, { passive: true }); mainContent.addEventListener('click', async (e) => { const actionBtn = e.target.closest('.track-action-btn, .like-btn, .play-btn'); if (actionBtn && actionBtn.dataset.action) { e.preventDefault(); // Prevent card navigation e.stopPropagation(); const itemElement = actionBtn.closest('.track-item, .card'); const action = actionBtn.dataset.action; const type = actionBtn.dataset.type || 'track'; let item = itemElement ? trackDataStore.get(itemElement) : trackDataStore.get(actionBtn); // If no item from element (e.g. header buttons), try to get from hash if (!item && action === 'toggle-like') { const id = window.location.pathname.split('/')[2]; if (id) { try { if (type === 'album') { const data = await api.getAlbum(id); item = data.album; } else if (type === 'artist') { item = await api.getArtist(id); } else if (type === 'playlist') { const data = await api.getPlaylist(id); item = data.playlist; } else if (type === 'mix') { const data = await api.getMix(id); item = data.mix; } else if (type === 'track') { const data = await api.getTrack(id); item = data.track; } } catch (err) { console.error(err); } } } if (item) { await handleTrackAction(action, item, player, api, lyricsManager, type, ui, scrobbler); } return; } const cardMenuBtn = e.target.closest('.card-menu-btn, #album-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) { // Check if item is stored on the button itself (e.g., album page header menu) item = trackDataStore.get(cardMenuBtn); } if (!item) { // Fallback: create a shell item 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; 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(); const trackItem = menuBtn.closest('.track-item'); if (trackItem && !trackItem.dataset.queueIndex) { const clickedTrack = trackDataStore.get(trackItem); if (clickedTrack && clickedTrack.isLocal) return; if ( contextMenu.style.display === 'block' && contextTrack && 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 = menuBtn.dataset.type || trackItem.dataset.type || 'track'; if (trackSelection.isSelecting && trackSelection.selectedIds.size > 0) { const selectedTracks = []; document.querySelectorAll('.track-item.selected').forEach((item) => { const track = trackDataStore.get(item); if (track) selectedTracks.push(track); }); contextMenu._selectedTracks = selectedTracks; } await updateContextMenuLikeState(contextMenu, contextTrack); const rect = menuBtn.getBoundingClientRect(); positionMenu(contextMenu, rect.left, rect.bottom + 5, rect); } } return; } const checkbox = e.target.closest('.track-checkbox'); if (checkbox) { e.stopPropagation(); const trackItem = checkbox.closest('.track-item'); if (trackItem) { toggleTrackSelection(trackItem, isMultiSelectToggle(e), isMultiSelectRange(e)); } return; } const trackItem = e.target.closest('.track-item'); if (trackItem && trackItem.classList.contains('unavailable')) { return; } if (isLongPress && longPressTrackItem === trackItem) { return; } if ( trackItem && !trackItem.classList.contains('blocked') && !trackItem.dataset.queueIndex && !e.target.closest('.remove-from-playlist-btn') && !e.target.closest('.artist-link') && !e.target.closest('.like-btn') ) { const clickedTrackId = trackItem.dataset.trackId; const isSearch = window.location.pathname.startsWith('/search/'); if (isMultiSelectToggle(e)) { e.preventDefault(); toggleTrackSelection(trackItem, true, isMultiSelectRange(e)); return; } if (isMultiSelectRange(e) && trackSelection.isSelecting) { e.preventDefault(); toggleTrackSelection(trackItem, false, true); return; } if (trackSelection.isSelecting) { return; } if (isSearch) { const clickedTrack = trackDataStore.get(trackItem); if (clickedTrack) { if (trackItem.dataset.type === 'video') { player.playVideo(clickedTrack); } else { player.setQueue([clickedTrack], 0); document.getElementById('shuffle-btn').classList.remove('active'); player.playTrackFromQueue(); api.getTrackRecommendations(clickedTrack.id).then((recs) => { if (recs && recs.length > 0) { player.addToQueue(recs); } }); } } } else { const parentList = trackItem.closest('.track-list'); const allTrackElements = Array.from(parentList.querySelectorAll('.track-item')); const trackList = allTrackElements.map((el) => trackDataStore.get(el)).filter(Boolean); if (trackList.length > 0) { const startIndex = trackList.findIndex((t) => t.id == clickedTrackId); player.setQueue(trackList, startIndex); // Set artist popular tracks context if on artist page console.log('[Events] Setting context:', { page: ui.currentPage, artistId: ui.currentArtistId, trackCount: trackList.length, }); if (ui.currentPage === 'artist' && ui.currentArtistId) { player.setArtistPopularTracksContext(ui.currentArtistId, trackList, trackList.length, true); } document.getElementById('shuffle-btn').classList.remove('active'); player.playTrackFromQueue(); } } } // 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) { if (e.target.closest('.edit-playlist-btn') || e.target.closest('.delete-playlist-btn')) { return; } const libraryTracksContainer = card.closest('#library-tracks-container'); if (libraryTracksContainer && card.dataset.trackId) { if (card.classList.contains('blocked')) return; if ( e.target.closest('.like-btn') || e.target.closest('.card-play-btn') || e.target.closest('.card-menu-btn') ) { return; } e.preventDefault(); const clickedTrackId = card.dataset.trackId; const clickedTrack = trackDataStore.get(card); if (!clickedTrack) return; const allTrackElements = Array.from(libraryTracksContainer.querySelectorAll('.card[data-track-id]')); const trackList = allTrackElements.map((el) => trackDataStore.get(el)).filter(Boolean); if (trackList.length === 0) return; const startIndex = trackList.findIndex((t) => t.id == clickedTrackId); player.setQueue(trackList, startIndex); if (ui.currentPage === 'artist' && ui.currentArtistId) { player.setArtistPopularTracksContext(ui.currentArtistId, trackList, trackList.length, true); } document.getElementById('shuffle-btn').classList.remove('active'); player.playTrackFromQueue(); return; } const href = card.dataset.href; if (href) { // Allow native links inside card to work if any exist if (e.target.closest('a')) return; e.preventDefault(); navigate(href); } } }); 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')) { // For queue items, get track from player's queue const queueIndex = parseInt(trackItem.dataset.queueIndex); contextTrack = player.getCurrentQueue()[queueIndex]; } else { // For regular track items contextTrack = trackDataStore.get(trackItem); } if (contextTrack) { if (contextTrack.isLocal) return; if (contextMenu._originalHTML) { contextMenu.innerHTML = contextMenu._originalHTML; contextMenu._originalHTML = null; } // Store selected tracks for multi-select actions let selectedTracks = []; if (trackSelection.isSelecting && trackSelection.selectedIds.size > 0) { document.querySelectorAll('.track-item.selected').forEach((item) => { const track = trackDataStore.get(item); if (track) selectedTracks.push(track); }); } // Hide actions for unavailable tracks const unavailableActions = ['play-next', 'add-to-queue', 'download', 'track-mix']; contextMenu.querySelectorAll('[data-action]').forEach((btn) => { if (unavailableActions.includes(btn.dataset.action)) { btn.style.display = contextTrack.isUnavailable ? 'none' : 'block'; } }); contextMenu._contextTrack = contextTrack; contextMenu._contextType = contextTrack.type || 'track'; contextMenu._selectedTracks = selectedTracks; await updateContextMenuLikeState(contextMenu, contextTrack); positionMenu(contextMenu, e.clientX, e.clientY); } } 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, }; if (contextMenu._originalHTML) { contextMenu.innerHTML = contextMenu._originalHTML; contextMenu._originalHTML = null; } contextTrack = item; contextMenu._contextTrack = item; contextMenu._contextType = type.replace('userplaylist', 'user-playlist'); contextMenu._contextHref = card.dataset.href; await updateContextMenuLikeState(contextMenu, item); positionMenu(contextMenu, e.clientX, e.clientY); } }); document.querySelector('.now-playing-bar')?.addEventListener('contextmenu', async (e) => { if (!player.currentTrack) return; const track = player.currentTrack; if (track.isLocal) return; const target = e.target.closest('.cover, .title, .album, .artist'); if (!target) return; e.preventDefault(); e.stopPropagation(); if (contextMenu._originalHTML) { contextMenu.innerHTML = contextMenu._originalHTML; contextMenu._originalHTML = null; } contextTrack = track; contextMenu._contextTrack = track; contextMenu._contextType = track.type || 'track'; contextMenu._selectedTracks = []; const unavailableActions = ['play-next', 'add-to-queue', 'download', 'track-mix']; contextMenu.querySelectorAll('[data-action]').forEach((btn) => { if (unavailableActions.includes(btn.dataset.action)) { btn.style.display = track.isUnavailable ? 'none' : 'block'; } }); await updateContextMenuLikeState(contextMenu, track); positionMenu(contextMenu, e.clientX, e.clientY); }); document.addEventListener('click', async (e) => { if (contextMenu.style.display === 'block') { if (contextMenu._originalHTML) { contextMenu.innerHTML = contextMenu._originalHTML; } contextMenu.style.display = 'none'; contextMenu._contextType = null; contextMenu._originalHTML = null; } if ( trackSelection.isSelecting && !e.target.closest('.track-item') && !e.target.closest('.selection-bar') && !e.target.closest('.track-checkbox') ) { clearSelection(); } }); document.addEventListener('keydown', async (e) => { if (e.key === 'Escape' && trackSelection.isSelecting) { clearSelection(); } }); contextMenu.addEventListener('click', async (e) => { e.stopPropagation(); const target = e.target.closest('[data-action]'); if (!target) return; 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.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) { const selectedTracks = contextMenu._selectedTracks || []; const isMultiSelect = selectedTracks.length > 1; if (isMultiSelect) { // Handle multi-select actions switch (action) { case 'play-next': selectedTracks.forEach((t) => { trackPlayNext(t); player.addNextToQueue(t); }); if (window.renderQueueFunction) await window.renderQueueFunction(); showNotification(`Playing next: ${selectedTracks.length} tracks`); clearSelection(); break; case 'add-to-queue': player.addToQueue(selectedTracks); if (window.renderQueueFunction) await window.renderQueueFunction(); showNotification(`Added ${selectedTracks.length} tracks to queue`); clearSelection(); break; case 'toggle-like': selectedTracks.forEach(async (t) => { const added = await db.toggleFavorite('track', t); await syncManager.syncLibraryItem('track', t, added); }); showNotification(`Liked ${selectedTracks.length} tracks`); clearSelection(); break; case 'add-to-playlist': await showMultiSelectPlaylistModal(selectedTracks); clearSelection(); break; case 'download': showNotification(`Downloading ${selectedTracks.length} tracks`); clearSelection(); for (const track of selectedTracks) { await downloadTrackWithMetadata( track, downloadQualitySettings.getQuality(), api, lyricsManager ); } break; default: clearSelection(); break; } } else { // 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; contextMenu._selectedTracks = null; }); // Now playing bar interactions document.querySelector('.now-playing-bar .title').addEventListener('click', () => { const track = player.currentTrack; if (track?.album?.id) { navigate(`/album/${track.album.id}`); } }); document.querySelector('.now-playing-bar .album').addEventListener('click', () => { const track = player.currentTrack; if (track?.album?.id) { navigate(`/album/${track.album.id}`); } }); document.querySelector('.now-playing-bar .artist').addEventListener('click', (e) => { const link = e.target.closest('.artist-link'); if (link) { e.stopPropagation(); const artistId = link.dataset.artistId; const trackerSheetId = link.dataset.trackerSheetId; if (trackerSheetId) { // Navigate to tracker artist page navigate(`/unreleased/${trackerSheetId}`); } else if (artistId) { navigate(`/artist/${artistId}`); } return; } // Fallback for non-link clicks (e.g. separators) or single artist legacy const track = player.currentTrack; if (track) { // Check if this is a tracker track const isTracker = track.isTracker || (track.id && String(track.id).startsWith('tracker-')); if (isTracker && track.trackerInfo?.sheetId) { navigate(`/unreleased/${track.trackerInfo.sheetId}`); } else if (track.artist?.id) { navigate(`/artist/${track.artist.id}`); } } }); const nowPlayingLikeBtn = document.getElementById('now-playing-like-btn'); if (nowPlayingLikeBtn) { nowPlayingLikeBtn.addEventListener('click', async (e) => { e.stopPropagation(); if (player.currentTrack) { await handleTrackAction( 'toggle-like', player.currentTrack, player, api, lyricsManager, player.currentTrack.type || 'track', ui, scrobbler ); } }); } const nowPlayingMixBtn = document.getElementById('now-playing-mix-btn'); if (nowPlayingMixBtn) { nowPlayingMixBtn.addEventListener('click', async (e) => { e.stopPropagation(); if (player.currentTrack) { await handleTrackAction( 'track-mix', player.currentTrack, player, api, lyricsManager, 'track', ui, scrobbler ); } }); } const nowPlayingAddPlaylistBtn = document.getElementById('now-playing-add-playlist-btn'); if (nowPlayingAddPlaylistBtn) { nowPlayingAddPlaylistBtn.addEventListener('click', async (e) => { e.stopPropagation(); if (player.currentTrack) { await handleTrackAction( 'add-to-playlist', player.currentTrack, player, api, lyricsManager, player.currentTrack.type || 'track', ui, scrobbler ); } }); } // Mobile add playlist button functionality const mobileAddPlaylistBtn = document.getElementById('mobile-add-playlist-btn'); if (mobileAddPlaylistBtn) { mobileAddPlaylistBtn.addEventListener('click', async (e) => { e.stopPropagation(); if (player.currentTrack) { await handleTrackAction( 'add-to-playlist', player.currentTrack, player, api, lyricsManager, player.currentTrack.type || 'track', ui, scrobbler ); } }); } } function showSleepTimerModal(player) { const modal = document.getElementById('sleep-timer-modal'); if (!modal) return; const closeModal = () => { modal.classList.remove('active'); cleanup(); }; const handleOptionClick = (e) => { const timerOption = e.target.closest('.timer-option'); if (timerOption) { let minutes; if (timerOption.id === 'custom-timer-btn') { const customInput = document.getElementById('custom-minutes'); minutes = parseInt(customInput.value); if (!minutes || minutes < 1) { showNotification('Please enter a valid number of minutes'); return; } } else { minutes = parseInt(timerOption.dataset.minutes); } if (minutes) { player.setSleepTimer(minutes); trackSetSleepTimer(minutes); showNotification(`Sleep timer set for ${minutes} minute${minutes === 1 ? '' : 's'}`); closeModal(); } } }; const handleCancel = (e) => { if (e.target.id === 'cancel-sleep-timer' || e.target.classList.contains('modal-overlay')) { closeModal(); } }; const cleanup = () => { modal.removeEventListener('click', handleOptionClick); modal.removeEventListener('click', handleCancel); }; modal.addEventListener('click', handleOptionClick); modal.addEventListener('click', handleCancel); modal.classList.add('active'); }