mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 22:45:41 +00:00
9b03949f34
* feat(orbit): mirror host track-transition settings into the session Each client previously used its own local crossfade / gapless / AutoDJ settings, so guests blended tracks differently from the host and drifted out of sync — the exact drift the Catch-Up button exists to paper over. Add an optional OrbitSettings.transitions (crossfade enabled/secs/trim, AutoDJ smooth-skip, gapless), additive on the wire so older sessions simply omit it. The host seeds its own prefs at start and refreshes them every tick, so a mid-session change propagates. Guests snapshot their own prefs on join, adopt the host's for the session (idempotent — no setState churn / audio-sync re-fire when unchanged), and restore their own on leave. Applying via authStore.setState reuses the existing authSyncListener path to the Rust engine, so no new IPC. Without the Hot cache a guest's AutoDJ blend degrades gracefully via the existing byte-preload fallback. Add a bridge module with unit tests for read/apply/save/restore. * feat(orbit): lock guest transition controls during a session Since a guest now mirrors the host's track-transition settings (re-applied every read tick), their own controls would visibly fight the sync. Disable them while a guest is in a live session and explain why. Settings -> Audio -> Track transitions greys out the mode picker, seconds slider and smooth-skip toggle with a "controlled by the Orbit host" note; the queue toolbar's Gapless / Crossfade / AutoDJ quick-toggles get the same disabled state and tooltip. Host controls are untouched. New i18n key added across all locales. * docs(changelog): add Orbit transition sync (1158)
97 lines
2.9 KiB
TypeScript
97 lines
2.9 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import type { OrbitTransitionSettings } from '../../api/orbit';
|
|
|
|
const { store, setState } = vi.hoisted(() => {
|
|
const store = {
|
|
state: {
|
|
crossfadeEnabled: false,
|
|
crossfadeSecs: 3,
|
|
crossfadeTrimSilence: false,
|
|
autodjSmoothSkip: true,
|
|
gaplessEnabled: false,
|
|
} as Record<string, unknown>,
|
|
};
|
|
const setState = vi.fn((patch: Record<string, unknown>) => {
|
|
store.state = { ...store.state, ...patch };
|
|
});
|
|
return { store, setState };
|
|
});
|
|
|
|
vi.mock('../../store/authStore', () => ({
|
|
useAuthStore: { getState: () => store.state, setState },
|
|
}));
|
|
|
|
import {
|
|
applyOrbitTransitionSettings,
|
|
hasGuestTransitionsSnapshot,
|
|
readOrbitTransitionSettings,
|
|
restoreGuestTransitions,
|
|
saveGuestTransitionsOnce,
|
|
} from './transitions';
|
|
|
|
const GUEST_OWN: OrbitTransitionSettings = {
|
|
crossfadeEnabled: false,
|
|
crossfadeSecs: 3,
|
|
crossfadeTrimSilence: false,
|
|
autodjSmoothSkip: true,
|
|
gaplessEnabled: false,
|
|
};
|
|
const HOST: OrbitTransitionSettings = {
|
|
crossfadeEnabled: true,
|
|
crossfadeSecs: 8,
|
|
crossfadeTrimSilence: true,
|
|
autodjSmoothSkip: false,
|
|
gaplessEnabled: false,
|
|
};
|
|
|
|
beforeEach(() => {
|
|
restoreGuestTransitions(); // clear any leftover snapshot from a prior test
|
|
store.state = { ...GUEST_OWN };
|
|
setState.mockClear();
|
|
});
|
|
|
|
describe('read/apply transition settings', () => {
|
|
it('reads the five transition fields from the store', () => {
|
|
expect(readOrbitTransitionSettings()).toEqual(GUEST_OWN);
|
|
});
|
|
|
|
it('applies a set and is a no-op when already in sync', () => {
|
|
applyOrbitTransitionSettings(HOST);
|
|
expect(setState).toHaveBeenCalledTimes(1);
|
|
expect(readOrbitTransitionSettings()).toEqual(HOST);
|
|
|
|
// Same values again → must not churn setState (would re-fire audio sync).
|
|
applyOrbitTransitionSettings(HOST);
|
|
expect(setState).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|
|
|
|
describe('guest snapshot save/restore', () => {
|
|
it('snapshots the user own settings, adopts the host, then restores on leave', () => {
|
|
saveGuestTransitionsOnce();
|
|
expect(hasGuestTransitionsSnapshot()).toBe(true);
|
|
|
|
applyOrbitTransitionSettings(HOST);
|
|
expect(readOrbitTransitionSettings()).toEqual(HOST);
|
|
|
|
restoreGuestTransitions();
|
|
expect(readOrbitTransitionSettings()).toEqual(GUEST_OWN);
|
|
expect(hasGuestTransitionsSnapshot()).toBe(false);
|
|
});
|
|
|
|
it('save is idempotent — a second save never captures host-applied values', () => {
|
|
saveGuestTransitionsOnce(); // snapshots the user's own
|
|
applyOrbitTransitionSettings(HOST);
|
|
saveGuestTransitionsOnce(); // no-op: snapshot already held
|
|
restoreGuestTransitions();
|
|
expect(readOrbitTransitionSettings()).toEqual(GUEST_OWN);
|
|
});
|
|
|
|
it('restore is a no-op when nothing was saved', () => {
|
|
store.state = { ...HOST };
|
|
restoreGuestTransitions();
|
|
expect(readOrbitTransitionSettings()).toEqual(HOST);
|
|
});
|
|
});
|