//js/app.js import { LosslessAPI } from './api.js'; import { apiSettings, themeManager, nowPlayingSettings, trackListSettings } from './storage.js'; import { UIRenderer } from './ui.js'; import { Player } from './player.js'; import { LastFMScrobbler } from './lastfm.js'; import { LyricsManager, openLyricsPanel, clearLyricsPanelSync, renderLyricsInFullscreen, clearFullscreenLyricsSync } from './lyrics.js'; import { createRouter, updateTabTitle } from './router.js'; import { initializeSettings } from './settings.js'; import { initializePlayerEvents, initializeTrackInteractions, handleTrackAction } from './events.js'; import { initializeUIInteractions } from './ui-interactions.js'; import { downloadAlbumAsZip, downloadDiscography, downloadPlaylistAsZip } from './downloads.js'; import { debounce, SVG_PLAY } from './utils.js'; import { sidePanelManager } from './side-panel.js'; import { db } from './db.js'; import { syncManager } from './firebase/sync.js'; import { registerSW } from 'virtual:pwa-register'; 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(); audioPlayer.volume = Math.min(1, audioPlayer.volume + 0.1); break; case 'arrowdown': e.preventDefault(); audioPlayer.volume = Math.max(0, audioPlayer.volume - 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 = 'slideOut 0.3s ease forwards'; setTimeout(() => notification.remove(), 300); }, 5000); } function hideOfflineNotification() { const notification = document.querySelector('.offline-notification'); if (notification) { notification.style.animation = 'slideOut 0.3s ease forwards'; setTimeout(() => notification.remove(), 300); } } document.addEventListener('DOMContentLoaded', async () => { const api = new LosslessAPI(apiSettings); const audioPlayer = document.getElementById('audio-player'); const currentQuality = localStorage.getItem('playback-quality') || 'LOSSLESS'; const player = new Player(audioPlayer, api, currentQuality); const ui = new UIRenderer(api, player); const scrobbler = new LastFMScrobbler(); const lyricsManager = new LyricsManager(api); // Pre-load Kuroshiro for romaji conversion in background (always load so it's ready instantly) lyricsManager.loadKuroshiro().catch(err => { console.warn('Failed to pre-load Kuroshiro:', err); }); const currentTheme = themeManager.getTheme(); themeManager.setTheme(currentTheme); trackListSettings.getMode(); 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); 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 .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') { 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) { window.location.hash = `#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', () => { ui.closeFullscreenCover(); }); document.getElementById('fullscreen-cover-image')?.addEventListener('click', () => { ui.closeFullscreenCover(); }); 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); 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); } // Update Fullscreen/Enlarged Cover 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); } }); 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 albumId = window.location.hash.split('/')[1]; if (!albumId) return; try { const { tracks } = await api.getAlbum(albumId); if (tracks.length > 0) { player.setQueue(tracks, 0); document.getElementById('shuffle-btn').classList.remove('active'); player.playTrackFromQueue(); } } catch (error) { console.error('Failed to play album:', error); alert('Failed to play album: ' + error.message); } } if (e.target.closest('#download-mix-btn')) { const btn = e.target.closest('#download-mix-btn'); if (btn.disabled) return; const mixId = window.location.hash.split('#mix/')[1]; if (!mixId) return; btn.disabled = true; const originalHTML = btn.innerHTML; btn.innerHTML = 'Downloading...'; try { const { mix, tracks } = await api.getMix(mixId); await downloadPlaylistAsZip(mix, tracks, api, player.quality, 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.hash.split('/')[1]; if (!playlistId) return; btn.disabled = true; const originalHTML = btn.innerHTML; btn.innerHTML = 'Downloading...'; try { let playlist, tracks; const userPlaylist = await db.getPlaylist(playlistId); if (userPlaylist) { playlist = { ...userPlaylist, title: userPlaylist.name }; tracks = userPlaylist.tracks || []; } else { const data = await api.getPlaylist(playlistId); playlist = data.playlist; tracks = data.tracks; } await downloadPlaylistAsZip(playlist, tracks, api, player.quality, 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 = ''; 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.style.display = 'flex'; document.getElementById('playlist-name-input').focus(); } if (e.target.closest('#playlist-modal-save')) { const name = document.getElementById('playlist-name-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 (e) { // Ignore error if it wasn't public } } return playlist; }; if (editingId) { // Edit db.getPlaylist(editingId).then(async (playlist) => { if (playlist) { playlist.name = name; 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.hash === `#userplaylist/${editingId}`) { ui.renderPlaylistPage(editingId, 'user'); } modal.style.display = 'none'; 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'); try { // Show progress bar progressElement.style.display = 'block'; progressFill.style.width = '0%'; progressCurrent.textContent = '0'; currentTrackElement.textContent = 'Reading CSV file...'; 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; }); 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); } } db.createPlaylist(name, tracks, '').then(async playlist => { await handlePublicStatus(playlist); // Update DB again with isPublic flag await db.performTransaction('user_playlists', 'readwrite', (store) => store.put(playlist)); syncManager.syncUserPlaylist(playlist, 'create'); ui.renderLibraryPage(); modal.style.display = 'none'; }); } } } if (e.target.closest('#playlist-modal-cancel')) { document.getElementById('playlist-modal').style.display = 'none'; } 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; // Set Public Toggle const publicToggle = document.getElementById('playlist-public-toggle'); const shareBtn = document.getElementById('playlist-share-btn'); // Check if actually public in Firebase 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}${window.location.pathname}#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.style.display = 'flex'; 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.hash.split('/')[1]; 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; 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}${window.location.pathname}#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.style.display = 'flex'; document.getElementById('playlist-name-input').focus(); } }); } if (e.target.closest('#delete-playlist-btn')) { const playlistId = window.location.hash.split('/')[1]; if (confirm('Are you sure you want to delete this playlist?')) { db.deletePlaylist(playlistId).then(() => { window.location.hash = '#library'; }); } } if (e.target.closest('.remove-from-playlist-btn')) { e.stopPropagation(); const btn = e.target.closest('.remove-from-playlist-btn'); const index = parseInt(btn.dataset.trackIndex); const playlistId = window.location.hash.split('/')[1]; db.getPlaylist(playlistId).then(async (playlist) => { if (playlist && playlist.tracks[index]) { const trackId = playlist.tracks[index].id; const updatedPlaylist = await db.removeTrackFromPlaylist(playlistId, trackId); syncManager.syncUserPlaylist(updatedPlaylist, 'update'); ui.renderPlaylistPage(playlistId, 'user'); } }); } if (e.target.closest('#play-playlist-btn')) { const btn = e.target.closest('#play-playlist-btn'); if (btn.disabled) return; const playlistId = window.location.hash.split('/')[1]; if (!playlistId) return; try { let tracks; const userPlaylist = await db.getPlaylist(playlistId); if (userPlaylist) { tracks = userPlaylist.tracks; } else { // Try API, if fail, try Public Firebase 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.hash.split('/')[1]; if (!albumId) return; btn.disabled = true; const originalHTML = btn.innerHTML; btn.innerHTML = 'Downloading...'; try { const { album, tracks } = await api.getAlbum(albumId); await downloadAlbumAsZip(album, tracks, api, player.quality, 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('#play-artist-radio-btn')) { const btn = e.target.closest('#play-artist-radio-btn'); if (btn.disabled) return; const artistId = window.location.hash.split('/')[1]; if (!artistId) return; btn.disabled = true; const originalHTML = btn.innerHTML; btn.innerHTML = 'Loading...'; try { const artist = await api.getArtist(artistId); const allReleases = [...(artist.albums || []), ...(artist.eps || [])]; if (allReleases.length === 0) { throw new Error("No albums or EPs found for this artist"); } const trackSet = new Set(); const allTracks = []; const chunks = []; const chunkSize = 3; const albums = allReleases; for (let i = 0; i < albums.length; i += chunkSize) { chunks.push(albums.slice(i, i + chunkSize)); } for (const chunk of chunks) { 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); } })); } if (allTracks.length > 0) { for (let i = allTracks.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [allTracks[i], allTracks[j]] = [allTracks[j], allTracks[i]]; } player.setQueue(allTracks, 0); player.playTrackFromQueue(); } else { throw new Error("No tracks found across all albums"); } } catch (error) { console.error('Artist radio failed:', error); alert('Failed to start artist radio: ' + error.message); } finally { if (document.body.contains(btn)) { btn.disabled = false; btn.innerHTML = originalHTML; } } } if (e.target.closest('#download-discography-btn')) { const btn = e.target.closest('#download-discography-btn'); if (btn.disabled) return; const artistId = window.location.hash.split('/')[1]; if (!artistId) return; btn.disabled = true; const originalHTML = btn.innerHTML; btn.innerHTML = 'Downloading...'; try { const artist = await api.getArtist(artistId); await downloadDiscography(artist, api, player.quality, lyricsManager); } catch (error) { console.error('Discography download failed:', error); alert('Failed to download discography: ' + error.message); } finally { btn.disabled = false; btn.innerHTML = originalHTML; } } }); const searchForm = document.getElementById('search-form'); const searchInput = document.getElementById('search-input'); const performSearch = debounce((query) => { if (query) { window.location.hash = `#search/${encodeURIComponent(query)}`; } }, 300); searchInput.addEventListener('input', (e) => { const query = e.target.value.trim(); if (query.length > 2) { performSearch(query); } }); searchForm.addEventListener('submit', e => { e.preventDefault(); const query = searchInput.value.trim(); if (query) { window.location.hash = `#search/${encodeURIComponent(query)}`; } }); window.addEventListener('online', () => { hideOfflineNotification(); console.log('Back online'); }); window.addEventListener('offline', () => { showOfflineNotification(); console.log('Gone offline'); }); document.querySelector('.play-pause-btn').innerHTML = SVG_PLAY; const router = createRouter(ui); router(); window.addEventListener('hashchange', router); // Simple Navigation History const navStack = [window.location.hash]; let navIndex = 0; const updateNavButtons = () => { const backBtn = document.getElementById('nav-back'); const fwdBtn = document.getElementById('nav-forward'); if (backBtn) backBtn.disabled = navIndex <= 0; if (fwdBtn) fwdBtn.disabled = navIndex >= navStack.length - 1; }; window.addEventListener('hashchange', () => { const hash = window.location.hash; if (hash === navStack[navIndex]) return; if (navIndex > 0 && hash === navStack[navIndex - 1]) { navIndex--; // User went back } else if (navIndex < navStack.length - 1 && hash === navStack[navIndex + 1]) { navIndex++; // User went forward } else { navIndex++; navStack.splice(navIndex); // Truncate forward history navStack.push(hash); } updateNavButtons(); }); updateNavButtons(); audioPlayer.addEventListener('play', () => { updateTabTitle(player); }); // PWA Update Logic const updateSW = registerSW({ onNeedRefresh() { showUpdateNotification(() => updateSW(true)); }, onOfflineReady() { console.log('App ready to work offline'); }, }); let deferredPrompt; window.addEventListener('beforeinstallprompt', (e) => { e.preventDefault(); deferredPrompt = e; if (!localStorage.getItem('installPromptDismissed')) { showInstallPrompt(deferredPrompt); } }); document.getElementById('show-shortcuts-btn')?.addEventListener('click', () => { showKeyboardShortcuts(); }); if (!localStorage.getItem('shortcuts-shown') && window.innerWidth > 768) { setTimeout(() => { showKeyboardShortcuts(); localStorage.setItem('shortcuts-shown', 'true'); }, 3000); } // Listener for Firebase Sync updates window.addEventListener('library-changed', () => { const hash = window.location.hash; if (hash === '#library') { ui.renderLibraryPage(); } else if (hash === '#home' || hash === '') { ui.renderHomePage(); } }); window.addEventListener('history-changed', () => { const hash = window.location.hash; if (hash === '#recent') { ui.renderRecentPage(); } }); }); function showUpdateNotification(updateCallback) { const notification = document.createElement('div'); notification.className = 'update-notification'; notification.innerHTML = `
A new version of Monochrome is available.
Install this app for a better experience.
Unfortunately some songs weren't able to be added. This could be an issue with our import system, try searching for the song and adding it. But it could also be due to Monochrome not having it sadly :(