//js/app.js import { LosslessAPI } from './api.js'; import { apiSettings, themeManager, nowPlayingSettings, trackListSettings, downloadQualitySettings, } from './storage.js'; import { UIRenderer } from './ui.js'; import { Player } from './player.js'; import { LastFMScrobbler } from './lastfm.js'; import { LyricsManager, openLyricsPanel, 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'; import { sidePanelManager } from './side-panel.js'; import { db } from './db.js'; import { syncManager } from './accounts/pocketbase.js'; import { registerSW } from 'virtual:pwa-register'; import './smooth-scrolling.js'; import { readTrackMetadata } from './metadata.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) { 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); } } 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); // 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'; } } // 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); 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') { 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, 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); } }); 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, 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.hash.split('/')[1]; 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; } 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 = ''; 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('#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 { // 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; 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.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(); db.createPlaylist(name, tracks, cover).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.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 || ''; // 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}${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.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.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; document.getElementById('playlist-cover-input').value = playlist.cover || ''; 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.classList.add('active'); 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(() => { syncManager.syncUserPlaylist({ id: playlistId }, 'delete'); 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'); 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.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 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.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, 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('#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('#shuffle-liked-tracks-btn')) { const btn = e.target.closest('#shuffle-liked-tracks-btn'); if (btn.disabled) return; try { const likedTracks = await db.getFavorites('track'); if (likedTracks.length > 0) { // Shuffle array for (let i = likedTracks.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [likedTracks[i], likedTracks[j]] = [likedTracks[j], likedTracks[i]]; } player.setQueue(likedTracks, 0); document.getElementById('shuffle-btn').classList.remove('active'); player.playTrackFromQueue(); } } catch (error) { console.error('Failed to shuffle liked tracks:', error); } } 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; try { const artist = await api.getArtist(artistId); showDiscographyDownloadModal(artist, api, downloadQualitySettings.getQuality(), lyricsManager, btn); } catch (error) { console.error('Failed to load artist for discography download:', error); alert('Failed to load artist: ' + error.message); } } // Local Files Logic lollll if (e.target.closest('#select-local-folder-btn') || e.target.closest('#change-local-folder-btn')) { try { const handle = await window.showDirectoryPicker({ id: 'music-folder', mode: 'read', }); await db.saveSetting('local_folder_handle', handle); const btn = document.getElementById('select-local-folder-btn'); const btnText = document.getElementById('select-local-folder-text'); if (btn) { if (btnText) btnText.textContent = 'Scanning...'; else btn.textContent = 'Scanning...'; btn.disabled = true; } const tracks = []; let idCounter = 0; async function scanDirectory(dirHandle) { for await (const entry of dirHandle.values()) { if (entry.kind === 'file') { const name = entry.name.toLowerCase(); if ( name.endsWith('.flac') || name.endsWith('.mp3') || name.endsWith('.m4a') || name.endsWith('.wav') || name.endsWith('.ogg') ) { const file = await entry.getFile(); const metadata = await readTrackMetadata(file); metadata.id = `local-${idCounter++}-${file.name}`; tracks.push(metadata); } } else if (entry.kind === 'directory') { await scanDirectory(entry); } } } await scanDirectory(handle); tracks.sort((a, b) => { const artistA = a.artist.name || ''; const artistB = b.artist.name || ''; return artistA.localeCompare(artistB); }); window.localFilesCache = tracks; ui.renderLibraryPage(); } catch (err) { if (err.name !== 'AbortError') { console.error('Error selecting folder:', err); alert('Failed to access folder. Please try again.'); } const btn = document.getElementById('select-local-folder-btn'); const btnText = document.getElementById('select-local-folder-text'); if (btn) { if (btnText) btnText.textContent = 'Select Music Folder'; else btn.textContent = 'Select Music Folder'; btn.disabled = false; } } } }); 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 Pocketbase Sync updates window.addEventListener('library-changed', () => { const hash = window.location.hash; if (hash === '#library') { ui.renderLibraryPage(); } else if (hash === '#home' || hash === '') { ui.renderHomePage(); } else if (hash.startsWith('#userplaylist/')) { const playlistId = hash.split('/')[1]; const content = document.querySelector('.main-content'); const scroll = content ? content.scrollTop : 0; ui.renderPlaylistPage(playlistId, 'user').then(() => { if (content) content.scrollTop = scroll; }); } }); window.addEventListener('history-changed', () => { const hash = window.location.hash; if (hash === '#recent') { ui.renderRecentPage(); } }); const contextMenu = document.getElementById('context-menu'); if (contextMenu) { const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.type === 'attributes' && mutation.attributeName === 'style') { if (contextMenu.style.display === 'block') { const track = contextMenu._contextTrack; const albumItem = contextMenu.querySelector('[data-action="go-to-album"]'); const artistItem = contextMenu.querySelector('[data-action="go-to-artist"]'); if (track) { if (albumItem) { let label = 'Album'; const albumType = track.album?.type?.toUpperCase(); const trackCount = track.album?.numberOfTracks; if (albumType === 'SINGLE' || trackCount === 1) label = 'Single'; else if (albumType === 'EP') label = 'EP'; else if (trackCount && trackCount <= 6) label = 'EP'; albumItem.textContent = `Go to ${label}`; albumItem.style.display = track.album ? 'block' : 'none'; } if (artistItem) { const hasArtist = track.artist || (track.artists && track.artists.length > 0); artistItem.style.display = hasArtist ? 'block' : 'none'; } } } } }); }); observer.observe(contextMenu, { attributes: true }); } }); 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.