Merge pull request #11 from JulienMaille/persist
feat: implement queue persistence and improve playback restoration
This commit is contained in:
commit
f68586f456
4 changed files with 126 additions and 4 deletions
15
js/events.js
15
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);
|
||||
|
|
|
|||
75
js/player.js
75
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() {
|
||||
|
|
|
|||
|
|
@ -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
10
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
|
||||
})
|
||||
);
|
||||
});
|
||||
Loading…
Reference in a new issue