Files
Psychotoxical-psysonic/src/hooks/usePlayQueueSyncLedState.ts
T

122 lines
4.1 KiB
TypeScript

import { useCallback, useEffect, useMemo, useState, useSyncExternalStore } from 'react';
import { useTranslation } from 'react-i18next';
import type { ConnectionStatus } from '../hooks/useConnectionStatus';
import { pullPlayQueueFromActiveServer } from '../store/applyServerPlayQueue';
import { useAuthStore } from '../store/authStore';
import { useOrbitStore } from '../store/orbitStore';
import { usePlayerStore } from '../store/playerStore';
import { getPlaybackServerId, queueIsMultiServer } from '../utils/playback/playbackServer';
import {
getIdleQueuePullSuspendedSnapshot,
subscribeIdleQueuePullSuspended,
} from '../store/queuePlaybackIdle';
import { clearQueueHandoffPending, isQueueHandoffPending } from '../store/queueSyncUiState';
import { showToast } from '../utils/ui/toast';
export function usePlayQueueSyncLedState(status: ConnectionStatus) {
const { t } = useTranslation();
const activeServerId = useAuthStore(s => s.activeServerId);
const orbitRole = useOrbitStore(s => s.role);
const isPlaying = usePlayerStore(s => s.isPlaying);
const currentRadio = usePlayerStore(s => s.currentRadio);
const [pullInFlight, setPullInFlight] = useState(false);
const idlePullSuspended = useSyncExternalStore(
subscribeIdleQueuePullSuspended,
getIdleQueuePullSuspendedSnapshot,
);
const queueItems = usePlayerStore(s => s.queueItems);
const queueIndex = usePlayerStore(s => s.queueIndex);
const currentTrackId = usePlayerStore(s => s.currentTrack?.id);
const playbackServerId = useMemo(
() => getPlaybackServerId(),
[activeServerId, queueItems, queueIndex, currentTrackId],
);
useEffect(() => {
if (activeServerId && playbackServerId && activeServerId === playbackServerId) {
clearQueueHandoffPending();
}
}, [activeServerId, playbackServerId]);
const autoSyncContext = canAutoIdlePlayQueuePull(status, orbitRole);
const localQueueSyncPaused = autoSyncContext && idlePullSuspended && !isPlaying;
const needsQueuePull = status === 'connected'
&& Boolean(activeServerId)
&& (
(Boolean(playbackServerId) && activeServerId !== playbackServerId)
|| isQueueHandoffPending()
|| localQueueSyncPaused
);
const queueHandoffReason = status === 'connected'
&& Boolean(activeServerId)
&& Boolean(playbackServerId)
&& activeServerId !== playbackServerId;
const ledVariant = status === 'checking'
? 'checking'
: status === 'disconnected'
? 'disconnected'
: needsQueuePull
? 'queue-handoff'
: 'connected';
const pullFromActiveServer = useCallback(async () => {
if (status !== 'connected' || pullInFlight) return;
if (orbitRole === 'host' || orbitRole === 'guest') return;
if (currentRadio) return;
setPullInFlight(true);
try {
const result = await pullPlayQueueFromActiveServer();
switch (result) {
case 'noop':
showToast(t('connection.queueSynced'), 2500, 'info');
break;
case 'empty':
showToast(t('connection.queuePullEmpty'), 4000, 'info');
break;
case 'applied':
showToast(t('connection.queuePullSuccess'), 3000, 'info');
break;
case 'error':
showToast(t('connection.queuePullFailed'), 5000, 'error');
break;
default:
break;
}
} finally {
setPullInFlight(false);
}
}, [currentRadio, orbitRole, pullInFlight, status, t]);
const syncRingVisible = status === 'connected' && (needsQueuePull || pullInFlight);
return {
ledVariant,
needsQueuePull,
localQueueSyncPaused,
queueHandoffReason,
pullInFlight,
syncRingVisible,
pullFromActiveServer,
};
}
export function canAutoIdlePlayQueuePull(
status: ConnectionStatus,
orbitRole: string | null,
): boolean {
if (status !== 'connected') return false;
if (orbitRole === 'host' || orbitRole === 'guest') return false;
if (usePlayerStore.getState().currentRadio) return false;
if (queueIsMultiServer()) return false;
const activeId = useAuthStore.getState().activeServerId;
const playbackId = getPlaybackServerId();
if (!activeId || !playbackId || activeId !== playbackId) return false;
return true;
}