From 81dab0ed4856371a043ca9c5f211949fb36f9254 Mon Sep 17 00:00:00 2001 From: Julien Maille Date: Tue, 23 Dec 2025 21:16:38 +0100 Subject: [PATCH] feat: implement queue persistence and improve playback restoration --- js/events.js | 15 +++++++++++ js/player.js | 75 +++++++++++++++++++++++++++++++++++++++++++++++++-- js/storage.js | 30 +++++++++++++++++++++ sw.js | 10 +++++-- 4 files changed, 126 insertions(+), 4 deletions(-) diff --git a/js/events.js b/js/events.js index 209f0d4..33814f3 100644 --- a/js/events.js +++ b/js/events.js @@ -11,6 +11,21 @@ export function initializePlayerEvents(player, audioPlayer, scrobbler) { const shuffleBtn = document.getElementById('shuffle-btn'); const repeatBtn = document.getElementById('repeat-btn'); + // Sync UI with player state on load + if (player.shuffleActive) { + shuffleBtn.classList.add('active'); + } + + if (player.repeatMode !== REPEAT_MODE.OFF) { + repeatBtn.classList.add('active'); + if (player.repeatMode === REPEAT_MODE.ONE) { + repeatBtn.classList.add('repeat-one'); + } + repeatBtn.title = player.repeatMode === REPEAT_MODE.ALL ? 'Repeat Queue' : 'Repeat One'; + } else { + repeatBtn.title = 'Repeat'; + } + audioPlayer.addEventListener('play', () => { if (scrobbler.isAuthenticated() && lastFMStorage.isEnabled() && player.currentTrack) { scrobbler.updateNowPlaying(player.currentTrack); diff --git a/js/player.js b/js/player.js index 35d6c40..c67b689 100644 --- a/js/player.js +++ b/js/player.js @@ -1,5 +1,6 @@ //js/player.js import { REPEAT_MODE, formatTime, getTrackArtists, getTrackTitle} from './utils.js'; +import { queueManager } from './storage.js'; export class Player { constructor(audioElement, api, quality = 'LOSSLESS') { @@ -16,7 +17,58 @@ export class Player { this.preloadAbortController = null; this.currentTrack = null; + this.loadQueueState(); this.setupMediaSession(); + + window.addEventListener('beforeunload', () => { + this.saveQueueState(); + }); + } + + loadQueueState() { + const savedState = queueManager.getQueue(); + if (savedState) { + this.queue = savedState.queue || []; + this.shuffledQueue = savedState.shuffledQueue || []; + this.originalQueueBeforeShuffle = savedState.originalQueueBeforeShuffle || []; + this.currentQueueIndex = savedState.currentQueueIndex ?? -1; + this.shuffleActive = savedState.shuffleActive || false; + this.repeatMode = savedState.repeatMode || REPEAT_MODE.OFF; + + // Restore current track if queue exists and index is valid + const currentQueue = this.shuffleActive ? this.shuffledQueue : this.queue; + if (this.currentQueueIndex >= 0 && this.currentQueueIndex < currentQueue.length) { + this.currentTrack = currentQueue[this.currentQueueIndex]; + + // Restore UI + const track = this.currentTrack; + const trackTitle = getTrackTitle(track); + const trackArtists = getTrackArtists(track); + + const coverEl = document.querySelector('.now-playing-bar .cover'); + const titleEl = document.querySelector('.now-playing-bar .title'); + const artistEl = document.querySelector('.now-playing-bar .artist'); + + if (coverEl) coverEl.src = this.api.getCoverUrl(track.album?.cover, '1280'); + if (titleEl) titleEl.textContent = trackTitle; + if (artistEl) artistEl.textContent = trackArtists; + document.title = `${trackTitle} • ${track.artist?.name || 'Unknown'}`; + + this.updatePlayingTrackIndicator(); + this.updateMediaSession(track); + } + } + } + + saveQueueState() { + queueManager.saveQueue({ + queue: this.queue, + shuffledQueue: this.shuffledQueue, + originalQueueBeforeShuffle: this.originalQueueBeforeShuffle, + currentQueueIndex: this.currentQueueIndex, + shuffleActive: this.shuffleActive, + repeatMode: this.repeatMode + }); } setupMediaSession() { @@ -105,6 +157,8 @@ export class Player { return; } + this.saveQueueState(); + const track = currentQueue[this.currentQueueIndex]; this.currentTrack = track; @@ -187,12 +241,23 @@ export class Player { } handlePlayPause() { - if (!this.audio.src) return; + if (!this.audio.src || this.audio.error) { + if (this.currentTrack) { + this.playTrackFromQueue(); + } + return; + } if (this.audio.paused) { - this.audio.play().catch(console.error); + this.audio.play().catch(e => { + console.error("Play failed, reloading track:", e); + if (this.currentTrack) { + this.playTrackFromQueue(); + } + }); } else { this.audio.pause(); + this.saveQueueState(); } } @@ -230,10 +295,12 @@ export class Player { this.preloadCache.clear(); this.preloadNextTracks(); + this.saveQueueState(); } toggleRepeat() { this.repeatMode = (this.repeatMode + 1) % 3; + this.saveQueueState(); return this.repeatMode; } @@ -242,6 +309,7 @@ export class Player { this.currentQueueIndex = startIndex; this.shuffleActive = false; this.preloadCache.clear(); + this.saveQueueState(); } addToQueue(track) { @@ -251,6 +319,7 @@ export class Player { this.currentQueueIndex = this.queue.length - 1; this.playTrackFromQueue(); } + this.saveQueueState(); } removeFromQueue(index) { @@ -271,6 +340,7 @@ export class Player { this.playTrackFromQueue(); } } + this.saveQueueState(); } moveInQueue(fromIndex, toIndex) { @@ -289,6 +359,7 @@ export class Player { } else if (fromIndex > this.currentQueueIndex && toIndex <= this.currentQueueIndex) { this.currentQueueIndex++; } + this.saveQueueState(); } getCurrentQueue() { diff --git a/js/storage.js b/js/storage.js index 58660e4..f78ae4d 100644 --- a/js/storage.js +++ b/js/storage.js @@ -326,6 +326,36 @@ export const lyricsSettings = { } }; +export const queueManager = { + STORAGE_KEY: 'monochrome-queue', + + getQueue() { + try { + const data = localStorage.getItem(this.STORAGE_KEY); + return data ? JSON.parse(data) : null; + } catch (e) { + return null; + } + }, + + saveQueue(queueState) { + try { + // Only save essential data to avoid quota limits + const minimalState = { + queue: queueState.queue, + shuffledQueue: queueState.shuffledQueue, + originalQueueBeforeShuffle: queueState.originalQueueBeforeShuffle, + currentQueueIndex: queueState.currentQueueIndex, + shuffleActive: queueState.shuffleActive, + repeatMode: queueState.repeatMode + }; + localStorage.setItem(this.STORAGE_KEY, JSON.stringify(minimalState)); + } catch (e) { + console.warn('Failed to save queue to localStorage:', e); + } + } +}; + // System theme listener if (typeof window !== 'undefined' && window.matchMedia) { window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => { diff --git a/sw.js b/sw.js index f91848d..bf80be7 100644 --- a/sw.js +++ b/sw.js @@ -1,5 +1,5 @@ //sw.js -const CACHE_NAME = 'monochrome-v1'; +const CACHE_NAME = 'monochrome-v2'; const urlsToCache = [ '/', '/index.html', @@ -15,6 +15,7 @@ const urlsToCache = [ ]; self.addEventListener('install', event => { + self.skipWaiting(); // Force activation event.waitUntil( caches.open(CACHE_NAME) .then(cache => cache.addAll(urlsToCache)) @@ -25,6 +26,11 @@ self.addEventListener('fetch', event => { event.respondWith( caches.match(event.request) .then(response => response || fetch(event.request)) + .catch(() => { + // Return 404 or handle offline fallback here if needed + // For now, just ensuring the promise doesn't reject uncaught + return new Response('Network error', { status: 408 }); + }) ); }); @@ -38,7 +44,7 @@ self.addEventListener('activate', event => { return caches.delete(cacheName); } }) - ); + ).then(() => self.clients.claim()); // Take control immediately }) ); }); \ No newline at end of file