mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 06:25:41 +00:00
fix(player): cross-device resume — push queue position on pause + all exit paths
Server queue position was effectively pinned at 0 because syncQueueToServer only fired on track-change, seek, and queue edits (with a 5s debounce). A user listening for minutes without seeking would close the app and the server still held position=0, so a second device restarted the track. Three pieces: - 15s heartbeat from audio:progress flushes the live position while playing. - pause() flushes immediately so a quick close after pause is captured. - Tray "Exit" and macOS Cmd+Q used to call app.exit(0) directly, bypassing the JS close handler. Both now emit a new app:force-quit event that shares the same flush + Orbit-teardown path as window:close-requested, and exit_app stops the audio engine on its way out. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+33
-17
@@ -93,7 +93,7 @@ import { initHotCachePrefetch } from './hotCachePrefetch';
|
||||
import i18n from './i18n';
|
||||
import { playByOpaqueId } from './utils/playByOpaqueId';
|
||||
import { switchActiveServer } from './utils/switchActiveServer';
|
||||
import { usePlayerStore, initAudioListeners, songToTrack, shuffleArray } from './store/playerStore';
|
||||
import { usePlayerStore, initAudioListeners, songToTrack, shuffleArray, flushPlayQueuePosition } from './store/playerStore';
|
||||
import { useThemeStore } from './store/themeStore';
|
||||
import { useThemeScheduler } from './hooks/useThemeScheduler';
|
||||
import { useFontStore } from './store/fontStore';
|
||||
@@ -961,31 +961,47 @@ function TauriEventBridge() {
|
||||
unlisten.push(u);
|
||||
}
|
||||
|
||||
// window:close-requested is emitted by Rust (prevent_close + emit).
|
||||
// JS decides: minimize to tray or exit, based on user setting.
|
||||
// Shared exit path: flush play-queue position so other devices can
|
||||
// resume from where we left off, tear down any active Orbit session,
|
||||
// then ask Rust to exit. Each step is capped at 1500 ms so a slow
|
||||
// server can't keep the app hanging on quit; the playback heartbeat
|
||||
// is the safety net for anything that didn't make it out in time.
|
||||
const performExit = async () => {
|
||||
await Promise.race([
|
||||
flushPlayQueuePosition(),
|
||||
new Promise(r => setTimeout(r, 1500)),
|
||||
]);
|
||||
const role = useOrbitStore.getState().role;
|
||||
if (role === 'host' || role === 'guest') {
|
||||
const teardown = role === 'host' ? endOrbitSession() : leaveOrbitSession();
|
||||
await Promise.race([
|
||||
teardown.catch(() => {}),
|
||||
new Promise(r => setTimeout(r, 1500)),
|
||||
]);
|
||||
}
|
||||
await invoke('exit_app');
|
||||
};
|
||||
|
||||
// window:close-requested is emitted by Rust (prevent_close + emit) on
|
||||
// the X-button. JS decides: minimize to tray or exit.
|
||||
const u = await listen('window:close-requested', async () => {
|
||||
if (useAuthStore.getState().minimizeToTray) {
|
||||
await invoke('pause_rendering').catch(() => {});
|
||||
await getCurrentWindow().hide();
|
||||
} else {
|
||||
// Clean up an active Orbit session before we go down — leaving
|
||||
// the session playlists behind would litter the server. Capped at
|
||||
// 1500 ms so a slow server can't keep the app hanging on quit; the
|
||||
// next launch's orphan sweep is the safety net for anything that
|
||||
// didn't make it out in time.
|
||||
const role = useOrbitStore.getState().role;
|
||||
if (role === 'host' || role === 'guest') {
|
||||
const teardown = role === 'host' ? endOrbitSession() : leaveOrbitSession();
|
||||
await Promise.race([
|
||||
teardown.catch(() => {}),
|
||||
new Promise(r => setTimeout(r, 1500)),
|
||||
]);
|
||||
}
|
||||
await invoke('exit_app');
|
||||
await performExit();
|
||||
}
|
||||
});
|
||||
if (cancelled) { u(); return; }
|
||||
unlisten.push(u);
|
||||
|
||||
// app:force-quit bypasses the minimize-to-tray decision — used by the
|
||||
// tray "Exit" menu item and the macOS red close button.
|
||||
const fq = await listen('app:force-quit', async () => {
|
||||
await performExit();
|
||||
});
|
||||
if (cancelled) { fq(); return; }
|
||||
unlisten.push(fq);
|
||||
};
|
||||
|
||||
setup();
|
||||
|
||||
Reference in New Issue
Block a user