Spaces:
Sleeping
Sleeping
| let currentTrack = null; | |
| let isPlaying = false; | |
| let playlist = []; | |
| let currentView = 'all'; | |
| let playlists = JSON.parse(localStorage.getItem('playlists')) || {}; | |
| let currentLyrics = null; | |
| let isPlayingPlaylist = false; | |
| // Add these cache objects at the top of the file | |
| const ALBUM_CACHE = new Map(); | |
| const ARTIST_CACHE = new Map(); | |
| let lastStructureUpdate = 0; | |
| const audioPlayer = document.getElementById('audio-player'); | |
| const playBtn = document.getElementById('play-btn'); | |
| const prevBtn = document.getElementById('prev-btn'); | |
| const nextBtn = document.getElementById('next-btn'); | |
| const seekSlider = document.getElementById('seek-slider'); | |
| const volumeSlider = document.getElementById('volume-slider'); | |
| const currentTimeSpan = document.getElementById('current-time'); | |
| const durationSpan = document.getElementById('duration'); | |
| // Add this constant at the top of the file | |
| const DEFAULT_COVER = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMDAiIGhlaWdodD0iMjAwIiB2aWV3Qm94PSIwIDAgMjAwIDIwMCI+PHJlY3Qgd2lkdGg9IjIwMCIgaGVpZ2h0PSIyMDAiIGZpbGw9IiNlOWVjZWYiLz48dGV4dCB4PSI1MCUiIHk9IjUwJSIgc3R5bGU9ImZvbnQtZmFtaWx5OiBBcmlhbDsgZm9udC1zaXplOiAyMHB4OyBmaWxsOiAjNmM3NTdkOyBkb21pbmFudC1iYXNlbGluZTogbWlkZGxlOyB0ZXh0LWFuY2hvcjogbWlkZGxlOyI+Tm8gQWxidW0gQXJ0PC90ZXh0Pjwvc3ZnPg=='; | |
| // Add this function to check if we need to rebuild caches | |
| function shouldRebuildCaches() { | |
| const now = Date.now(); | |
| if (now - lastStructureUpdate > 5000) { // Only rebuild every 5 seconds max | |
| lastStructureUpdate = now; | |
| return true; | |
| } | |
| return false; | |
| } | |
| // Fetch music structure from the server | |
| fetch('/api/music-structure') | |
| .then(response => response.json()) | |
| .then(data => { | |
| playlist = flattenPlaylist(data); | |
| initializeViews(); | |
| }); | |
| function flattenPlaylist(structure, prefix = '') { | |
| let flat = []; | |
| structure.forEach(item => { | |
| if (item.type === 'file') { | |
| item.fullPath = prefix + item.path; | |
| flat.push(item); | |
| } else if (item.type === 'folder') { | |
| flat = flat.concat(flattenPlaylist(item.contents, item.path + '/')); | |
| } | |
| }); | |
| return flat; | |
| } | |
| function playTrack(index) { | |
| // If playing directly from main playlist, clear playlist context | |
| if (!isPlayingPlaylist) { | |
| currentPlaylist = null; | |
| } | |
| currentTrack = index; | |
| const track = playlist[index]; | |
| if (!track) return; | |
| audioPlayer.src = `/music/${track.path}`; | |
| audioPlayer.play() | |
| .then(() => { | |
| updatePlayerInfo(track); | |
| updatePlayButton(true); | |
| highlightCurrentTrack(); | |
| updateActiveLyricLine(audioPlayer.currentTime); | |
| }) | |
| .catch(error => { | |
| console.error('Error playing track:', error); | |
| showNotification('Error playing track'); | |
| playNext(); | |
| }); | |
| } | |
| function updatePlayerInfo(track) { | |
| document.getElementById('track-title').textContent = track.metadata?.title || track.name; | |
| document.getElementById('track-artist').textContent = track.metadata?.artist || 'Unknown'; | |
| document.getElementById('track-album').textContent = track.metadata?.album || 'Unknown'; | |
| // Update album art with error handling | |
| const albumArt = document.getElementById('album-art'); | |
| if (track.metadata?.artwork) { | |
| albumArt.src = track.metadata.artwork; | |
| // Add error handling for the album art | |
| albumArt.onerror = () => { | |
| albumArt.src = DEFAULT_COVER; | |
| }; | |
| } else { | |
| albumArt.src = DEFAULT_COVER; | |
| } | |
| // Update lyrics | |
| currentLyrics = track.metadata?.synchronized_lyrics || null; | |
| const lyricsContent = document.getElementById('lyrics-content'); | |
| if (track.metadata?.lyrics || currentLyrics) { | |
| if (currentLyrics) { | |
| displaySyncedLyrics(currentLyrics); | |
| } else { | |
| lyricsContent.innerHTML = `<div class="lyrics-line">${track.metadata.lyrics || ''}</div>`; | |
| } | |
| } else { | |
| lyricsContent.innerHTML = '<div class="lyrics-line">No lyrics available</div>'; | |
| } | |
| } | |
| function displaySyncedLyrics(lyrics) { | |
| const lyricsContent = document.getElementById('lyrics-content'); | |
| lyricsContent.innerHTML = lyrics | |
| .map((line, index) => ` | |
| <div class="lyrics-line" data-time="${line.time}" id="lyric-line-${index}"> | |
| ${line.text} | |
| </div> | |
| `) | |
| .join(''); | |
| // Add click handlers for each line | |
| lyricsContent.querySelectorAll('.lyrics-line').forEach(line => { | |
| line.addEventListener('click', () => { | |
| const time = parseFloat(line.dataset.time); | |
| if (!isNaN(time)) { | |
| audioPlayer.currentTime = time; | |
| audioPlayer.play() | |
| .then(() => updatePlayButton(true)) | |
| .catch(error => console.error('Error playing:', error)); | |
| } | |
| }); | |
| }); | |
| } | |
| function updateActiveLyricLine(currentTime) { | |
| if (!currentLyrics || !currentLyrics.length) return; | |
| // Find the current line by looking for the last lyric before current time | |
| let currentLineIndex = -1; | |
| for (let i = 0; i < currentLyrics.length; i++) { | |
| if (currentLyrics[i].time <= currentTime) { | |
| currentLineIndex = i; | |
| } else { | |
| break; | |
| } | |
| } | |
| // Remove active class from all lines | |
| document.querySelectorAll('.lyrics-line').forEach(line => { | |
| line.classList.remove('active'); | |
| }); | |
| // If we found a current line, highlight it and scroll to it | |
| if (currentLineIndex >= 0) { | |
| const lineElement = document.getElementById(`lyric-line-${currentLineIndex}`); | |
| if (lineElement) { | |
| lineElement.classList.add('active'); | |
| // Improved scrolling logic | |
| const container = document.getElementById('lyrics-content'); | |
| const containerRect = container.getBoundingClientRect(); | |
| const lineRect = lineElement.getBoundingClientRect(); | |
| // Only scroll if the line is outside the visible area | |
| if (lineRect.top < containerRect.top || lineRect.bottom > containerRect.bottom) { | |
| lineElement.scrollIntoView({ | |
| behavior: 'smooth', | |
| block: 'center' | |
| }); | |
| } | |
| } | |
| } | |
| } | |
| function updatePlayButton(playing) { | |
| isPlaying = playing; | |
| playBtn.innerHTML = playing ? '<i class="fas fa-pause"></i>' : '<i class="fas fa-play"></i>'; | |
| } | |
| function highlightCurrentTrack() { | |
| document.querySelectorAll('.track-list .music-item').forEach((item, index) => { | |
| item.classList.toggle('playing', index === currentTrack); | |
| }); | |
| } | |
| // Event Listeners | |
| playBtn.onclick = () => { | |
| if (currentTrack === null && playlist.length > 0) { | |
| playTrack(0); | |
| } else if (audioPlayer.paused) { | |
| audioPlayer.play(); | |
| updatePlayButton(true); | |
| } else { | |
| audioPlayer.pause(); | |
| updatePlayButton(false); | |
| } | |
| }; | |
| prevBtn.onclick = () => { | |
| playPrevious(); | |
| }; | |
| nextBtn.onclick = () => { | |
| playNext(); | |
| }; | |
| volumeSlider.oninput = (e) => { | |
| audioPlayer.volume = e.target.value / 100; | |
| }; | |
| seekSlider.oninput = (e) => { | |
| const time = (audioPlayer.duration * e.target.value) / 100; | |
| audioPlayer.currentTime = time; | |
| }; | |
| audioPlayer.ontimeupdate = () => { | |
| const percent = (audioPlayer.currentTime / audioPlayer.duration) * 100; | |
| seekSlider.value = percent; | |
| currentTimeSpan.textContent = formatTime(audioPlayer.currentTime); | |
| updateActiveLyricLine(audioPlayer.currentTime); | |
| }; | |
| audioPlayer.onloadedmetadata = () => { | |
| durationSpan.textContent = formatTime(audioPlayer.duration); | |
| }; | |
| audioPlayer.onended = () => { | |
| playNext(); | |
| }; | |
| function formatTime(seconds) { | |
| const minutes = Math.floor(seconds / 60); | |
| const remainingSeconds = Math.floor(seconds % 60); | |
| return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`; | |
| } | |
| function initializeViews() { | |
| document.querySelectorAll('.nav-menu a').forEach(link => { | |
| link.addEventListener('click', (e) => { | |
| e.preventDefault(); | |
| const view = e.target.dataset.view; | |
| switchView(view); | |
| }); | |
| }); | |
| document.getElementById('search-input').addEventListener('input', (e) => { | |
| filterContent(e.target.value); | |
| }); | |
| document.getElementById('new-playlist-btn').addEventListener('click', showPlaylistModal); | |
| document.getElementById('save-playlist').addEventListener('click', savePlaylist); | |
| document.getElementById('cancel-playlist').addEventListener('click', hidePlaylistModal); | |
| renderPlaylists(); | |
| // Initialize with all songs view | |
| renderAllSongs(); | |
| } | |
| function switchView(view) { | |
| currentView = view; | |
| document.querySelectorAll('.nav-menu a').forEach(link => { | |
| link.classList.toggle('active', link.dataset.view === view); | |
| }); | |
| switch(view) { | |
| case 'albums': | |
| renderAlbumView(); | |
| break; | |
| case 'artists': | |
| renderArtistView(); | |
| break; | |
| case 'playlists': | |
| renderPlaylistView(); | |
| break; | |
| default: | |
| renderAllSongs(); | |
| } | |
| } | |
| // Optimize renderAlbumView | |
| function renderAlbumView() { | |
| if (shouldRebuildCaches()) { | |
| ALBUM_CACHE.clear(); | |
| playlist.forEach(track => { | |
| if (track.metadata) { | |
| const albumName = track.metadata.album || 'Unknown Album'; | |
| const artistName = track.metadata.artist || 'Unknown Artist'; | |
| const albumKey = `${albumName}___${artistName}`; | |
| if (!ALBUM_CACHE.has(albumKey)) { | |
| ALBUM_CACHE.set(albumKey, { | |
| name: albumName, | |
| artist: artistName, | |
| artwork: track.metadata.artwork || DEFAULT_COVER, | |
| tracks: [] | |
| }); | |
| } | |
| ALBUM_CACHE.get(albumKey).tracks.push(track); | |
| } | |
| }); | |
| } | |
| const container = document.getElementById('view-container'); | |
| const albumsHtml = Array.from(ALBUM_CACHE.values()) | |
| .map(album => { | |
| const escapedName = escapeHtml(album.name); | |
| const escapedArtist = escapeHtml(album.artist); | |
| return ` | |
| <div class="album-card"> | |
| <div class="album-art-container"> | |
| <img class="album-art" src="${album.artwork}" onerror="this.src='${DEFAULT_COVER}'"> | |
| <button class="play-overlay" onclick="playAlbum('${escapedName}','${escapedArtist}')"> | |
| <i class="fas fa-play"></i> | |
| </button> | |
| </div> | |
| <div class="album-info" onclick="showAlbumTracks('${escapedName}','${escapedArtist}')"> | |
| <h3>${escapedName}</h3> | |
| <p>${album.artist}</p> | |
| <p>${album.tracks.length} tracks</p> | |
| </div> | |
| </div>`; | |
| }).join(''); | |
| container.innerHTML = `<div class="album-grid">${albumsHtml}</div>`; | |
| } | |
| // Optimize renderArtistView | |
| function renderArtistView() { | |
| if (shouldRebuildCaches()) { | |
| ARTIST_CACHE.clear(); | |
| playlist.forEach(track => { | |
| if (track.metadata) { | |
| const artistName = track.metadata.artist || 'Unknown Artist'; | |
| if (!ARTIST_CACHE.has(artistName)) { | |
| ARTIST_CACHE.set(artistName, { | |
| name: artistName, | |
| artwork: track.metadata.artwork || DEFAULT_COVER, | |
| tracks: [] | |
| }); | |
| } | |
| const artist = ARTIST_CACHE.get(artistName); | |
| artist.tracks.push(track); | |
| if (!artist.artwork && track.metadata.artwork) { | |
| artist.artwork = track.metadata.artwork; | |
| } | |
| } | |
| }); | |
| } | |
| const container = document.getElementById('view-container'); | |
| const artistsHtml = Array.from(ARTIST_CACHE.values()) | |
| .map(artist => { | |
| const escapedName = escapeHtml(artist.name); | |
| return ` | |
| <div class="artist-card"> | |
| <div class="artist-art-container"> | |
| <img class="artist-image" src="${artist.artwork}" onerror="this.src='${DEFAULT_COVER}'" alt="${escapedName}"> | |
| <button class="play-overlay" onclick="playArtist('${escapedName}')"> | |
| <i class="fas fa-play"></i> | |
| </button> | |
| </div> | |
| <div class="artist-info" onclick="showArtistTracks('${escapedName}')"> | |
| <h3>${escapedName}</h3> | |
| <p>${artist.tracks.length} tracks</p> | |
| </div> | |
| </div>`; | |
| }).join(''); | |
| container.innerHTML = `<div class="artist-grid">${artistsHtml}</div>`; | |
| } | |
| // Optimize track filtering functions | |
| function getAlbumTracks(albumName, artistName) { | |
| const albumKey = `${albumName}___${artistName}`; | |
| return ALBUM_CACHE.get(albumKey)?.tracks || []; | |
| } | |
| function getArtistTracks(artistName) { | |
| return ARTIST_CACHE.get(artistName)?.tracks || []; | |
| } | |
| function showAlbumTracks(albumName, artistName) { | |
| const tracks = getAlbumTracks(albumName, artistName); | |
| const container = document.getElementById('view-container'); | |
| const viewTitle = document.getElementById('view-title'); | |
| viewTitle.textContent = `Album: ${albumName}`; | |
| const albumArtwork = tracks[0]?.metadata?.artwork || DEFAULT_COVER; | |
| container.innerHTML = ` | |
| <div class="album-header"> | |
| <div class="album-info-large"> | |
| <img class="album-art-large" src="${albumArtwork}" onerror="this.src='${DEFAULT_COVER}'" alt="${escapeHtml(albumName)}"> | |
| <div class="album-details"> | |
| <h2>${escapeHtml(albumName)}</h2> | |
| <p>${artistName}</p> | |
| <p>${tracks.length} tracks</p> | |
| <button class="play-album-btn" onclick="playAlbum('${escapeHtml(albumName)}', '${escapeHtml(artistName)}')"> | |
| <i class="fas fa-play"></i> Play Album | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="track-list"> | |
| ${tracks.map((track, index) => ` | |
| <div class="music-item"> | |
| <div class="track-info" onclick="playTrack(${playlist.indexOf(track)})"> | |
| <div class="track-number">${(index + 1).toString().padStart(2, '0')}</div> | |
| <div> | |
| <div class="track-name">${track.metadata?.title || track.name}</div> | |
| <div class="track-artist">${track.metadata?.artist || 'Unknown Artist'}</div> | |
| </div> | |
| </div> | |
| <div class="track-actions"> | |
| <button class="menu-trigger" onclick="showTrackMenu(event, ${playlist.indexOf(track)})"> | |
| <i class="fas fa-ellipsis-v"></i> | |
| </button> | |
| </div> | |
| </div> | |
| `).join('')} | |
| </div> | |
| `; | |
| } | |
| function showArtistTracks(artistName) { | |
| const tracks = getArtistTracks(artistName); | |
| renderTrackList(tracks, `Artist: ${artistName}`); | |
| } | |
| function renderTrackList(tracks, title) { | |
| const container = document.getElementById('view-container'); | |
| document.getElementById('view-title').textContent = title; | |
| container.innerHTML = ` | |
| <div class="track-list"> | |
| ${tracks.map((track, index) => ` | |
| <div class="music-item"> | |
| <div class="track-info" onclick="playTrack(${playlist.indexOf(track)})"> | |
| <div class="track-name">${track.metadata?.title || track.name}</div> | |
| <div class="track-artist">${track.metadata?.artist || 'Unknown'}</div> | |
| </div> | |
| <div class="track-actions"> | |
| <button class="menu-trigger" onclick="showTrackMenu(event, ${playlist.indexOf(track)})"> | |
| <i class="fas fa-ellipsis-v"></i> | |
| </button> | |
| </div> | |
| </div> | |
| `).join('')} | |
| </div> | |
| `; | |
| } | |
| function showTrackMenu(event, trackIndex) { | |
| event.stopPropagation(); | |
| // Remove any existing menus | |
| document.querySelectorAll('.context-menu').forEach(m => m.remove()); | |
| const menu = document.createElement('div'); | |
| menu.className = 'context-menu'; | |
| const track = playlist[trackIndex]; | |
| menu.innerHTML = ` | |
| <div class="menu-items"> | |
| <div class="menu-item" onclick="playTrack(${trackIndex})"> | |
| <i class="fas fa-play"></i> Play | |
| </div> | |
| <div class="menu-item" onclick="showPlaylistModal(${trackIndex})"> | |
| <i class="fas fa-plus"></i> Add to Playlist | |
| </div> | |
| ${currentPlaylist ? ` | |
| <div class="menu-item" onclick="removeFromPlaylist('${currentPlaylist.name}', ${trackIndex})"> | |
| <i class="fas fa-trash"></i> Remove from Playlist | |
| </div> | |
| ` : ''} | |
| </div> | |
| `; | |
| document.body.appendChild(menu); | |
| // Position the menu | |
| const rect = event.target.closest('.menu-trigger').getBoundingClientRect(); | |
| menu.style.position = 'fixed'; | |
| menu.style.left = `${rect.left - menu.offsetWidth + rect.width}px`; | |
| menu.style.top = `${rect.bottom}px`; | |
| // Close menu when clicking outside | |
| document.addEventListener('click', function closeMenu(e) { | |
| if (!menu.contains(e.target) && !e.target.closest('.menu-trigger')) { | |
| menu.remove(); | |
| document.removeEventListener('click', closeMenu); | |
| } | |
| }); | |
| } | |
| function showPlaylistModal(trackIndex = null) { | |
| const modal = document.getElementById('playlist-modal'); | |
| modal.dataset.trackIndex = trackIndex !== null ? trackIndex : ''; | |
| // Clear previous input | |
| document.getElementById('playlist-name').value = ''; | |
| // Update modal title and render existing playlists | |
| const modalTitle = modal.querySelector('h2'); | |
| modalTitle.textContent = trackIndex !== null ? 'Add to Playlist' : 'Create Playlist'; | |
| // Render existing playlists | |
| const existingPlaylists = modal.querySelector('.existing-playlists'); | |
| if (trackIndex !== null) { | |
| existingPlaylists.innerHTML = Object.values(playlists).map(playlist => ` | |
| <div class="existing-playlist-item" onclick="addToExistingPlaylist('${playlist.name}', ${trackIndex})"> | |
| <i class="fas fa-list"></i> | |
| ${playlist.name} | |
| <span class="playlist-count">${playlist.tracks.length} tracks</span> | |
| </div> | |
| `).join('') || '<div class="no-playlists">No playlists yet</div>'; | |
| } else { | |
| existingPlaylists.innerHTML = ''; | |
| } | |
| modal.style.display = 'flex'; | |
| } | |
| function addToExistingPlaylist(playlistName, trackIndex) { | |
| const track = playlist[trackIndex]; | |
| if (!track) return; | |
| if (!playlists[playlistName].tracks.some(t => t.path === track.path)) { | |
| playlists[playlistName].tracks.push(track); | |
| localStorage.setItem('playlists', JSON.stringify(playlists)); | |
| showNotification(`Added to ${playlistName}`); | |
| renderPlaylists(); | |
| // If we're in playlist view, refresh it | |
| if (currentView === 'playlists') { | |
| renderPlaylistView(); | |
| } | |
| } else { | |
| showNotification('Track already in playlist'); | |
| } | |
| hidePlaylistModal(); | |
| } | |
| function hidePlaylistModal() { | |
| document.getElementById('playlist-modal').style.display = 'none'; | |
| document.getElementById('playlist-name').value = ''; | |
| } | |
| function savePlaylist() { | |
| const name = document.getElementById('playlist-name').value.trim(); | |
| const trackIndex = document.getElementById('playlist-modal').dataset.trackIndex; | |
| if (!name) { | |
| showNotification('Please enter a playlist name'); | |
| return; | |
| } | |
| if (playlists[name]) { | |
| showNotification('Playlist already exists'); | |
| return; | |
| } | |
| // Create new playlist | |
| playlists[name] = { | |
| name: name, | |
| tracks: [] | |
| }; | |
| // If we're adding a specific track | |
| if (trackIndex !== '') { | |
| const track = playlist[parseInt(trackIndex)]; | |
| playlists[name].tracks.push(track); | |
| } | |
| localStorage.setItem('playlists', JSON.stringify(playlists)); | |
| renderPlaylists(); | |
| hidePlaylistModal(); | |
| showNotification('Playlist created'); | |
| // If we're in playlist view, refresh it | |
| if (currentView === 'playlists') { | |
| renderPlaylistView(); | |
| } | |
| } | |
| function renderPlaylists() { | |
| const container = document.getElementById('playlist-list'); | |
| container.innerHTML = Object.values(playlists).map(playlist => ` | |
| <li> | |
| <a href="#" onclick="showPlaylist('${playlist.name}')"> | |
| <i class="fas fa-list"></i> ${playlist.name} | |
| <span class="playlist-count">${playlist.tracks.length}</span> | |
| </a> | |
| </li> | |
| `).join(''); | |
| } | |
| function showPlaylist(name) { | |
| if (!playlists[name]) return; | |
| currentPlaylist = playlists[name]; | |
| isPlayingPlaylist = true; | |
| currentView = 'playlist-detail'; | |
| const container = document.getElementById('view-container'); | |
| const viewTitle = document.getElementById('view-title'); | |
| viewTitle.textContent = name; | |
| container.innerHTML = ` | |
| <div class="playlist-header"> | |
| <div class="playlist-info"> | |
| <i class="fas fa-list playlist-icon"></i> | |
| <div> | |
| <h2>${name}</h2> | |
| <p>${currentPlaylist.tracks.length} tracks</p> | |
| </div> | |
| </div> | |
| <button class="play-playlist" onclick="playPlaylist('${name}')"> | |
| <i class="fas fa-play"></i> Play | |
| </button> | |
| </div> | |
| <div class="track-list"> | |
| ${currentPlaylist.tracks.map((track, index) => ` | |
| <div class="music-item" data-index="${index}"> | |
| <div class="track-info" onclick="playPlaylistTrack('${name}', ${index})"> | |
| <div class="track-name">${track.metadata?.title || track.name}</div> | |
| <div class="track-artist">${track.metadata?.artist || 'Unknown'}</div> | |
| </div> | |
| <div class="track-actions"> | |
| <button class="menu-trigger" onclick="showPlaylistTrackMenu(event, '${name}', ${index})"> | |
| <i class="fas fa-ellipsis-v"></i> | |
| </button> | |
| </div> | |
| </div> | |
| `).join('')} | |
| </div> | |
| `; | |
| } | |
| function playPlaylist(name) { | |
| if (!playlists[name] || !playlists[name].tracks.length) return; | |
| currentPlaylist = playlists[name]; | |
| isPlayingPlaylist = true; | |
| // Find the first track in the main playlist | |
| const firstTrack = currentPlaylist.tracks[0]; | |
| const mainPlaylistIndex = playlist.findIndex(t => t.path === firstTrack.path); | |
| if (mainPlaylistIndex !== -1) { | |
| playTrack(mainPlaylistIndex); | |
| showNotification(`Playing playlist: ${name}`); | |
| } | |
| } | |
| function playPlaylistTrack(playlistName, index) { | |
| if (!playlists[playlistName]) return; | |
| currentPlaylist = playlists[playlistName]; | |
| isPlayingPlaylist = true; | |
| const track = currentPlaylist.tracks[index]; | |
| // Find the track in the main playlist | |
| const mainPlaylistIndex = playlist.findIndex(t => t.path === track.path); | |
| if (mainPlaylistIndex !== -1) { | |
| playTrack(mainPlaylistIndex); | |
| } | |
| } | |
| function showPlaylistTrackMenu(event, playlistName, index) { | |
| event.stopPropagation(); | |
| const menu = document.createElement('div'); | |
| menu.className = 'context-menu'; | |
| menu.innerHTML = ` | |
| <div class="menu-items"> | |
| <div class="menu-item" onclick="playPlaylistTrack('${playlistName}', ${index})"> | |
| <i class="fas fa-play"></i> Play | |
| </div> | |
| <div class="menu-item" onclick="removeFromPlaylist('${playlistName}', ${index})"> | |
| <i class="fas fa-trash"></i> Remove from Playlist | |
| </div> | |
| </div> | |
| `; | |
| // Remove any existing menus | |
| document.querySelectorAll('.context-menu').forEach(m => m.remove()); | |
| document.body.appendChild(menu); | |
| // Position the menu | |
| const rect = event.target.getBoundingClientRect(); | |
| menu.style.position = 'fixed'; | |
| menu.style.left = `${rect.left}px`; | |
| menu.style.top = `${rect.bottom}px`; | |
| // Close menu when clicking outside | |
| document.addEventListener('click', function closeMenu(e) { | |
| if (!menu.contains(e.target) && !e.target.closest('.menu-trigger')) { | |
| menu.remove(); | |
| document.removeEventListener('click', closeMenu); | |
| } | |
| }); | |
| } | |
| function playNext() { | |
| if (isPlayingPlaylist && currentPlaylist) { | |
| // Find current track index in playlist | |
| const currentTrackPath = playlist[currentTrack].path; | |
| const playlistTrackIndex = currentPlaylist.tracks.findIndex(t => t.path === currentTrackPath); | |
| if (playlistTrackIndex < currentPlaylist.tracks.length - 1) { | |
| // Play next track in playlist | |
| const nextTrack = currentPlaylist.tracks[playlistTrackIndex + 1]; | |
| const mainPlaylistIndex = playlist.findIndex(t => t.path === nextTrack.path); | |
| if (mainPlaylistIndex !== -1) { | |
| playTrack(mainPlaylistIndex); | |
| return; | |
| } | |
| } else { | |
| // End of playlist reached | |
| currentPlaylist = null; | |
| isPlayingPlaylist = false; | |
| updatePlayButton(false); | |
| return; | |
| } | |
| } | |
| // If no playlist or playlist ended, play next in main playlist | |
| if (currentTrack < playlist.length - 1) { | |
| playTrack(currentTrack + 1); | |
| } else { | |
| updatePlayButton(false); | |
| } | |
| } | |
| // Add this function to handle previous track | |
| function playPrevious() { | |
| if (isPlayingPlaylist && currentPlaylist) { | |
| // Find current track index in playlist | |
| const currentTrackPath = playlist[currentTrack].path; | |
| const playlistTrackIndex = currentPlaylist.tracks.findIndex(t => t.path === currentTrackPath); | |
| if (playlistTrackIndex > 0) { | |
| // Play previous track in playlist | |
| const prevTrack = currentPlaylist.tracks[playlistTrackIndex - 1]; | |
| const mainPlaylistIndex = playlist.findIndex(t => t.path === prevTrack.path); | |
| if (mainPlaylistIndex !== -1) { | |
| playTrack(mainPlaylistIndex); | |
| return; | |
| } | |
| } | |
| } | |
| // If no playlist or at start of playlist, play previous in main playlist | |
| if (currentTrack > 0) { | |
| playTrack(currentTrack - 1); | |
| } | |
| } | |
| // Add this function to initialize audio player event listeners | |
| function initializeAudioPlayer() { | |
| audioPlayer.addEventListener('ended', () => { | |
| playNext(); | |
| }); | |
| audioPlayer.addEventListener('error', (e) => { | |
| console.error('Audio player error:', e); | |
| showNotification('Error playing track'); | |
| playNext(); // Skip to next track on error | |
| }); | |
| // Add this timeupdate listener here instead of in DOMContentLoaded | |
| audioPlayer.addEventListener('timeupdate', () => { | |
| const percent = (audioPlayer.currentTime / audioPlayer.duration) * 100; | |
| seekSlider.value = percent; | |
| currentTimeSpan.textContent = formatTime(audioPlayer.currentTime); | |
| updateActiveLyricLine(audioPlayer.currentTime); | |
| }); | |
| } | |
| // Update the initialization code | |
| document.addEventListener('DOMContentLoaded', function() { | |
| initializeAudioPlayer(); | |
| document.querySelectorAll('.queue-item-actions button').forEach(button => { | |
| button.addEventListener('click', (e) => { | |
| e.stopPropagation(); | |
| }); | |
| }); | |
| // Add lyrics toggle handler | |
| document.getElementById('lyrics-toggle').addEventListener('click', () => { | |
| syncedLyricsMode = !syncedLyricsMode; | |
| if (currentTrack !== null) { | |
| updatePlayerInfo(playlist[currentTrack]); | |
| } | |
| }); | |
| }); | |
| function renderAllSongs() { | |
| const container = document.getElementById('view-container'); | |
| const viewTitle = document.getElementById('view-title'); | |
| viewTitle.textContent = 'All Songs'; | |
| container.innerHTML = ` | |
| <div class="track-list"> | |
| ${playlist.map((track, index) => ` | |
| <div class="music-item"> | |
| <div class="track-info" onclick="playFromMainPlaylist(${index})"> | |
| <div class="track-name">${track.metadata?.title || track.name}</div> | |
| <div class="track-artist">${track.metadata?.artist || 'Unknown'}</div> | |
| </div> | |
| <div class="track-actions"> | |
| <button class="menu-trigger" onclick="showTrackMenu(event, ${index})"> | |
| <i class="fas fa-ellipsis-v"></i> | |
| </button> | |
| </div> | |
| </div> | |
| `).join('')} | |
| </div> | |
| `; | |
| } | |
| function filterContent(query) { | |
| query = query.toLowerCase(); | |
| const filteredTracks = playlist.filter(track => | |
| (track.metadata?.title || track.name).toLowerCase().includes(query) || | |
| (track.metadata?.artist || 'Unknown').toLowerCase().includes(query) || | |
| (track.metadata?.album || 'Unknown').toLowerCase().includes(query) | |
| ); | |
| const container = document.getElementById('view-container'); | |
| const viewTitle = document.getElementById('view-title'); | |
| viewTitle.textContent = 'Search Results'; | |
| container.innerHTML = ` | |
| <div class="track-list"> | |
| ${filteredTracks.map((track, index) => ` | |
| <div class="music-item"> | |
| <div class="track-info" onclick="playTrack(${playlist.indexOf(track)})"> | |
| <div class="track-name">${track.metadata?.title || track.name}</div> | |
| <div class="track-artist">${track.metadata?.artist || 'Unknown'}</div> | |
| </div> | |
| <div class="track-actions"> | |
| <button class="menu-trigger" onclick="showTrackMenu(event, ${playlist.indexOf(track)})"> | |
| <i class="fas fa-ellipsis-v"></i> | |
| </button> | |
| </div> | |
| </div> | |
| `).join('')} | |
| </div> | |
| `; | |
| } | |
| function renderPlaylistView() { | |
| const container = document.getElementById('view-container'); | |
| const viewTitle = document.getElementById('view-title'); | |
| viewTitle.textContent = 'Playlists'; | |
| container.innerHTML = ` | |
| <div class="playlist-grid"> | |
| ${Object.values(playlists).map(playlist => ` | |
| <div class="playlist-card" onclick="showPlaylist('${playlist.name}')"> | |
| <div class="playlist-art"> | |
| <i class="fas fa-music"></i> | |
| </div> | |
| <h3>${playlist.name}</h3> | |
| <p>${playlist.tracks.length} tracks</p> | |
| <button class="delete-playlist" onclick="deletePlaylist(event, '${playlist.name}')"> | |
| <i class="fas fa-trash"></i> | |
| </button> | |
| </div> | |
| `).join('')} | |
| </div> | |
| `; | |
| } | |
| function deletePlaylist(event, playlistName) { | |
| event.stopPropagation(); | |
| if (confirm(`Delete playlist "${playlistName}"?`)) { | |
| delete playlists[playlistName]; | |
| localStorage.setItem('playlists', JSON.stringify(playlists)); | |
| renderPlaylistView(); | |
| renderPlaylists(); | |
| showNotification('Playlist deleted'); | |
| } | |
| } | |
| function removeFromPlaylist(playlistName, index) { | |
| if (!playlists[playlistName]) return; | |
| playlists[playlistName].tracks.splice(index, 1); | |
| localStorage.setItem('playlists', JSON.stringify(playlists)); | |
| // Refresh the current playlist view | |
| showPlaylist(playlistName); | |
| renderPlaylists(); | |
| showNotification('Track removed from playlist'); | |
| } | |
| function showNotification(message) { | |
| const notification = document.createElement('div'); | |
| notification.className = 'notification'; | |
| notification.textContent = message; | |
| document.body.appendChild(notification); | |
| setTimeout(() => { | |
| notification.classList.add('fade-out'); | |
| setTimeout(() => notification.remove(), 300); | |
| }, 2000); | |
| } | |
| // Add these new functions to handle playing albums and artists | |
| function playAlbum(albumName, artistName) { | |
| const albumTracks = getAlbumTracks(albumName, artistName); | |
| if (albumTracks.length > 0) { | |
| currentPlaylist = { | |
| name: albumName, | |
| tracks: [...albumTracks] | |
| }; | |
| isPlayingPlaylist = true; | |
| const mainPlaylistIndex = playlist.findIndex(track => track.path === albumTracks[0].path); | |
| if (mainPlaylistIndex !== -1) { | |
| playTrack(mainPlaylistIndex); | |
| showNotification(`Playing album: ${albumName}`); | |
| } | |
| } | |
| } | |
| function playArtist(artistName) { | |
| const tracks = getArtistTracks(artistName); | |
| if (tracks.length > 0) { | |
| currentPlaylist = { | |
| name: artistName, | |
| tracks: [...tracks] | |
| }; | |
| isPlayingPlaylist = true; | |
| const mainPlaylistIndex = playlist.findIndex(track => track.path === tracks[0].path); | |
| if (mainPlaylistIndex !== -1) { | |
| playTrack(mainPlaylistIndex); | |
| showNotification(`Playing artist: ${artistName}`); | |
| } | |
| } | |
| } | |
| // Add this new function to handle playing from main playlist | |
| function playFromMainPlaylist(index) { | |
| isPlayingPlaylist = false; | |
| currentPlaylist = null; | |
| playTrack(index); | |
| } | |
| // Add this helper function to escape HTML special characters | |
| function escapeHtml(unsafe) { | |
| return unsafe | |
| .replace(/&/g, "&") | |
| .replace(/</g, "<") | |
| .replace(/>/g, ">") | |
| .replace(/"/g, """) | |
| .replace(/'/g, "'"); | |
| } |