//js/app.js console.log('[App] Script loaded'); import { LosslessAPI } from './api.js'; import { apiSettings, themeManager, nowPlayingSettings, downloadQualitySettings, sidebarSettings, pwaUpdateSettings, } 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 } from './utils.js'; import { sidePanelManager } from './side-panel.js'; import { db } from './db.js'; import { syncManager } from './accounts/pocketbase.js'; import { registerSW } from 'virtual:pwa-register'; import { initializeDiscordRPC } from './discord-rpc.js'; import * as Neutralino from '@neutralinojs/lib'; import './smooth-scrolling.js'; // Assign Neutralino to window for global access if (typeof window !== 'undefined' && window.NL_MODE) { window.Neutralino = Neutralino; } import { initTracker } from './tracker.js'; // 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) { document.addEventListener('keydown', (e) => { if (e.target.matches('input, textarea')) return; switch (e.key.toLowerCase()) { case ' ': e.preventDefault(); player.handlePlayPause(); break; case 'arrowright': if (e.shiftKey) { player.playNext(); } else { audioPlayer.currentTime = Math.min(audioPlayer.duration, audioPlayer.currentTime + 10); } break; case 'arrowleft': if (e.shiftKey) { player.playPrev(); } else { audioPlayer.currentTime = Math.max(0, audioPlayer.currentTime - 10); } break; case 'arrowup': e.preventDefault(); player.setVolume(player.userVolume + 0.1); break; case 'arrowdown': e.preventDefault(); player.setVolume(player.userVolume - 0.1); break; case 'm': audioPlayer.muted = !audioPlayer.muted; break; case 's': document.getElementById('shuffle-btn')?.click(); break; case 'r': document.getElementById('repeat-btn')?.click(); break; case 'q': document.getElementById('queue-btn')?.click(); break; case '/': e.preventDefault(); document.getElementById('search-input')?.focus(); break; case 'escape': document.getElementById('search-input')?.blur(); sidePanelManager.close(); clearLyricsPanelSync(audioPlayer, sidePanelManager.panel); break; case 'l': document.querySelector('.now-playing-bar .cover')?.click(); break; } }); } 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); } } } document.addEventListener('DOMContentLoaded', async () => { // Initialize desktop environment (Neutralino) const urlParams = new URLSearchParams(window.location.search); const hasNLParams = urlParams.has('NL_PORT') || urlParams.has('NL_TOKEN'); const isDesktop = typeof window !== 'undefined' && (window.NL_MODE || window.location.port === '5050' || hasNLParams); if (typeof window !== 'undefined') { const nlGlobals = Object.keys(window).filter((k) => k.startsWith('NL_')); console.log('[App] Environment Check:', { isDesktop, port: window.location.port, hasNLParams, nlGlobals, }); } if (typeof window !== 'undefined' && window.Neutralino) { if (isDesktop) { console.log('[App] Initializing Neutralino desktop environment...'); try { Neutralino.init(); console.log('[App] Neutralino.init() called successfully.'); // Register events immediately Neutralino.events.on('windowClose', () => { console.log('[App] Window close event triggered.'); Neutralino.app.exit(); }); } catch (error) { console.error('[App] Failed to initialize desktop environment:', error); } } else { console.log('[App] Skipping Neutralino.init() on regular web environment.'); } } else { console.log('[App] Neutralino object NOT detected.'); } const api = new LosslessAPI(apiSettings); const audioPlayer = document.getElementById('audio-player'); // i love ios and macos!!!! webkit fucking SUCKS BULLSHIT sorry ios/macos heads yall getting lossless only const ua = navigator.userAgent.toLowerCase(); const isIOS = /iphone|ipad|ipod/.test(ua) || (ua.includes('mac') && navigator.maxTouchPoints > 1); 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); const ui = new UIRenderer(api, player); const scrobbler = new MultiScrobbler(); const lyricsManager = new LyricsManager(api); const originalRenderPlaylistPage = ui.renderPlaylistPage.bind(ui); ui.renderPlaylistPage = async function (id, type) { await originalRenderPlaylistPage(id, type); if (type === 'user') { try { const playlist = await db.getPlaylist(id); const imgElement = document.getElementById('playlist-detail-image'); if (!imgElement) return; let container = imgElement.parentElement; let collageElement = document.getElementById('playlist-detail-collage'); if (!container.classList.contains('detail-header-cover-container')) { container = document.createElement('div'); container.className = 'detail-header-cover-container'; imgElement.parentNode.insertBefore(container, imgElement); container.appendChild(imgElement); collageElement = document.createElement('div'); collageElement.id = 'playlist-detail-collage'; collageElement.className = 'detail-header-collage'; collageElement.style.display = 'none'; container.appendChild(collageElement); } if (playlist && !playlist.cover && collageElement && playlist.tracks && playlist.tracks.length > 0) { const tracksWithCovers = playlist.tracks.filter((t) => t.album && t.album.cover); if (tracksWithCovers.length > 0) { imgElement.style.setProperty('display', 'none', 'important'); collageElement.style.display = 'grid'; collageElement.innerHTML = ''; const uniqueCovers = []; const seen = new Set(); for (const t of tracksWithCovers) { if (!seen.has(t.album.cover)) { seen.add(t.album.cover); uniqueCovers.push(t.album.cover); if (uniqueCovers.length >= 4) break; } } const images = []; for (let i = 0; i < 4; i++) { images.push(uniqueCovers[i % uniqueCovers.length]); } images.forEach((src) => { const img = document.createElement('img'); img.src = api.getCoverUrl(src); collageElement.appendChild(img); }); } else { imgElement.style.removeProperty('display'); collageElement.style.display = 'none'; } } else if (collageElement) { imgElement.style.removeProperty('display'); collageElement.style.display = 'none'; } } catch (e) { console.error('Error generating playlist cover:', e); } } }; // 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; if (!isChromeOrEdge || !hasFileSystemApi) { selectLocalBtn.style.display = 'none'; browserWarning.style.display = 'block'; } } // 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(); // Load settings module and initialize const { initializeSettings } = await loadSettingsModule(); initializeSettings(scrobbler, player, api, ui); 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); // Initialize tracker initTracker(player); if (typeof window !== 'undefined' && window.Neutralino && isDesktop) { console.log('[App] Starting Discord RPC...'); initializeDiscordRPC(player); } const castBtn = document.getElementById('cast-btn'); initializeCasting(audioPlayer, castBtn); // Restore UI state for the current track (like button, theme) if (player.currentTrack) { ui.setCurrentTrack(player.currentTrack); } document.querySelector('.now-playing-bar .cover').addEventListener('click', async () => { if (!player.currentTrack) { alert('No track is currently playing'); return; } const mode = nowPlayingSettings.getMode(); 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', () => { if (window.location.hash === '#fullscreen') { window.history.back(); } else { ui.closeFullscreenCover(); } }); document.getElementById('fullscreen-cover-image')?.addEventListener('click', () => { 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); }); 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'); const albumId = albumIndex !== -1 ? pathParts[albumIndex + 1] : null; 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'); const albumId = albumIndex !== -1 ? pathParts[albumIndex + 1] : null; 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; document.getElementById('play-artist-radio-btn')?.click(); } 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')) { 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-description-input').value = ''; modal.dataset.editingId = ''; document.getElementById('csv-import-section').style.display = 'block'; document.getElementById('csv-file-input').value = ''; // 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'; modal.classList.add('active'); document.getElementById('playlist-name-input').focus(); } if (e.target.closest('#create-folder-btn')) { 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) { await db.createFolder(name, cover); ui.renderLibraryPage(); document.getElementById('folder-modal').classList.remove('active'); } } 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); navigate('/library'); } } if (e.target.closest('#playlist-modal-save')) { const name = document.getElementById('playlist-name-input').value.trim(); const 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'); let tracks = []; if (csvFileInput.files.length > 0) { // Import from CSV const file = csvFileInput.files[0]; 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'); try { // Show progress bar 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 totalTracks = Math.max(0, lines.length - 1); 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 || ''; }); tracks = result.tracks; const missingTracks = result.missingTracks; 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`); // if theres missing songs, warn the user 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 { // Hide progress bar setTimeout(() => { progressElement.style.display = 'none'; }, 1000); } } const cover = document.getElementById('playlist-cover-input').value.trim(); // 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'); ui.renderLibraryPage(); modal.classList.remove('active'); }); } } } 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 = `${window.location.origin}/userplaylist/${playlist.id}`; navigator.clipboard.writeText(url).then(() => alert('Link copied to clipboard!')); }; } modal.dataset.editingId = playlistId; document.getElementById('csv-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 = `${window.location.origin}/userplaylist/${playlist.id}`; navigator.clipboard.writeText(url).then(() => alert('Link copied to clipboard!')); }; } modal.dataset.editingId = playlistId; document.getElementById('csv-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; // Prefer ID if available (from sorted view) if (btn.dataset.trackId) { trackId = btn.dataset.trackId; } 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; } } if (trackId) { const updatedPlaylist = await db.removeTrackFromPlaylist(playlistId, trackId); 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.