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 shuffleBtn = document.getElementById('shuffle-btn');
|
||||||
const repeatBtn = document.getElementById('repeat-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', () => {
|
audioPlayer.addEventListener('play', () => {
|
||||||
if (scrobbler.isAuthenticated() && lastFMStorage.isEnabled() && player.currentTrack) {
|
if (scrobbler.isAuthenticated() && lastFMStorage.isEnabled() && player.currentTrack) {
|
||||||
scrobbler.updateNowPlaying(player.currentTrack);
|
scrobbler.updateNowPlaying(player.currentTrack);
|
||||||
|
|
|
||||||
75
js/player.js
75
js/player.js
|
|
@ -1,5 +1,6 @@
|
||||||
//js/player.js
|
//js/player.js
|
||||||
import { REPEAT_MODE, formatTime, getTrackArtists, getTrackTitle} from './utils.js';
|
import { REPEAT_MODE, formatTime, getTrackArtists, getTrackTitle} from './utils.js';
|
||||||
|
import { queueManager } from './storage.js';
|
||||||
|
|
||||||
export class Player {
|
export class Player {
|
||||||
constructor(audioElement, api, quality = 'LOSSLESS') {
|
constructor(audioElement, api, quality = 'LOSSLESS') {
|
||||||
|
|
@ -16,7 +17,58 @@ export class Player {
|
||||||
this.preloadAbortController = null;
|
this.preloadAbortController = null;
|
||||||
this.currentTrack = null;
|
this.currentTrack = null;
|
||||||
|
|
||||||
|
this.loadQueueState();
|
||||||
this.setupMediaSession();
|
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() {
|
setupMediaSession() {
|
||||||
|
|
@ -105,6 +157,8 @@ export class Player {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.saveQueueState();
|
||||||
|
|
||||||
const track = currentQueue[this.currentQueueIndex];
|
const track = currentQueue[this.currentQueueIndex];
|
||||||
this.currentTrack = track;
|
this.currentTrack = track;
|
||||||
|
|
||||||
|
|
@ -187,12 +241,23 @@ export class Player {
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePlayPause() {
|
handlePlayPause() {
|
||||||
if (!this.audio.src) return;
|
if (!this.audio.src || this.audio.error) {
|
||||||
|
if (this.currentTrack) {
|
||||||
|
this.playTrackFromQueue();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.audio.paused) {
|
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 {
|
} else {
|
||||||
this.audio.pause();
|
this.audio.pause();
|
||||||
|
this.saveQueueState();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -230,10 +295,12 @@ export class Player {
|
||||||
|
|
||||||
this.preloadCache.clear();
|
this.preloadCache.clear();
|
||||||
this.preloadNextTracks();
|
this.preloadNextTracks();
|
||||||
|
this.saveQueueState();
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleRepeat() {
|
toggleRepeat() {
|
||||||
this.repeatMode = (this.repeatMode + 1) % 3;
|
this.repeatMode = (this.repeatMode + 1) % 3;
|
||||||
|
this.saveQueueState();
|
||||||
return this.repeatMode;
|
return this.repeatMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -242,6 +309,7 @@ export class Player {
|
||||||
this.currentQueueIndex = startIndex;
|
this.currentQueueIndex = startIndex;
|
||||||
this.shuffleActive = false;
|
this.shuffleActive = false;
|
||||||
this.preloadCache.clear();
|
this.preloadCache.clear();
|
||||||
|
this.saveQueueState();
|
||||||
}
|
}
|
||||||
|
|
||||||
addToQueue(track) {
|
addToQueue(track) {
|
||||||
|
|
@ -251,6 +319,7 @@ export class Player {
|
||||||
this.currentQueueIndex = this.queue.length - 1;
|
this.currentQueueIndex = this.queue.length - 1;
|
||||||
this.playTrackFromQueue();
|
this.playTrackFromQueue();
|
||||||
}
|
}
|
||||||
|
this.saveQueueState();
|
||||||
}
|
}
|
||||||
|
|
||||||
removeFromQueue(index) {
|
removeFromQueue(index) {
|
||||||
|
|
@ -271,6 +340,7 @@ export class Player {
|
||||||
this.playTrackFromQueue();
|
this.playTrackFromQueue();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this.saveQueueState();
|
||||||
}
|
}
|
||||||
|
|
||||||
moveInQueue(fromIndex, toIndex) {
|
moveInQueue(fromIndex, toIndex) {
|
||||||
|
|
@ -289,6 +359,7 @@ export class Player {
|
||||||
} else if (fromIndex > this.currentQueueIndex && toIndex <= this.currentQueueIndex) {
|
} else if (fromIndex > this.currentQueueIndex && toIndex <= this.currentQueueIndex) {
|
||||||
this.currentQueueIndex++;
|
this.currentQueueIndex++;
|
||||||
}
|
}
|
||||||
|
this.saveQueueState();
|
||||||
}
|
}
|
||||||
|
|
||||||
getCurrentQueue() {
|
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
|
// System theme listener
|
||||||
if (typeof window !== 'undefined' && window.matchMedia) {
|
if (typeof window !== 'undefined' && window.matchMedia) {
|
||||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
|
||||||
|
|
|
||||||
10
sw.js
10
sw.js
|
|
@ -1,5 +1,5 @@
|
||||||
//sw.js
|
//sw.js
|
||||||
const CACHE_NAME = 'monochrome-v1';
|
const CACHE_NAME = 'monochrome-v2';
|
||||||
const urlsToCache = [
|
const urlsToCache = [
|
||||||
'/',
|
'/',
|
||||||
'/index.html',
|
'/index.html',
|
||||||
|
|
@ -15,6 +15,7 @@ const urlsToCache = [
|
||||||
];
|
];
|
||||||
|
|
||||||
self.addEventListener('install', event => {
|
self.addEventListener('install', event => {
|
||||||
|
self.skipWaiting(); // Force activation
|
||||||
event.waitUntil(
|
event.waitUntil(
|
||||||
caches.open(CACHE_NAME)
|
caches.open(CACHE_NAME)
|
||||||
.then(cache => cache.addAll(urlsToCache))
|
.then(cache => cache.addAll(urlsToCache))
|
||||||
|
|
@ -25,6 +26,11 @@ self.addEventListener('fetch', event => {
|
||||||
event.respondWith(
|
event.respondWith(
|
||||||
caches.match(event.request)
|
caches.match(event.request)
|
||||||
.then(response => response || fetch(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);
|
return caches.delete(cacheName);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
).then(() => self.clients.claim()); // Take control immediately
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
Loading…
Reference in a new issue