Merge pull request #11 from JulienMaille/persist

feat: implement queue persistence and improve playback restoration
This commit is contained in:
Samidy 2025-12-23 20:38:21 -08:00 committed by GitHub
commit f68586f456
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 126 additions and 4 deletions

View file

@ -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);

View file

@ -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() {

View file

@ -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 => {

10
sw.js
View file

@ -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
})
);
});