mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 15:25:46 +00:00
refactor(queue): co-locate QueuePanel UI into features/queue
Extract the queue UI (QueuePanel + queuePanel/* components + useQueue* hooks)
into features/queue/ with a barrel. This is the chosen playback/queue boundary:
queue = the QueuePanel UI that CONSUMES the playback store; queue STATE and the
audio engine stay in store/ (playback-core, moved separately). Verified
one-way: no playback-core file imports the queue UI back (only doc comments
mention it).
Excluded as NOT queue-UI: useIdlePlayQueuePull (AppShell) and
usePlayQueueSyncLedState (ConnectionIndicator) are queue-SYNC orchestration,
not panel UI — left in hooks/ to move with playback.
Pure move. One test fix: QueuePanel.test.tsx reads its subject via a hardcoded
readFileSync('src/components/QueuePanel.tsx') path (architecture-pin grep for
forbidden HTML5 DnD) — repointed to the new location (the move tool can't
rewrite non-import string paths). tsc 0, lint 0/0, suite 319/2353 green.
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
import { libraryGetFacts, libraryGetTrack } from '@/lib/api/library';
|
||||
import { usePlaybackServerId } from '@/hooks/usePlaybackServerId';
|
||||
import { useLibraryIndexStore } from '@/store/libraryIndexStore';
|
||||
import {
|
||||
enrichmentDisplayComplete,
|
||||
OXIMEDIA_MOOD_UI_ENABLED,
|
||||
parseTrackEnrichmentFacts,
|
||||
type ParsedTrackEnrichment,
|
||||
} from '@/utils/library/trackEnrichment';
|
||||
import { libraryIsReady } from '@/utils/library/libraryReady';
|
||||
import { normalizeAnalysisTrackId } from '@/utils/playback/queueIdentity';
|
||||
|
||||
const EMPTY: ParsedTrackEnrichment = {
|
||||
serverBpm: null,
|
||||
measuredBpm: null,
|
||||
moodLabels: [],
|
||||
};
|
||||
|
||||
/** Enrichment may finish several seconds after CPU seed / playback start. */
|
||||
const REFETCH_MS = [3_000, 8_000, 15_000, 30_000, 60_000] as const;
|
||||
|
||||
const ENRICHMENT_FACT_KINDS = OXIMEDIA_MOOD_UI_ENABLED
|
||||
? (['bpm', 'moods', 'mood_tag', 'mood_labels', 'valence', 'arousal'] as const)
|
||||
: (['bpm'] as const);
|
||||
|
||||
/**
|
||||
* Loads server BPM + oximedia mood facts for the queue "now playing" block.
|
||||
* Uses the playback server id (queue scope), not the browsed server.
|
||||
*/
|
||||
export function useQueueTrackEnrichment(trackId: string | undefined): ParsedTrackEnrichment {
|
||||
const serverId = usePlaybackServerId();
|
||||
const indexEnabled = useLibraryIndexStore(s =>
|
||||
serverId ? s.isIndexEnabled(serverId) : false,
|
||||
);
|
||||
const [data, setData] = useState<ParsedTrackEnrichment>(EMPTY);
|
||||
const [refreshNonce, setRefreshNonce] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (!serverId || !trackId || !indexEnabled) {
|
||||
// React Compiler set-state-in-effect rule: state set from a timer/animation callback.
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setData(EMPTY);
|
||||
return;
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
const timers: ReturnType<typeof setTimeout>[] = [];
|
||||
|
||||
const load = async () => {
|
||||
if (!(await libraryIsReady(serverId))) return;
|
||||
try {
|
||||
const [track, facts] = await Promise.all([
|
||||
libraryGetTrack(serverId, trackId),
|
||||
libraryGetFacts(serverId, trackId, [...ENRICHMENT_FACT_KINDS]),
|
||||
]);
|
||||
if (cancelled) return;
|
||||
const parsed = parseTrackEnrichmentFacts(facts, track?.bpm ?? null);
|
||||
setData(parsed);
|
||||
if (enrichmentDisplayComplete(parsed)) {
|
||||
for (const id of timers) clearTimeout(id);
|
||||
timers.length = 0;
|
||||
}
|
||||
} catch {
|
||||
if (!cancelled) setData(EMPTY);
|
||||
}
|
||||
};
|
||||
|
||||
void load();
|
||||
for (const ms of REFETCH_MS) {
|
||||
timers.push(setTimeout(() => { void load(); }, ms));
|
||||
}
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
for (const id of timers) clearTimeout(id);
|
||||
};
|
||||
}, [serverId, trackId, indexEnabled, refreshNonce]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!serverId || !trackId || !indexEnabled) return;
|
||||
|
||||
let unlisten: (() => void) | undefined;
|
||||
void listen<{ trackId: string; serverId: string }>('analysis:enrichment-updated', ({ payload }) => {
|
||||
if (!payload?.trackId) return;
|
||||
const eventTrackId = normalizeAnalysisTrackId(payload.trackId);
|
||||
const currentId = normalizeAnalysisTrackId(trackId);
|
||||
if (!eventTrackId || eventTrackId !== currentId) return;
|
||||
if (payload.serverId && payload.serverId !== serverId) return;
|
||||
setRefreshNonce(n => n + 1);
|
||||
}).then(fn => {
|
||||
unlisten = fn;
|
||||
});
|
||||
|
||||
return () => {
|
||||
unlisten?.();
|
||||
};
|
||||
}, [serverId, trackId, indexEnabled]);
|
||||
|
||||
return data;
|
||||
}
|
||||
Reference in New Issue
Block a user