//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, createLyricsPanel, showSyncedLyricsPanel, clearLyricsPanelSync } 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'; 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, lyricsPanel) { 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(); document.getElementById('queue-modal-overlay').style.display = 'none'; if (lyricsPanel) { lyricsPanel.classList.add('hidden'); clearLyricsPanelSync(audioPlayer, lyricsPanel); } 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); const lyricsPanel = createLyricsPanel(); 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); initializeUIInteractions(player, api); initializeKeyboardShortcuts(player, audioPlayer, lyricsPanel); 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 isHidden = lyricsPanel.classList.contains('hidden'); lyricsPanel.classList.toggle('hidden'); if (isHidden) { await showSyncedLyricsPanel(player.currentTrack, audioPlayer, lyricsPanel, lyricsManager); } else { clearLyricsPanelSync(audioPlayer, lyricsPanel); } } 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); } } else { // Default to 'album' mode - navigate to album if (player.currentTrack.album?.id) { window.location.hash = `#album/${player.currentTrack.album.id}`; } } }); document.getElementById('close-fullscreen-cover-btn')?.addEventListener('click', () => { ui.closeFullscreenCover(); }); document.getElementById('fullscreen-cover-image')?.addEventListener('click', () => { ui.closeFullscreenCover(); }); document.getElementById('toggle-lyrics-btn')?.addEventListener('click', async (e) => { e.stopPropagation(); if (!player.currentTrack) { alert('No track is currently playing'); return; } const isHidden = lyricsPanel.classList.contains('hidden'); lyricsPanel.classList.toggle('hidden'); if (isHidden) { await showSyncedLyricsPanel(player.currentTrack, audioPlayer, lyricsPanel, lyricsManager); } else { clearLyricsPanelSync(audioPlayer, lyricsPanel); } }); document.getElementById('close-lyrics-btn')?.addEventListener('click', (e) => { e.stopPropagation(); lyricsPanel.classList.add('hidden'); clearLyricsPanelSync(audioPlayer, lyricsPanel); }); document.getElementById('download-lrc-btn')?.addEventListener('click', async (e) => { e.stopPropagation(); if (!player.currentTrack) { alert('No track is currently playing'); return; } const btn = e.target.closest('#download-lrc-btn'); btn.disabled = true; try { const lyricsData = await lyricsManager.fetchLyrics(player.currentTrack.id, player.currentTrack); if (lyricsData) { lyricsManager.downloadLRC(lyricsData, player.currentTrack); } else { alert('No synced lyrics available for download'); } } catch (error) { console.error('Failed to download lyrics!', error); alert('Failed to Download Lyrics! check the console for more information.') } finally { btn.disabled = false; } }); 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 (!lyricsPanel.classList.contains('hidden')) { clearLyricsPanelSync(audioPlayer, lyricsPanel); await showSyncedLyricsPanel(player.currentTrack, audioPlayer, lyricsPanel, 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); } }); 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-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 { const { playlist, tracks } = await api.getPlaylist(playlistId); 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('#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 { const { tracks } = await api.getPlaylist(playlistId); 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); audioPlayer.addEventListener('play', () => { updateTabTitle(player); }); if ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker.register('./sw.js') .then(reg => { console.log('Service worker registered'); reg.addEventListener('updatefound', () => { const newWorker = reg.installing; newWorker.addEventListener('statechange', () => { if (newWorker.state === 'installed' && navigator.serviceWorker.controller) { showUpdateNotification(); } }); }); }) .catch(err => console.log('Service worker not registered', err)); }); } 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); } }); function showUpdateNotification() { const notification = document.createElement('div'); notification.className = 'update-notification'; notification.innerHTML = `
Update Available

A new version of Monochrome is available.

`; document.body.appendChild(notification); } function showInstallPrompt(deferredPrompt) { if (!deferredPrompt) return; const notification = document.createElement('div'); notification.className = 'install-prompt'; notification.innerHTML = `
Install Monochrome

Install this app for a better experience.

`; document.body.appendChild(notification); document.getElementById('install-btn').addEventListener('click', async () => { notification.remove(); deferredPrompt.prompt(); const { outcome } = await deferredPrompt.userChoice; console.log(`User response to install prompt: ${outcome}`); deferredPrompt = null; }); document.getElementById('dismiss-install').addEventListener('click', () => { notification.remove(); localStorage.setItem('installPromptDismissed', 'true'); }); } function showKeyboardShortcuts() { const modal = document.createElement('div'); modal.className = 'shortcuts-modal-overlay'; modal.innerHTML = `

Keyboard Shortcuts

Space Play / Pause
Seek forward 10s
Seek backward 10s
Shift + Next track
Shift + Previous track
Volume up
Volume down
M Mute / Unmute
S Toggle shuffle
R Toggle repeat
Q Open queue
L Toggle lyrics
/ Focus search
Esc Close modals
`; document.body.appendChild(modal); modal.addEventListener('click', (e) => { if (e.target === modal || e.target.classList.contains('close-shortcuts')) { modal.remove(); } }); }