//js/app.js import { MusicAPI } from './music-api.js'; import { apiSettings, themeManager, nowPlayingSettings, fullscreenCoverClickSettings, downloadQualitySettings, sidebarSettings, pwaUpdateSettings, modalSettings, keyboardShortcuts, } from './storage.js'; import { UIRenderer } from './ui.js'; import { Player } from './player.js'; import { MultiScrobbler } from './multi-scrobbler.js'; import { LyricsManager, openLyricsPanel, clearLyricsPanelSync } from './lyrics.js'; import { createRouter, updateTabTitle, navigate } from './router.js'; import { initializePlayerEvents, initializeTrackInteractions, handleTrackAction } from './events.js'; import { initializeUIInteractions } from './ui-interactions.js'; import { debounce, SVG_PLAY, getShareUrl } from './utils.js'; import { sidePanelManager } from './side-panel.js'; import { db } from './db.js'; import { syncManager } from './accounts/pocketbase.js'; import { authManager } from './accounts/auth.js'; import { registerSW } from 'virtual:pwa-register'; import './smooth-scrolling.js'; import { openEditProfile } from './profile.js'; import { ThemeStore } from './themeStore.js'; import './commandPalette.js'; import { initTracker } from './tracker.js'; import { initAnalytics, trackSidebarNavigation, trackCreatePlaylist, trackCreateFolder, trackImportJSPF, trackImportCSV, trackImportXSPF, trackImportXML, trackImportM3U, trackSelectLocalFolder, trackChangeLocalFolder, trackOpenModal, trackCloseModal, trackKeyboardShortcut, trackPwaUpdate, trackDismissUpdate, trackOpenFullscreenCover, trackCloseFullscreenCover, trackOpenLyrics, trackCloseLyrics, } from './analytics.js'; import { parseCSV, parseJSPF, parseXSPF, parseXML, parseM3U, parseDynamicCSV, importToLibrary, } from './playlist-importer.js'; // Capture real iOS state before spoofing (needed for background audio) if (typeof window !== 'undefined') { const _ua = navigator.userAgent.toLowerCase(); window.__IS_IOS__ = /iphone|ipad|ipod/.test(_ua) || (_ua.includes('mac') && navigator.maxTouchPoints > 1); // Spoof User-Agent to bypass Google's embedded browser check Object.defineProperty(navigator, 'userAgent', { get: function () { return 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'; }, }); // analytics const plausibleScript = document.createElement('script'); plausibleScript.async = true; plausibleScript.src = 'https://plausible.canine.tools/js/pa-dCMvQpiD1-AJmi8o3xviO.js'; document.head.appendChild(plausibleScript); window.plausible = window.plausible || function () { (window.plausible.q = window.plausible.q || []).push(arguments); }; window.plausible.init = window.plausible.init || function (i) { window.plausible.o = i || {}; }; window.plausible.init(); } // Lazy-loaded modules let settingsModule = null; let downloadsModule = null; let metadataModule = null; async function loadSettingsModule() { if (!settingsModule) { settingsModule = await import('./settings.js'); } return settingsModule; } async function loadDownloadsModule() { if (!downloadsModule) { downloadsModule = await import('./downloads.js'); } return downloadsModule; } async function loadMetadataModule() { if (!metadataModule) { metadataModule = await import('./metadata.js'); } return metadataModule; } function initializeCasting(audioPlayer, castBtn) { if (!castBtn) return; if ('remote' in audioPlayer) { audioPlayer.remote .watchAvailability((available) => { if (available) { castBtn.style.display = 'flex'; castBtn.classList.add('available'); } }) .catch((err) => { console.log('Remote playback not available:', err); if (window.innerWidth > 768) { castBtn.style.display = 'flex'; } }); castBtn.addEventListener('click', () => { if (!audioPlayer.src) { alert('Please play a track first to enable casting.'); return; } audioPlayer.remote.prompt().catch((err) => { if (err.name === 'NotAllowedError') return; if (err.name === 'NotFoundError') { alert('No remote playback devices (Chromecast/AirPlay) were found on your network.'); return; } console.log('Cast prompt error:', err); }); }); audioPlayer.addEventListener('playing', () => { if (audioPlayer.remote && audioPlayer.remote.state === 'connected') { castBtn.classList.add('connected'); } }); audioPlayer.addEventListener('pause', () => { if (audioPlayer.remote && audioPlayer.remote.state === 'disconnected') { castBtn.classList.remove('connected'); } }); } else if (audioPlayer.webkitShowPlaybackTargetPicker) { castBtn.style.display = 'flex'; castBtn.classList.add('available'); castBtn.addEventListener('click', () => { audioPlayer.webkitShowPlaybackTargetPicker(); }); audioPlayer.addEventListener('webkitplaybacktargetavailabilitychanged', (e) => { if (e.availability === 'available') { castBtn.classList.add('available'); } }); audioPlayer.addEventListener('webkitcurrentplaybacktargetiswirelesschanged', () => { if (audioPlayer.webkitCurrentPlaybackTargetIsWireless) { castBtn.classList.add('connected'); } else { castBtn.classList.remove('connected'); } }); } else if (window.innerWidth > 768) { castBtn.style.display = 'flex'; castBtn.addEventListener('click', () => { alert('Casting is not supported in this browser. Try Chrome for Chromecast or Safari for AirPlay.'); }); } } function initializeKeyboardShortcuts(player, audioPlayer) { const keyActionMap = { playPause: () => { trackKeyboardShortcut('Space'); player.handlePlayPause(); }, seekForward: () => { trackKeyboardShortcut('Right'); audioPlayer.currentTime = Math.min(audioPlayer.duration, audioPlayer.currentTime + 10); }, seekBackward: () => { trackKeyboardShortcut('Left'); audioPlayer.currentTime = Math.max(0, audioPlayer.currentTime - 10); }, nextTrack: () => { trackKeyboardShortcut('Shift+Right'); player.playNext(); }, previousTrack: () => { trackKeyboardShortcut('Shift+Left'); player.playPrev(); }, volumeUp: () => { trackKeyboardShortcut('Up'); player.setVolume(player.userVolume + 0.1); }, volumeDown: () => { trackKeyboardShortcut('Down'); player.setVolume(player.userVolume - 0.1); }, mute: () => { trackKeyboardShortcut('M'); audioPlayer.muted = !audioPlayer.muted; }, shuffle: () => { trackKeyboardShortcut('S'); document.getElementById('shuffle-btn')?.click(); }, repeat: () => { trackKeyboardShortcut('R'); document.getElementById('repeat-btn')?.click(); }, queue: () => { trackKeyboardShortcut('Q'); document.getElementById('queue-btn')?.click(); }, lyrics: () => { trackKeyboardShortcut('L'); document.querySelector('.now-playing-bar .cover')?.click(); }, search: () => { trackKeyboardShortcut('/'); document.getElementById('search-input')?.focus(); }, escape: () => { trackKeyboardShortcut('Escape'); document.getElementById('search-input')?.blur(); sidePanelManager.close(); clearLyricsPanelSync(audioPlayer, sidePanelManager.panel); }, visualizerNext: () => { trackKeyboardShortcut('VisualizerNext'); const ui = window.monochromeUi; if (ui?.visualizer?.presets?.['butterchurn']) { ui.visualizer.presets['butterchurn'].nextPreset(); } }, visualizerPrev: () => { trackKeyboardShortcut('VisualizerPrev'); const ui = window.monochromeUi; if (ui?.visualizer?.presets?.['butterchurn']) { ui.visualizer.presets['butterchurn'].prevPreset(); } }, visualizerCycle: () => { trackKeyboardShortcut('VisualizerCycle'); const ui = window.monochromeUi; if (ui?.visualizer?.presets?.['butterchurn']) { ui.visualizer.presets['butterchurn'].toggleCycle(); } }, }; document.addEventListener('keydown', (e) => { if (e.target.matches('input, textarea, [contenteditable="true"]')) return; const shortcuts = keyboardShortcuts.getShortcuts(); const pressedKey = e.key.toLowerCase(); const hasShift = e.shiftKey; const hasCtrl = e.ctrlKey || e.metaKey; const hasAlt = e.altKey; for (const [action, shortcut] of Object.entries(shortcuts)) { if (!shortcut?.key) continue; const shortcutKey = shortcut.key.toLowerCase(); const matches = pressedKey === shortcutKey && shortcut.shift === hasShift && shortcut.ctrl === hasCtrl && shortcut.alt === hasAlt; if (matches) { e.preventDefault(); const actionFn = keyActionMap[action]; if (actionFn) { actionFn(); } return; } } }); } function showOfflineNotification() { const notification = document.createElement('div'); notification.className = 'offline-notification'; notification.innerHTML = ` You are offline. Some features may not work. `; document.body.appendChild(notification); setTimeout(() => { notification.style.animation = 'slide-out 0.3s ease forwards'; setTimeout(() => notification.remove(), 300); }, 5000); } function hideOfflineNotification() { const notification = document.querySelector('.offline-notification'); if (notification) { notification.style.animation = 'slide-out 0.3s ease forwards'; setTimeout(() => notification.remove(), 300); } } async function disablePwaForAuthGate() { if (!('serviceWorker' in navigator)) return; try { const registrations = await navigator.serviceWorker.getRegistrations(); await Promise.all(registrations.map((registration) => registration.unregister())); } catch (error) { console.warn('Failed to unregister service workers:', error); } if ('caches' in window) { try { const cacheKeys = await caches.keys(); await Promise.all(cacheKeys.map((key) => caches.delete(key))); } catch (error) { console.warn('Failed to clear caches:', error); } } } async function uploadCoverImage(file) { try { const formData = new FormData(); formData.append('file', file); const response = await fetch('/upload', { method: 'POST', body: formData, }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || `Upload failed: ${response.status}`); } const data = await response.json(); return data.url; } catch (error) { console.error('Cover upload error:', error); throw error; } } document.addEventListener('DOMContentLoaded', async () => { // Initialize analytics initAnalytics(); new ThemeStore(); const api = new MusicAPI(apiSettings); const audioPlayer = document.getElementById('audio-player'); // i love ios and macos!!!! webkit fucking SUCKS BULLSHIT sorry ios/macos heads yall getting lossless only // Use window.__IS_IOS__ (set before UA spoof in index.html) so detection works on real iOS. const isIOS = typeof window !== 'undefined' && window.__IS_IOS__ === true; const ua = navigator.userAgent.toLowerCase(); const isSafari = ua.includes('safari') && !ua.includes('chrome') && !ua.includes('crios') && !ua.includes('android'); if (isIOS || isSafari) { const qualitySelect = document.getElementById('streaming-quality-setting'); const downloadSelect = document.getElementById('download-quality-setting'); const removeHiRes = (select) => { if (!select) return; const option = select.querySelector('option[value="HI_RES_LOSSLESS"]'); if (option) option.remove(); }; removeHiRes(qualitySelect); removeHiRes(downloadSelect); const currentQualitySetting = localStorage.getItem('playback-quality'); if (!currentQualitySetting || currentQualitySetting === 'HI_RES_LOSSLESS') { localStorage.setItem('playback-quality', 'LOSSLESS'); } } const currentQuality = localStorage.getItem('playback-quality') || 'HI_RES_LOSSLESS'; const player = new Player(audioPlayer, api, currentQuality); window.monochromePlayer = player; // Initialize tracker initTracker(player); // Linux Media Keys Fix if (window.NL_MODE) { import('./desktop/neutralino-bridge.js').then(({ events }) => { events.on('mediaNext', () => player.playNext()); events.on('mediaPrevious', () => player.playPrev()); events.on('mediaPlayPause', () => player.handlePlayPause()); events.on('mediaStop', () => { player.audio.pause(); player.audio.currentTime = 0; }); console.log('Media keys initialized via bridge'); }); } // Initialize desktop features if in Neutralino mode if ( typeof window !== 'undefined' && (window.NL_MODE || window.location.search.includes('mode=neutralino') || window.location.search.includes('nl_port=')) ) { window.NL_MODE = true; try { const desktopModule = await import('./desktop/desktop.js'); await desktopModule.initDesktop(player); import('./desktop/neutralino-bridge.js').then(({ updater }) => { setTimeout(async () => { try { // my worker should detect a users OS and serve the right ver const update = await updater.checkForUpdates('https://update.samidy.xyz/update.json'); if (update && update.available) { const modal = document.getElementById('desktop-update-modal'); const notes = document.getElementById('desktop-update-notes'); const confirmBtn = document.getElementById('desktop-update-confirm'); const cancelBtn = document.getElementById('desktop-update-cancel'); if (modal) { notes.innerHTML = update.notes || 'Bug fixes and improvements.'; modal.classList.add('active'); confirmBtn.onclick = async () => { confirmBtn.disabled = true; confirmBtn.textContent = 'Updating...'; try { await updater.install(); } catch (err) { console.error(err); confirmBtn.textContent = 'Failed'; setTimeout(() => { confirmBtn.disabled = false; confirmBtn.textContent = 'Update Now'; }, 2000); } }; cancelBtn.onclick = () => modal.classList.remove('active'); } } } catch (e) { console.warn('Failed to check for desktop updates:', e); } }, 3000); }); } catch (err) { console.error('Failed to load desktop module:', err); } } const castBtn = document.getElementById('cast-btn'); initializeCasting(audioPlayer, castBtn); const ui = new UIRenderer(api, player); window.monochromeUi = ui; const scrobbler = new MultiScrobbler(); window.monochromeScrobbler = scrobbler; const lyricsManager = new LyricsManager(api); ui.lyricsManager = lyricsManager; // Check browser support for local files const selectLocalBtn = document.getElementById('select-local-folder-btn'); const browserWarning = document.getElementById('local-browser-warning'); if (selectLocalBtn && browserWarning) { const ua = navigator.userAgent; const isChromeOrEdge = (ua.indexOf('Chrome') > -1 || ua.indexOf('Edg') > -1) && !/Mobile|Android/.test(ua); const hasFileSystemApi = 'showDirectoryPicker' in window; const isNeutralino = window.NL_MODE || window.location.search.includes('mode=neutralino') || window.location.search.includes('nl_port='); if (!isNeutralino && (!isChromeOrEdge || !hasFileSystemApi)) { selectLocalBtn.style.display = 'none'; browserWarning.style.display = 'block'; } else if (isNeutralino) { selectLocalBtn.style.display = 'flex'; browserWarning.style.display = 'none'; } } // Kuroshiro is now loaded on-demand only when needed for Asian text with Romaji mode enabled const currentTheme = themeManager.getTheme(); themeManager.setTheme(currentTheme); // Restore sidebar state sidebarSettings.restoreState(); // Render pinned items await ui.renderPinnedItems(); // Load settings module and initialize const { initializeSettings } = await loadSettingsModule(); initializeSettings(scrobbler, player, api, ui); // Track sidebar navigation clicks document.querySelectorAll('.sidebar-nav a').forEach((link) => { link.addEventListener('click', () => { const href = link.getAttribute('href'); if (href && !href.startsWith('http')) { const item = link.querySelector('span')?.textContent || href; trackSidebarNavigation(item); } }); }); initializePlayerEvents(player, audioPlayer, scrobbler, ui); initializeTrackInteractions( player, api, document.querySelector('.main-content'), document.getElementById('context-menu'), lyricsManager, ui, scrobbler ); initializeUIInteractions(player, api, ui); initializeKeyboardShortcuts(player, audioPlayer); // Restore UI state for the current track (like button, theme) if (player.currentTrack) { ui.setCurrentTrack(player.currentTrack); } document.querySelector('.now-playing-bar').addEventListener('click', async (e) => { if (!e.target.closest('.cover')) return; if (!player.currentTrack) { alert('No track is currently playing'); return; } const mode = nowPlayingSettings.getMode(); if (mode === 'lyrics') { const isActive = sidePanelManager.isActive('lyrics'); if (isActive) { trackCloseLyrics(player.currentTrack); } else { trackOpenLyrics(player.currentTrack); } } else if (mode === 'cover') { const overlay = document.getElementById('fullscreen-cover-overlay'); if (overlay && overlay.style.display === 'flex') { trackCloseFullscreenCover(); } else { trackOpenFullscreenCover(player.currentTrack); } } if (mode === 'lyrics') { const isActive = sidePanelManager.isActive('lyrics'); if (isActive) { sidePanelManager.close(); clearLyricsPanelSync(audioPlayer, sidePanelManager.panel); } else { openLyricsPanel(player.currentTrack, audioPlayer, lyricsManager); } } else if (mode === 'cover') { const overlay = document.getElementById('fullscreen-cover-overlay'); if (overlay && overlay.style.display === 'flex') { if (window.location.hash === '#fullscreen') { window.history.back(); } else { ui.closeFullscreenCover(); } } else { const nextTrack = player.getNextTrack(); ui.showFullscreenCover(player.currentTrack, nextTrack, lyricsManager, audioPlayer); } } else { // Default to 'album' mode - navigate to album if (player.currentTrack.album?.id) { navigate(`/album/${player.currentTrack.album.id}`); } } }); // Toggle Share Button visibility on switch change document.getElementById('playlist-public-toggle')?.addEventListener('change', (e) => { const shareBtn = document.getElementById('playlist-share-btn'); if (shareBtn) shareBtn.style.display = e.target.checked ? 'flex' : 'none'; }); document.getElementById('close-fullscreen-cover-btn')?.addEventListener('click', () => { trackCloseFullscreenCover(); if (window.location.hash === '#fullscreen') { window.history.back(); } else { ui.closeFullscreenCover(); } }); document.getElementById('fullscreen-cover-overlay')?.addEventListener('click', (e) => { const coverImage = document.getElementById('fullscreen-cover-image'); if (!coverImage) return; const isOnCoverImage = e.target.closest('#fullscreen-cover-image') || e.target.id === 'fullscreen-cover-image'; if (!isOnCoverImage) return; const action = fullscreenCoverClickSettings.getAction(); const overlay = document.getElementById('fullscreen-cover-overlay'); const playerInstance = window.monochromePlayer; switch (action) { case 'exit': if (window.location.hash === '#fullscreen') { window.history.back(); } else { ui.closeFullscreenCover(); } break; case 'hide-ui': if (overlay) { const isCurrentlyHidden = overlay.classList.contains('ui-hidden'); if (isCurrentlyHidden) { overlay.classList.remove('ui-hidden'); const toggleBtn = document.getElementById('toggle-ui-btn'); if (toggleBtn) { toggleBtn.classList.remove('active'); toggleBtn.classList.add('visible'); toggleBtn.title = 'Hide UI'; } } else { overlay.classList.add('ui-hidden'); const toggleBtn = document.getElementById('toggle-ui-btn'); if (toggleBtn) { toggleBtn.classList.add('active'); toggleBtn.classList.remove('visible'); toggleBtn.title = 'Show UI'; } } if (ui && typeof ui.setupUIToggleButton === 'function') { if (ui.uiToggleCleanup) { ui.uiToggleCleanup(); } ui.setupUIToggleButton(overlay); } } break; case 'pause-resume': if (playerInstance) playerInstance.handlePlayPause(); break; case 'next': if (playerInstance) playerInstance.playNext(); break; case 'previous': if (playerInstance) playerInstance.playPrev(); break; case 'nothing': break; default: if (window.location.hash === '#fullscreen') { window.history.back(); } else { ui.closeFullscreenCover(); } } }); document.getElementById('sidebar-toggle')?.addEventListener('click', () => { document.body.classList.toggle('sidebar-collapsed'); const isCollapsed = document.body.classList.contains('sidebar-collapsed'); const toggleBtn = document.getElementById('sidebar-toggle'); if (toggleBtn) { toggleBtn.innerHTML = isCollapsed ? '' : ''; } // Save sidebar state to localStorage sidebarSettings.setCollapsed(isCollapsed); }); // Import tab switching in playlist modal document.querySelectorAll('.import-tab').forEach((tab) => { tab.addEventListener('click', () => { const importType = tab.dataset.importType; // Update tab styles document.querySelectorAll('.import-tab').forEach((t) => { t.classList.remove('active'); t.style.opacity = '0.7'; }); tab.classList.add('active'); tab.style.opacity = '1'; // Show/hide panels document.getElementById('csv-import-panel').style.display = importType === 'csv' ? 'block' : 'none'; document.getElementById('jspf-import-panel').style.display = importType === 'jspf' ? 'block' : 'none'; document.getElementById('xspf-import-panel').style.display = importType === 'xspf' ? 'block' : 'none'; document.getElementById('xml-import-panel').style.display = importType === 'xml' ? 'block' : 'none'; document.getElementById('m3u-import-panel').style.display = importType === 'm3u' ? 'block' : 'none'; // Clear all file inputs except the active one document.getElementById('csv-file-input').value = importType === 'csv' ? document.getElementById('csv-file-input').value : ''; document.getElementById('jspf-file-input').value = importType === 'jspf' ? document.getElementById('jspf-file-input').value : ''; document.getElementById('xspf-file-input').value = importType === 'xspf' ? document.getElementById('xspf-file-input').value : ''; document.getElementById('xml-file-input').value = importType === 'xml' ? document.getElementById('xml-file-input').value : ''; document.getElementById('m3u-file-input').value = importType === 'm3u' ? document.getElementById('m3u-file-input').value : ''; }); }); const spotifyBtn = document.getElementById('csv-spotify-btn'); const appleBtn = document.getElementById('csv-apple-btn'); const ytmBtn = document.getElementById('csv-ytm-btn'); const spotifyGuide = document.getElementById('csv-spotify-guide'); const appleGuide = document.getElementById('csv-apple-guide'); const ytmGuide = document.getElementById('csv-ytm-guide'); const inputContainer = document.getElementById('csv-input-container'); if (spotifyBtn && appleBtn && ytmBtn) { spotifyBtn.addEventListener('click', () => { spotifyBtn.classList.remove('btn-secondary'); spotifyBtn.classList.add('btn-primary'); spotifyBtn.style.opacity = '1'; appleBtn.classList.remove('btn-primary'); appleBtn.classList.add('btn-secondary'); appleBtn.style.opacity = '0.7'; ytmBtn.classList.remove('btn-primary'); ytmBtn.classList.add('btn-secondary'); ytmBtn.style.opacity = '0.7'; spotifyGuide.style.display = 'block'; appleGuide.style.display = 'none'; ytmGuide.style.display = 'none'; inputContainer.style.display = 'block'; }); appleBtn.addEventListener('click', () => { appleBtn.classList.remove('btn-secondary'); appleBtn.classList.add('btn-primary'); appleBtn.style.opacity = '1'; spotifyBtn.classList.remove('btn-primary'); spotifyBtn.classList.add('btn-secondary'); spotifyBtn.style.opacity = '0.7'; ytmBtn.classList.remove('btn-primary'); ytmBtn.classList.add('btn-secondary'); ytmBtn.style.opacity = '0.7'; appleGuide.style.display = 'block'; spotifyGuide.style.display = 'none'; ytmGuide.style.display = 'none'; inputContainer.style.display = 'block'; }); ytmBtn.addEventListener('click', () => { ytmBtn.classList.remove('btn-secondary'); ytmBtn.classList.add('btn-primary'); ytmBtn.style.opacity = '1'; spotifyBtn.classList.remove('btn-primary'); spotifyBtn.classList.add('btn-secondary'); spotifyBtn.style.opacity = '0.7'; appleBtn.classList.remove('btn-primary'); appleBtn.classList.add('btn-secondary'); appleBtn.style.opacity = '0.7'; ytmGuide.style.display = 'block'; spotifyGuide.style.display = 'none'; appleGuide.style.display = 'none'; inputContainer.style.display = 'none'; }); } // Cover image upload functionality const coverUploadBtn = document.getElementById('playlist-cover-upload-btn'); const coverFileInput = document.getElementById('playlist-cover-file-input'); const coverToggleUrlBtn = document.getElementById('playlist-cover-toggle-url-btn'); const coverUrlInput = document.getElementById('playlist-cover-input'); const coverUploadStatus = document.getElementById('playlist-cover-upload-status'); const coverUploadText = document.getElementById('playlist-cover-upload-text'); let useUrlInput = false; coverUploadBtn?.addEventListener('click', () => { if (useUrlInput) return; coverFileInput?.click(); }); coverFileInput?.addEventListener('change', async (e) => { const file = e.target.files?.[0]; if (!file) return; // Validate file type if (!file.type.startsWith('image/')) { alert('Please select an image file'); return; } // Show uploading status coverUploadStatus.style.display = 'block'; coverUploadText.textContent = 'Uploading...'; coverUploadBtn.disabled = true; try { const publicUrl = await uploadCoverImage(file); coverUrlInput.value = publicUrl; coverUploadText.textContent = 'Done!'; coverUploadText.style.color = 'var(--success)'; setTimeout(() => { coverUploadStatus.style.display = 'none'; }, 2000); } catch (error) { coverUploadText.textContent = 'Failed - try URL'; coverUploadText.style.color = 'var(--error)'; console.error('Upload failed:', error); } finally { coverUploadBtn.disabled = false; } }); coverToggleUrlBtn?.addEventListener('click', () => { useUrlInput = !useUrlInput; if (useUrlInput) { coverUploadBtn.style.flex = '0 0 auto'; coverUploadBtn.style.display = 'none'; coverUrlInput.style.display = 'block'; coverToggleUrlBtn.textContent = 'Upload'; coverToggleUrlBtn.title = 'Switch to file upload'; } else { coverUploadBtn.style.flex = '1'; coverUploadBtn.style.display = 'flex'; coverUrlInput.style.display = 'none'; coverToggleUrlBtn.textContent = 'or URL'; coverToggleUrlBtn.title = 'Switch to URL input'; } }); document.getElementById('nav-back')?.addEventListener('click', () => { window.history.back(); }); document.getElementById('nav-forward')?.addEventListener('click', () => { window.history.forward(); }); document.getElementById('toggle-lyrics-btn')?.addEventListener('click', async (e) => { e.stopPropagation(); if (!player.currentTrack) { alert('No track is currently playing'); return; } const isActive = sidePanelManager.isActive('lyrics'); if (isActive) { sidePanelManager.close(); clearLyricsPanelSync(audioPlayer, sidePanelManager.panel); } else { openLyricsPanel(player.currentTrack, audioPlayer, lyricsManager); } }); document.getElementById('download-current-btn')?.addEventListener('click', () => { if (player.currentTrack) { handleTrackAction('download', player.currentTrack, player, api, lyricsManager, 'track', ui); } }); // Auto-update lyrics when track changes let previousTrackId = null; audioPlayer.addEventListener('play', async () => { if (!player.currentTrack) return; // Update UI with current track info for theme ui.setCurrentTrack(player.currentTrack); // Update Media Session with new track player.updateMediaSession(player.currentTrack); const currentTrackId = player.currentTrack.id; if (currentTrackId === previousTrackId) return; previousTrackId = currentTrackId; // Update lyrics panel if it's open if (sidePanelManager.isActive('lyrics')) { // Re-open forces update/refresh of content and sync openLyricsPanel(player.currentTrack, audioPlayer, lyricsManager, true); } // Update Fullscreen if it's open const fullscreenOverlay = document.getElementById('fullscreen-cover-overlay'); if (fullscreenOverlay && getComputedStyle(fullscreenOverlay).display !== 'none') { const nextTrack = player.getNextTrack(); ui.showFullscreenCover(player.currentTrack, nextTrack, lyricsManager, audioPlayer); } // DEV: Auto-open fullscreen mode if ?fullscreen=1 in URL const urlParams = new URLSearchParams(window.location.search); if ( urlParams.get('fullscreen') === '1' && fullscreenOverlay && getComputedStyle(fullscreenOverlay).display === 'none' ) { const nextTrack = player.getNextTrack(); ui.showFullscreenCover(player.currentTrack, nextTrack, lyricsManager, audioPlayer); } }); document.addEventListener('click', async (e) => { if (e.target.closest('#play-album-btn')) { const btn = e.target.closest('#play-album-btn'); if (btn.disabled) return; const pathParts = window.location.pathname.split('/'); const albumIndex = pathParts.indexOf('album'); let albumId = albumIndex !== -1 ? pathParts[albumIndex + 1] : null; // Handle /album/t/ID format if (albumId === 't') { albumId = pathParts[albumIndex + 2]; } if (!albumId) return; try { const { tracks } = await api.getAlbum(albumId); if (tracks && tracks.length > 0) { // Sort tracks by disc and track number for consistent playback const sortedTracks = [...tracks].sort((a, b) => { const discA = a.volumeNumber ?? a.discNumber ?? 1; const discB = b.volumeNumber ?? b.discNumber ?? 1; if (discA !== discB) return discA - discB; return a.trackNumber - b.trackNumber; }); player.setQueue(sortedTracks, 0); const shuffleBtn = document.getElementById('shuffle-btn'); if (shuffleBtn) shuffleBtn.classList.remove('active'); player.shuffleActive = false; player.playTrackFromQueue(); } } catch (error) { console.error('Failed to play album:', error); const { showNotification } = await loadDownloadsModule(); showNotification('Failed to play album'); } } if (e.target.closest('#shuffle-album-btn')) { const btn = e.target.closest('#shuffle-album-btn'); if (btn.disabled) return; const pathParts = window.location.pathname.split('/'); const albumIndex = pathParts.indexOf('album'); let albumId = albumIndex !== -1 ? pathParts[albumIndex + 1] : null; // Handle /album/t/ID format if (albumId === 't') { albumId = pathParts[albumIndex + 2]; } if (!albumId) return; try { const { tracks } = await api.getAlbum(albumId); if (tracks && tracks.length > 0) { const shuffledTracks = [...tracks].sort(() => Math.random() - 0.5); player.setQueue(shuffledTracks, 0); const shuffleBtn = document.getElementById('shuffle-btn'); if (shuffleBtn) shuffleBtn.classList.remove('active'); player.shuffleActive = false; player.playTrackFromQueue(); const { showNotification } = await loadDownloadsModule(); showNotification('Shuffling album'); } } catch (error) { console.error('Failed to shuffle album:', error); const { showNotification } = await loadDownloadsModule(); showNotification('Failed to shuffle album'); } } if (e.target.closest('#shuffle-artist-btn')) { const btn = e.target.closest('#shuffle-artist-btn'); if (btn.disabled) return; const artistId = window.location.pathname.split('/')[2]; if (!artistId) return; btn.disabled = true; const originalHTML = btn.innerHTML; btn.innerHTML = 'Shuffling...'; try { const artist = await api.getArtist(artistId); const allReleases = [...(artist.albums || []), ...(artist.eps || [])]; const trackSet = new Set(); const allTracks = []; // Fetch full artist discography tracks (albums + EPs), deduped by track ID. const chunkSize = 8; for (let i = 0; i < allReleases.length; i += chunkSize) { const chunk = allReleases.slice(i, i + chunkSize); await Promise.all( chunk.map(async (album) => { try { const { tracks } = await api.getAlbum(album.id); tracks.forEach((track) => { if (!trackSet.has(track.id)) { trackSet.add(track.id); allTracks.push(track); } }); } catch (err) { console.warn(`Failed to fetch tracks for album ${album.title}:`, err); } }) ); } // Fallback to artist top tracks if discography fetch yields nothing. if (allTracks.length === 0 && Array.isArray(artist.tracks)) { artist.tracks.forEach((track) => { if (!trackSet.has(track.id)) { trackSet.add(track.id); allTracks.push(track); } }); } if (allTracks.length === 0) { throw new Error('No tracks found for this artist'); } const shuffledTracks = [...allTracks].sort(() => Math.random() - 0.5); player.setQueue(shuffledTracks, 0); const shuffleBtn = document.getElementById('shuffle-btn'); if (shuffleBtn) shuffleBtn.classList.remove('active'); player.shuffleActive = false; player.playTrackFromQueue(); const { showNotification } = await loadDownloadsModule(); showNotification('Shuffling artist discography'); } catch (error) { console.error('Failed to shuffle artist tracks:', error); const { showNotification } = await loadDownloadsModule(); showNotification('Failed to shuffle artist tracks'); } finally { if (document.body.contains(btn)) { btn.disabled = false; btn.innerHTML = originalHTML; } } } if (e.target.closest('#download-mix-btn')) { const btn = e.target.closest('#download-mix-btn'); if (btn.disabled) return; const mixId = window.location.pathname.split('/')[2]; if (!mixId) return; btn.disabled = true; const originalHTML = btn.innerHTML; btn.innerHTML = 'Downloading...'; try { const { mix, tracks } = await api.getMix(mixId); const { downloadPlaylistAsZip } = await loadDownloadsModule(); await downloadPlaylistAsZip(mix, tracks, api, downloadQualitySettings.getQuality(), lyricsManager); } catch (error) { console.error('Mix download failed:', error); alert('Failed to download mix: ' + error.message); } finally { btn.disabled = false; btn.innerHTML = originalHTML; } } if (e.target.closest('#download-playlist-btn')) { const btn = e.target.closest('#download-playlist-btn'); if (btn.disabled) return; const playlistId = window.location.pathname.split('/')[2]; if (!playlistId) return; btn.disabled = true; const originalHTML = btn.innerHTML; btn.innerHTML = 'Downloading...'; try { let playlist, tracks; let userPlaylist = await db.getPlaylist(playlistId); if (!userPlaylist) { try { userPlaylist = await syncManager.getPublicPlaylist(playlistId); } catch { // Not a public playlist } } if (userPlaylist) { playlist = { ...userPlaylist, title: userPlaylist.name || userPlaylist.title }; tracks = userPlaylist.tracks || []; } else { const data = await api.getPlaylist(playlistId); playlist = data.playlist; tracks = data.tracks; } const { downloadPlaylistAsZip } = await loadDownloadsModule(); await downloadPlaylistAsZip(playlist, tracks, api, downloadQualitySettings.getQuality(), lyricsManager); } catch (error) { console.error('Playlist download failed:', error); alert('Failed to download playlist: ' + error.message); } finally { btn.disabled = false; btn.innerHTML = originalHTML; } } if (e.target.closest('#create-playlist-btn')) { trackOpenModal('Create Playlist'); const modal = 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 = ''; modal.dataset.editingId = ''; document.getElementById('import-section').style.display = 'block'; document.getElementById('csv-file-input').value = ''; document.getElementById('ytm-url-input').value = ''; document.getElementById('ytm-status').textContent = ''; document.getElementById('jspf-file-input').value = ''; document.getElementById('xspf-file-input').value = ''; document.getElementById('xml-file-input').value = ''; document.getElementById('m3u-file-input').value = ''; // Reset import tabs to CSV document.querySelectorAll('.import-tab').forEach((tab) => { tab.classList.toggle('active', tab.dataset.importType === 'csv'); }); document.getElementById('csv-import-panel').style.display = 'block'; document.getElementById('jspf-import-panel').style.display = 'none'; document.getElementById('xspf-import-panel').style.display = 'none'; document.getElementById('xml-import-panel').style.display = 'none'; document.getElementById('m3u-import-panel').style.display = 'none'; // Reset Public Toggle const publicToggle = document.getElementById('playlist-public-toggle'); const shareBtn = document.getElementById('playlist-share-btn'); if (publicToggle) publicToggle.checked = false; if (shareBtn) shareBtn.style.display = 'none'; // Reset cover upload state const coverUploadBtn = document.getElementById('playlist-cover-upload-btn'); const coverUrlInput = document.getElementById('playlist-cover-input'); const coverUploadStatus = document.getElementById('playlist-cover-upload-status'); 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 (coverUploadStatus) coverUploadStatus.style.display = 'none'; if (coverToggleUrlBtn) { coverToggleUrlBtn.textContent = 'or URL'; coverToggleUrlBtn.title = 'Switch to URL input'; } modal.classList.add('active'); document.getElementById('playlist-name-input').focus(); } if (e.target.closest('#create-folder-btn')) { trackOpenModal('Create Folder'); const modal = document.getElementById('folder-modal'); document.getElementById('folder-name-input').value = ''; document.getElementById('folder-cover-input').value = ''; modal.classList.add('active'); document.getElementById('folder-name-input').focus(); } if (e.target.closest('#folder-modal-save')) { const name = document.getElementById('folder-name-input').value.trim(); const cover = document.getElementById('folder-cover-input').value.trim(); if (name) { const folder = await db.createFolder(name, cover); trackCreateFolder(folder); await syncManager.syncUserFolder(folder, 'create'); ui.renderLibraryPage(); document.getElementById('folder-modal').classList.remove('active'); trackCloseModal('Create Folder'); } } if (e.target.closest('#folder-modal-cancel')) { document.getElementById('folder-modal').classList.remove('active'); } if (e.target.closest('#delete-folder-btn')) { const folderId = window.location.pathname.split('/')[2]; if (folderId && confirm('Are you sure you want to delete this folder?')) { await db.deleteFolder(folderId); // Sync deletion to cloud await syncManager.syncUserFolder({ id: folderId }, 'delete'); navigate('/library'); } } if (e.target.closest('#playlist-modal-save')) { let name = document.getElementById('playlist-name-input').value.trim(); let description = document.getElementById('playlist-description-input').value.trim(); const isPublic = document.getElementById('playlist-public-toggle')?.checked; if (name) { const modal = document.getElementById('playlist-modal'); const editingId = modal.dataset.editingId; const handlePublicStatus = async (playlist) => { playlist.isPublic = isPublic; if (isPublic) { try { await syncManager.publishPlaylist(playlist); } catch (e) { console.error('Failed to publish playlist:', e); alert('Failed to publish playlist. Please ensure you are logged in.'); } } else { try { await syncManager.unpublishPlaylist(playlist.id); } catch { // Ignore error if it wasn't public } } return playlist; }; if (editingId) { // Edit const cover = document.getElementById('playlist-cover-input').value.trim(); db.getPlaylist(editingId).then(async (playlist) => { if (playlist) { playlist.name = name; playlist.cover = cover; playlist.description = description; await handlePublicStatus(playlist); await db.performTransaction('user_playlists', 'readwrite', (store) => store.put(playlist)); syncManager.syncUserPlaylist(playlist, 'update'); ui.renderLibraryPage(); // Also update current page if we are on it if (window.location.pathname === `/userplaylist/${editingId}`) { ui.renderPlaylistPage(editingId, 'user'); } modal.classList.remove('active'); delete modal.dataset.editingId; } }); } else { // Create const csvFileInput = document.getElementById('csv-file-input'); const jspfFileInput = document.getElementById('jspf-file-input'); const xspfFileInput = document.getElementById('xspf-file-input'); const xmlFileInput = document.getElementById('xml-file-input'); const m3uFileInput = document.getElementById('m3u-file-input'); const importOptions = { strictArtistMatch: true, albumMatch: true }; let tracks = []; let importSource = 'manual'; let cover = document.getElementById('playlist-cover-input').value.trim(); // Helper function for import progress const setupProgressElements = () => { const progressElement = document.getElementById('csv-import-progress'); const progressFill = document.getElementById('csv-progress-fill'); const progressCurrent = document.getElementById('csv-progress-current'); const progressTotal = document.getElementById('csv-progress-total'); const currentTrackElement = progressElement.querySelector('.current-track'); const currentArtistElement = progressElement.querySelector('.current-artist'); return { progressElement, progressFill, progressCurrent, progressTotal, currentTrackElement, currentArtistElement, }; }; const isYTMActive = document.getElementById('csv-ytm-btn')?.classList.contains('btn-primary'); const ytmUrlInput = document.getElementById('ytm-url-input'); if (isYTMActive && ytmUrlInput.value.trim()) { importSource = 'ytm_import'; const url = ytmUrlInput.value.trim(); const playlistId = url.split('list=')[1]?.split('&')[0]; const workerUrl = `https://ytmimport.samidy.workers.dev?playlistId=${playlistId}`; if (!playlistId) { alert("Invalid URL. Make sure it has 'list=' in it."); return; } const { progressElement, progressFill, progressCurrent, progressTotal, currentTrackElement, currentArtistElement, } = setupProgressElements(); try { progressElement.style.display = 'block'; progressFill.style.width = '0%'; progressCurrent.textContent = '0'; currentTrackElement.textContent = 'Fetching from YouTube...'; if (currentArtistElement) currentArtistElement.textContent = ''; const response = await fetch(workerUrl); const songs = await response.json(); if (songs.error) throw new Error(songs.error); currentTrackElement.textContent = `Processing ${songs.length} songs...`; const headers = 'Title,Artist,URL\n'; const csvText = headers + songs .map( (s) => `"${s.title.replace(/"/g, '""')}","${s.artist.replace(/"/g, '""')}","${s.url}"` ) .join('\n'); const totalTracks = songs.length; progressTotal.textContent = totalTracks.toString(); const result = await parseCSV( csvText, api, (progress) => { const percentage = totalTracks > 0 ? (progress.current / totalTracks) * 100 : 0; progressFill.style.width = `${Math.min(percentage, 100)}%`; progressCurrent.textContent = progress.current.toString(); currentTrackElement.textContent = progress.currentTrack; if (currentArtistElement) currentArtistElement.textContent = progress.currentArtist || ''; }, importOptions ); tracks = result.tracks; const missingTracks = result.missingTracks; if (tracks.length === 0) { alert('No valid tracks found in the YouTube playlist!'); progressElement.style.display = 'none'; return; } console.log(`Imported ${tracks.length} tracks from YouTube`); trackImportCSV(name || 'Untitled', tracks.length, missingTracks.length); if (missingTracks.length > 0) { setTimeout(() => { showMissingTracksNotification(missingTracks); }, 500); } } catch (err) { console.error('YTM Import Error:', err); alert(`Error importing from YouTube: ${err.message}`); progressElement.style.display = 'none'; return; } finally { setTimeout(() => { progressElement.style.display = 'none'; }, 1000); } } else if (jspfFileInput.files.length > 0) { // Import from JSPF importSource = 'jspf_import'; const file = jspfFileInput.files[0]; const { progressElement, progressFill, progressCurrent, progressTotal, currentTrackElement, currentArtistElement, } = setupProgressElements(); try { progressElement.style.display = 'block'; progressFill.style.width = '0%'; progressCurrent.textContent = '0'; currentTrackElement.textContent = 'Reading JSPF file...'; if (currentArtistElement) currentArtistElement.textContent = ''; const jspfText = await file.text(); const result = await parseJSPF(jspfText, api, (progress) => { const percentage = progress.total > 0 ? (progress.current / progress.total) * 100 : 0; progressFill.style.width = `${Math.min(percentage, 100)}%`; progressCurrent.textContent = progress.current.toString(); progressTotal.textContent = progress.total.toString(); currentTrackElement.textContent = progress.currentTrack; if (currentArtistElement) currentArtistElement.textContent = progress.currentArtist || ''; }); tracks = result.tracks; const missingTracks = result.missingTracks; if (tracks.length === 0) { alert('No valid tracks found in the JSPF file! Please check the format.'); progressElement.style.display = 'none'; return; } console.log(`Imported ${tracks.length} tracks from JSPF`); // Auto-fill playlist metadata from JSPF if not provided const jspfData = result.jspfData; if (jspfData && jspfData.playlist) { const playlist = jspfData.playlist; if (!name && playlist.title) { name = playlist.title; } if (!description && playlist.annotation) { description = playlist.annotation; } if (!cover && playlist.image) { cover = playlist.image; } } // Track JSPF import const jspfPlaylist = result.jspfData?.playlist; const jspfCreator = jspfPlaylist?.creator || jspfPlaylist?.extension?.['https://musicbrainz.org/doc/jspf#playlist']?.creator || 'unknown'; trackImportJSPF( name || jspfPlaylist?.title || 'Untitled', tracks.length, missingTracks.length, jspfCreator ); if (missingTracks.length > 0) { setTimeout(() => { showMissingTracksNotification(missingTracks); }, 500); } } catch (error) { console.error('Failed to parse JSPF!', error); alert('Failed to parse JSPF file! ' + error.message); progressElement.style.display = 'none'; return; } finally { setTimeout(() => { progressElement.style.display = 'none'; }, 1000); } } else if (csvFileInput.files.length > 0) { const file = csvFileInput.files[0]; const { progressElement, progressFill, progressCurrent, progressTotal, currentTrackElement, currentArtistElement, } = setupProgressElements(); try { progressElement.style.display = 'block'; progressFill.style.width = '0%'; progressCurrent.textContent = '0'; currentTrackElement.textContent = 'Reading CSV file...'; if (currentArtistElement) currentArtistElement.textContent = ''; const csvText = await file.text(); const lines = csvText.trim().split('\n'); const totalItems = Math.max(0, lines.length - 1); progressTotal.textContent = totalItems.toString(); const result = await parseDynamicCSV( csvText, api, (progress) => { const percentage = totalItems > 0 ? (progress.current / totalItems) * 100 : 0; progressFill.style.width = `${Math.min(percentage, 100)}%`; progressCurrent.textContent = progress.current.toString(); currentTrackElement.textContent = progress.currentItem; if (currentArtistElement) { currentArtistElement.textContent = progress.type ? `Importing ${progress.type}...` : ''; } }, importOptions ); const hasMultipleTypes = result.tracks.length > 0 && (result.albums.length > 0 || result.artists.length > 0); if (hasMultipleTypes) { currentTrackElement.textContent = 'Adding to library...'; const importResults = await importToLibrary(result, db, (progress) => { if (progress.action === 'playlist') { currentTrackElement.textContent = `Creating playlist: ${progress.item}`; } else { currentTrackElement.textContent = `Adding ${progress.action}: ${progress.item}`; } }); console.log('Import results:', importResults); const summary = []; if (importResults.tracks.added > 0) summary.push(`${importResults.tracks.added} tracks`); if (importResults.albums.added > 0) summary.push(`${importResults.albums.added} albums`); if (importResults.artists.added > 0) summary.push(`${importResults.artists.added} artists`); if (importResults.playlists.created > 0) summary.push(`${importResults.playlists.created} playlists`); alert( `Imported to library:\n${summary.join(', ')}\n\n${ result.missingItems.length > 0 ? `${result.missingItems.length} items could not be found.` : '' }` ); progressElement.style.display = 'none'; return; } tracks = result.tracks; const missingTracks = result.missingItems.filter((i) => i.type === 'track'); if (tracks.length === 0) { alert('No valid tracks found in the CSV file! Please check the format.'); progressElement.style.display = 'none'; return; } console.log(`Imported ${tracks.length} tracks from CSV`); trackImportCSV(name || 'Untitled', tracks.length, missingTracks.length); if (missingTracks.length > 0) { setTimeout(() => { showMissingTracksNotification(missingTracks); }, 500); } } catch (error) { console.error('Failed to parse CSV!', error); alert('Failed to parse CSV file! ' + error.message); progressElement.style.display = 'none'; return; } finally { setTimeout(() => { progressElement.style.display = 'none'; }, 1000); } } else if (xspfFileInput.files.length > 0) { // Import from XSPF importSource = 'xspf_import'; const file = xspfFileInput.files[0]; const { progressElement, progressFill, progressCurrent, progressTotal, currentTrackElement, currentArtistElement, } = setupProgressElements(); try { progressElement.style.display = 'block'; progressFill.style.width = '0%'; progressCurrent.textContent = '0'; currentTrackElement.textContent = 'Reading XSPF file...'; if (currentArtistElement) currentArtistElement.textContent = ''; const xspfText = await file.text(); const result = await parseXSPF(xspfText, api, (progress) => { const percentage = progress.total > 0 ? (progress.current / progress.total) * 100 : 0; progressFill.style.width = `${Math.min(percentage, 100)}%`; progressCurrent.textContent = progress.current.toString(); progressTotal.textContent = progress.total.toString(); currentTrackElement.textContent = progress.currentTrack; if (currentArtistElement) currentArtistElement.textContent = progress.currentArtist || ''; }); tracks = result.tracks; const missingTracks = result.missingTracks; if (tracks.length === 0) { alert('No valid tracks found in the XSPF file! Please check the format.'); progressElement.style.display = 'none'; return; } console.log(`Imported ${tracks.length} tracks from XSPF`); trackImportXSPF(name || 'Untitled', tracks.length, missingTracks.length); if (missingTracks.length > 0) { setTimeout(() => { showMissingTracksNotification(missingTracks); }, 500); } } catch (error) { console.error('Failed to parse XSPF!', error); alert('Failed to parse XSPF file! ' + error.message); progressElement.style.display = 'none'; return; } finally { setTimeout(() => { progressElement.style.display = 'none'; }, 1000); } } else if (xmlFileInput.files.length > 0) { // Import from XML importSource = 'xml_import'; const file = xmlFileInput.files[0]; const { progressElement, progressFill, progressCurrent, progressTotal, currentTrackElement, currentArtistElement, } = setupProgressElements(); try { progressElement.style.display = 'block'; progressFill.style.width = '0%'; progressCurrent.textContent = '0'; currentTrackElement.textContent = 'Reading XML file...'; if (currentArtistElement) currentArtistElement.textContent = ''; const xmlText = await file.text(); const result = await parseXML(xmlText, api, (progress) => { const percentage = progress.total > 0 ? (progress.current / progress.total) * 100 : 0; progressFill.style.width = `${Math.min(percentage, 100)}%`; progressCurrent.textContent = progress.current.toString(); progressTotal.textContent = progress.total.toString(); currentTrackElement.textContent = progress.currentTrack; if (currentArtistElement) currentArtistElement.textContent = progress.currentArtist || ''; }); tracks = result.tracks; const missingTracks = result.missingTracks; if (tracks.length === 0) { alert('No valid tracks found in the XML file! Please check the format.'); progressElement.style.display = 'none'; return; } console.log(`Imported ${tracks.length} tracks from XML`); trackImportXML(name || 'Untitled', tracks.length, missingTracks.length); if (missingTracks.length > 0) { setTimeout(() => { showMissingTracksNotification(missingTracks); }, 500); } } catch (error) { console.error('Failed to parse XML!', error); alert('Failed to parse XML file! ' + error.message); progressElement.style.display = 'none'; return; } finally { setTimeout(() => { progressElement.style.display = 'none'; }, 1000); } } else if (m3uFileInput.files.length > 0) { // Import from M3U/M3U8 importSource = 'm3u_import'; const file = m3uFileInput.files[0]; const { progressElement, progressFill, progressCurrent, progressTotal, currentTrackElement, currentArtistElement, } = setupProgressElements(); try { progressElement.style.display = 'block'; progressFill.style.width = '0%'; progressCurrent.textContent = '0'; currentTrackElement.textContent = 'Reading M3U file...'; if (currentArtistElement) currentArtistElement.textContent = ''; const m3uText = await file.text(); const result = await parseM3U(m3uText, api, (progress) => { const percentage = progress.total > 0 ? (progress.current / progress.total) * 100 : 0; progressFill.style.width = `${Math.min(percentage, 100)}%`; progressCurrent.textContent = progress.current.toString(); progressTotal.textContent = progress.total.toString(); currentTrackElement.textContent = progress.currentTrack; if (currentArtistElement) currentArtistElement.textContent = progress.currentArtist || ''; }); tracks = result.tracks; const missingTracks = result.missingTracks; if (tracks.length === 0) { alert('No valid tracks found in the M3U file! Please check the format.'); progressElement.style.display = 'none'; return; } console.log(`Imported ${tracks.length} tracks from M3U`); trackImportM3U(name || 'Untitled', tracks.length, missingTracks.length); if (missingTracks.length > 0) { setTimeout(() => { showMissingTracksNotification(missingTracks); }, 500); } } catch (error) { console.error('Failed to parse M3U!', error); alert('Failed to parse M3U file! ' + error.message); progressElement.style.display = 'none'; return; } finally { setTimeout(() => { progressElement.style.display = 'none'; }, 1000); } } // Check for pending tracks (from Add to Playlist -> New Playlist) const modal = document.getElementById('playlist-modal'); if (modal._pendingTracks && Array.isArray(modal._pendingTracks)) { tracks = [...tracks, ...modal._pendingTracks]; delete modal._pendingTracks; // Also clear CSV input if we came from there? No, keep it separate. console.log(`Added ${tracks.length} tracks (including pending)`); } db.createPlaylist(name, tracks, cover, description).then(async (playlist) => { await handlePublicStatus(playlist); // Update DB again with isPublic flag await db.performTransaction('user_playlists', 'readwrite', (store) => store.put(playlist)); await syncManager.syncUserPlaylist(playlist, 'create'); trackCreatePlaylist(playlist, importSource); ui.renderLibraryPage(); modal.classList.remove('active'); trackCloseModal('Create Playlist'); }); } } } if (e.target.closest('#playlist-modal-cancel')) { document.getElementById('playlist-modal').classList.remove('active'); } if (e.target.closest('.edit-playlist-btn')) { const card = e.target.closest('.user-playlist'); const playlistId = card.dataset.userPlaylistId; db.getPlaylist(playlistId).then(async (playlist) => { if (playlist) { const modal = document.getElementById('playlist-modal'); document.getElementById('playlist-modal-title').textContent = 'Edit Playlist'; document.getElementById('playlist-name-input').value = playlist.name; document.getElementById('playlist-cover-input').value = playlist.cover || ''; document.getElementById('playlist-description-input').value = playlist.description || ''; // Set Public Toggle const publicToggle = document.getElementById('playlist-public-toggle'); const shareBtn = document.getElementById('playlist-share-btn'); // Check if actually public in Pocketbase to be sure (async) or trust local flag // We trust local flag for UI speed, but could verify. if (publicToggle) publicToggle.checked = !!playlist.isPublic; if (shareBtn) { shareBtn.style.display = playlist.isPublic ? 'flex' : 'none'; shareBtn.onclick = () => { const url = getShareUrl(`/userplaylist/${playlist.id}`); navigator.clipboard.writeText(url).then(() => alert('Link copied to clipboard!')); }; } // Set cover upload state - show URL input if there's an existing cover 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 (playlist.cover) { if (coverUploadBtn) coverUploadBtn.style.display = 'none'; if (coverUrlInput) coverUrlInput.style.display = 'block'; if (coverToggleUrlBtn) { coverToggleUrlBtn.textContent = 'Upload'; coverToggleUrlBtn.title = 'Switch to file upload'; } } else { 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'; } } modal.dataset.editingId = playlistId; document.getElementById('import-section').style.display = 'none'; modal.classList.add('active'); document.getElementById('playlist-name-input').focus(); } }); } if (e.target.closest('.delete-playlist-btn')) { const card = e.target.closest('.user-playlist'); const playlistId = card.dataset.userPlaylistId; if (confirm('Are you sure you want to delete this playlist?')) { db.deletePlaylist(playlistId).then(() => { syncManager.syncUserPlaylist({ id: playlistId }, 'delete'); ui.renderLibraryPage(); }); } } if (e.target.closest('#edit-playlist-btn')) { const playlistId = window.location.pathname.split('/')[2]; db.getPlaylist(playlistId).then((playlist) => { if (playlist) { const modal = document.getElementById('playlist-modal'); document.getElementById('playlist-modal-title').textContent = 'Edit Playlist'; document.getElementById('playlist-name-input').value = playlist.name; document.getElementById('playlist-cover-input').value = playlist.cover || ''; document.getElementById('playlist-description-input').value = playlist.description || ''; const publicToggle = document.getElementById('playlist-public-toggle'); const shareBtn = document.getElementById('playlist-share-btn'); if (publicToggle) publicToggle.checked = !!playlist.isPublic; if (shareBtn) { shareBtn.style.display = playlist.isPublic ? 'flex' : 'none'; shareBtn.onclick = () => { const url = getShareUrl(`/userplaylist/${playlist.id}`); navigator.clipboard.writeText(url).then(() => alert('Link copied to clipboard!')); }; } // Set cover upload state - show URL input if there's an existing cover 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 (playlist.cover) { if (coverUploadBtn) coverUploadBtn.style.display = 'none'; if (coverUrlInput) coverUrlInput.style.display = 'block'; if (coverToggleUrlBtn) { coverToggleUrlBtn.textContent = 'Upload'; coverToggleUrlBtn.title = 'Switch to file upload'; } } else { 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'; } } modal.dataset.editingId = playlistId; document.getElementById('import-section').style.display = 'none'; modal.classList.add('active'); document.getElementById('playlist-name-input').focus(); } }); } if (e.target.closest('#delete-playlist-btn')) { const playlistId = window.location.pathname.split('/')[2]; if (confirm('Are you sure you want to delete this playlist?')) { db.deletePlaylist(playlistId).then(() => { syncManager.syncUserPlaylist({ id: playlistId }, 'delete'); navigate('/library'); }); } } if (e.target.closest('.remove-from-playlist-btn')) { e.stopPropagation(); const btn = e.target.closest('.remove-from-playlist-btn'); const playlistId = window.location.pathname.split('/')[2]; db.getPlaylist(playlistId).then(async (playlist) => { let trackId = null; let trackType = null; // Prefer ID if available (from sorted view) if (btn.dataset.trackId) { trackId = btn.dataset.trackId; trackType = btn.dataset.type || 'track'; } else if (btn.dataset.trackIndex) { // Fallback to index (legacy/unsorted) const index = parseInt(btn.dataset.trackIndex); if (playlist && playlist.tracks[index]) { trackId = playlist.tracks[index].id; trackType = playlist.tracks[index].type || 'track'; } } if (trackId) { const updatedPlaylist = await db.removeTrackFromPlaylist(playlistId, trackId, trackType); syncManager.syncUserPlaylist(updatedPlaylist, 'update'); const scrollTop = document.querySelector('.main-content').scrollTop; await ui.renderPlaylistPage(playlistId, 'user'); document.querySelector('.main-content').scrollTop = scrollTop; } }); } if (e.target.closest('#play-playlist-btn')) { const btn = e.target.closest('#play-playlist-btn'); if (btn.disabled) return; const playlistId = window.location.pathname.split('/')[2]; if (!playlistId) return; try { let tracks; const userPlaylist = await db.getPlaylist(playlistId); if (userPlaylist) { tracks = userPlaylist.tracks; } else { // Try API, if fail, try Public Pocketbase try { const { tracks: apiTracks } = await api.getPlaylist(playlistId); tracks = apiTracks; } catch (e) { const publicPlaylist = await syncManager.getPublicPlaylist(playlistId); if (publicPlaylist) { tracks = publicPlaylist.tracks; } else { throw e; } } } if (tracks.length > 0) { player.setQueue(tracks, 0); document.getElementById('shuffle-btn').classList.remove('active'); player.playTrackFromQueue(); } } catch (error) { console.error('Failed to play playlist:', error); alert('Failed to play playlist: ' + error.message); } } if (e.target.closest('#download-album-btn')) { const btn = e.target.closest('#download-album-btn'); if (btn.disabled) return; const albumId = window.location.pathname.split('/')[2]; if (!albumId) return; btn.disabled = true; const originalHTML = btn.innerHTML; btn.innerHTML = 'Downloading...'; try { const { album, tracks } = await api.getAlbum(albumId); const { downloadAlbumAsZip } = await loadDownloadsModule(); await downloadAlbumAsZip(album, tracks, api, downloadQualitySettings.getQuality(), lyricsManager); } catch (error) { console.error('Album download failed:', error); alert('Failed to download album: ' + error.message); } finally { btn.disabled = false; btn.innerHTML = originalHTML; } } if (e.target.closest('#add-album-to-playlist-btn')) { const btn = e.target.closest('#add-album-to-playlist-btn'); if (btn.disabled) return; const albumId = window.location.pathname.split('/')[2]; if (!albumId) return; try { const { tracks } = await api.getAlbum(albumId); if (!tracks || tracks.length === 0) { const { showNotification } = await loadDownloadsModule(); showNotification('No tracks found in this album.'); return; } 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 playlists = await db.getPlaylists(false); list.innerHTML = `
A new version of Monochrome is available.