//js/app.js
import { LosslessAPI } from './api.js';
import { apiSettings, themeManager, nowPlayingSettings } from './storage.js';
import { UIRenderer } from './ui.js';
import { Player } from './player.js';
import { LastFMScrobbler } from './lastfm.js';
import { LyricsManager, createLyricsPanel, showKaraokeView, showSyncedLyricsPanel, clearLyricsPanelSync } from './lyrics.js';
import { createRouter, updateTabTitle } from './router.js';
import { initializeSettings } from './settings.js';
import { initializePlayerEvents, initializeTrackInteractions } 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', () => {
audioPlayer.remote.prompt().catch(err => {
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);
}
const karaokeView = document.getElementById('karaoke-view');
if (karaokeView) {
karaokeView.remove();
}
break;
case 'l':
document.querySelector('.now-playing-bar .cover')?.click();
break;
}
});
}
function initializeMediaSessionHandlers(player) {
if (!('mediaSession' in navigator)) return;
try {
navigator.mediaSession.setActionHandler('seekto', (details) => {
if (details.seekTime !== undefined && details.fastSeek !== undefined && details.fastSeek) {
player.audio.currentTime = details.seekTime;
player.updateMediaSessionPositionState();
}
});
} catch (error) {
console.log('seekto action not supported');
}
}
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 ui = new UIRenderer(api);
const audioPlayer = document.getElementById('audio-player');
const currentQuality = localStorage.getItem('playback-quality') || 'LOSSLESS';
const player = new Player(audioPlayer, api, currentQuality);
const scrobbler = new LastFMScrobbler();
const lyricsManager = new LyricsManager(api);
const lyricsPanel = createLyricsPanel();
const currentTheme = themeManager.getTheme();
themeManager.setTheme(currentTheme);
initializeSettings(scrobbler, player, api, ui);
initializePlayerEvents(player, audioPlayer, scrobbler);
initializeTrackInteractions(player, api, document.querySelector('.main-content'), document.getElementById('context-menu'), lyricsManager);
initializeUIInteractions(player, api);
initializeKeyboardShortcuts(player, audioPlayer, lyricsPanel);
initializeMediaSessionHandlers(player);
const castBtn = document.getElementById('cast-btn');
initializeCasting(audioPlayer, castBtn);
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 === 'karaoke') {
lyricsPanel.classList.add('hidden');
clearLyricsPanelSync(audioPlayer, lyricsPanel);
const lyricsData = await lyricsManager.fetchLyrics(player.currentTrack.id);
if (lyricsData) {
showKaraokeView(player.currentTrack, lyricsData, audioPlayer);
} else {
alert('No lyrics available for this track');
}
} else if (mode === 'lyrics') {
const isHidden = lyricsPanel.classList.contains('hidden');
lyricsPanel.classList.toggle('hidden');
if (isHidden) {
const content = lyricsPanel.querySelector('.lyrics-content');
content.innerHTML = '
Loading lyrics...
';
const lyricsData = await lyricsManager.fetchLyrics(player.currentTrack.id);
if (lyricsData) {
lyricsManager.currentLyrics = lyricsData;
showSyncedLyricsPanel(lyricsData, audioPlayer, lyricsPanel);
} else {
content.innerHTML = 'Failed to load lyrics
';
}
} else {
// Clear sync when hiding
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', (e) => {
e.stopPropagation();
if (lyricsManager.currentLyrics && player.currentTrack) {
lyricsManager.downloadLRC(lyricsManager.currentLyrics, player.currentTrack);
}
});
document.getElementById('download-current-btn')?.addEventListener('click', () => {
if (player.currentTrack) {
downloadTrackWithMetadata(player.currentTrack, player.quality, api, lyricsManager);
}
});
// Auto-update lyrics when track changes
let previousTrackId = null;
audioPlayer.addEventListener('play', async () => {
if (!player.currentTrack) return;
const currentTrackId = player.currentTrack.id;
if (currentTrackId === previousTrackId) return;
previousTrackId = currentTrackId;
// Update lyrics panel if it's open
if (!lyricsPanel.classList.contains('hidden')) {
const mode = nowPlayingSettings.getMode();
if (mode === 'lyrics') {
const content = lyricsPanel.querySelector('.lyrics-content');
content.innerHTML = 'Loading lyrics...
';
const lyricsData = await lyricsManager.fetchLyrics(player.currentTrack.id);
if (lyricsData) {
lyricsManager.currentLyrics = lyricsData;
// Clear old sync before showing new
clearLyricsPanelSync(audioPlayer, lyricsPanel);
showSyncedLyricsPanel(lyricsData, audioPlayer, lyricsPanel);
} else {
content.innerHTML = 'No lyrics available for this track
';
}
}
}
});
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);
if (!artist.albums || artist.albums.length === 0) {
throw new Error("No albums found for this artist");
}
const trackSet = new Set();
const allTracks = [];
const chunks = [];
const chunkSize = 3;
const albums = artist.albums;
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);
}
});
if (!localStorage.getItem('shortcuts-shown')) {
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 = `
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();
}
});
}