mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 15:25:46 +00:00
9058abd340
Track is the app's normalized song model and QueueItemRef its thin queue identity -- consumed app-wide (queue, cover, context menus, sharing, lucky-mix, library browse), not a playback-internal detail. They lived in features/playback/store/playerStoreTypes, forcing ~115 core/feature files to reach into the playback feature for the central media type. Move the two interfaces to lib/media/trackTypes; playerStoreTypes keeps PlayerState and now imports them from lib. 115 importers repointed (mixed 'PlayerState, Track' lines split so PlayerState stays). Pure type move (erased at runtime). tsc 0, lint 0.
107 lines
4.2 KiB
TypeScript
107 lines
4.2 KiB
TypeScript
import type { QueueItemRef, Track } from '@/lib/media/trackTypes';
|
||
import type { PlayerState } from '@/features/playback/store/playerStoreTypes';
|
||
/** Hard cap on undo/redo depth — keeps memory bounded for very long sessions. */
|
||
export const QUEUE_UNDO_MAX = 32;
|
||
|
||
export type QueueUndoSnapshot = {
|
||
/** Thin queue refs (thin-state phase 4) — not hydrated `Track[]`, so 32
|
||
* snapshots of a 50k queue cost refs, not 32×50k full tracks. Rebuilt to a
|
||
* display `Track[]` through the resolver on restore. */
|
||
queueItems: QueueItemRef[];
|
||
queueIndex: number;
|
||
/** Kept full — one resolved playing track, restored to the engine on undo. */
|
||
currentTrack: Track | null;
|
||
/** Seconds — captured with the snapshot (older entries may omit). */
|
||
currentTime?: number;
|
||
progress?: number;
|
||
isPlaying?: boolean;
|
||
/** Main queue panel list `scrollTop` when the snapshot was taken. */
|
||
queueListScrollTop?: number;
|
||
/** Canonical playback-server identity at snapshot time. Restore uses this
|
||
* for any ref it has to prepend (e.g. a still-playing track absent from the
|
||
* snapshot's queue) so a mid-restore server switch can't bind the prepended
|
||
* ref to the wrong server (B1/H3). Older in-memory entries may omit it;
|
||
* callers fall back to the live `queueServerId` in that case. */
|
||
queueServerId?: string | null;
|
||
};
|
||
|
||
const queueUndoStack: QueueUndoSnapshot[] = [];
|
||
const queueRedoStack: QueueUndoSnapshot[] = [];
|
||
|
||
/** Test-only: clears module-scoped undo/redo stacks so each test starts clean. */
|
||
export function _resetQueueUndoStacksForTest(): void {
|
||
queueUndoStack.length = 0;
|
||
queueRedoStack.length = 0;
|
||
}
|
||
|
||
/** QueuePanel registers a reader so undo snapshots capture list scroll position. */
|
||
let queueListScrollTopReader: (() => number | undefined) | null = null;
|
||
|
||
export function registerQueueListScrollTopReader(reader: (() => number | undefined) | null): void {
|
||
queueListScrollTopReader = reader;
|
||
}
|
||
|
||
function readQueueListScrollTopForUndo(): number | undefined {
|
||
return queueListScrollTopReader?.() ?? undefined;
|
||
}
|
||
|
||
/** Set in applyQueueHistorySnapshot; QueuePanel consumes in useLayoutEffect after commit. */
|
||
let pendingQueueListScrollTop: number | undefined;
|
||
|
||
export function setPendingQueueListScrollTop(value: number): void {
|
||
pendingQueueListScrollTop = value;
|
||
}
|
||
|
||
export function consumePendingQueueListScrollTop(): number | undefined {
|
||
const v = pendingQueueListScrollTop;
|
||
pendingQueueListScrollTop = undefined;
|
||
return v;
|
||
}
|
||
|
||
export function queueUndoSnapshotFromState(s: PlayerState): QueueUndoSnapshot {
|
||
const scrollTop = readQueueListScrollTopForUndo();
|
||
return {
|
||
// Thin refs straight off the canonical list — 32 snapshots cost refs, not
|
||
// 32×50k full tracks (the undo "hidden multiplier" the thin-state plan kills).
|
||
queueItems: [...s.queueItems],
|
||
queueIndex: s.queueIndex,
|
||
currentTrack: s.currentTrack ? { ...s.currentTrack } : null,
|
||
currentTime: s.currentTime,
|
||
progress: s.progress,
|
||
isPlaying: s.isPlaying,
|
||
queueServerId: s.queueServerId,
|
||
...(scrollTop !== undefined ? { queueListScrollTop: scrollTop } : {}),
|
||
};
|
||
}
|
||
|
||
/**
|
||
* Snapshot the current state onto the undo stack and clear the redo stack —
|
||
* standard "new action invalidates redo history" behaviour. Called from every
|
||
* mutating queue action.
|
||
*/
|
||
export function pushQueueUndoFromGetter(get: () => PlayerState): void {
|
||
queueRedoStack.length = 0;
|
||
queueUndoStack.push(queueUndoSnapshotFromState(get()));
|
||
while (queueUndoStack.length > QUEUE_UNDO_MAX) queueUndoStack.shift();
|
||
}
|
||
|
||
export function popQueueUndoSnapshot(): QueueUndoSnapshot | undefined {
|
||
return queueUndoStack.pop();
|
||
}
|
||
|
||
export function popQueueRedoSnapshot(): QueueUndoSnapshot | undefined {
|
||
return queueRedoStack.pop();
|
||
}
|
||
|
||
/** Push a prebuilt snapshot onto the redo stack — used after an undo step. */
|
||
export function pushQueueRedoSnapshot(snap: QueueUndoSnapshot): void {
|
||
queueRedoStack.push(snap);
|
||
while (queueRedoStack.length > QUEUE_UNDO_MAX) queueRedoStack.shift();
|
||
}
|
||
|
||
/** Push a prebuilt snapshot onto the undo stack — used after a redo step. */
|
||
export function pushQueueUndoSnapshot(snap: QueueUndoSnapshot): void {
|
||
queueUndoStack.push(snap);
|
||
while (queueUndoStack.length > QUEUE_UNDO_MAX) queueUndoStack.shift();
|
||
}
|