feat: окно инструкций, свёрнутая история и обновление спонсоров

Инструкции вынесены в отдельное окно Electron; токены игроков не пропадают при смене сцены; предпросмотр и плейлисты адаптированы; история свёрнута по умолчанию.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Ivan Fontosh
2026-09-17 06:21:42 +08:00
parent 712086ddca
commit a29c72b307
24 changed files with 506 additions and 290 deletions
@@ -81,29 +81,27 @@ export function useScenePlayerTokensSession(): [
visible: false,
byPlayerId: {},
});
const localRevRef = useRef(0);
/** Только revision с main. Оптимистичные move/setSize не должны глушить смену сцены. */
const serverRevRef = useRef(0);
useEffect(() => {
const applyServer = (next: ScenePlayerTokensSessionState) => {
if (next.revision < serverRevRef.current) return;
serverRevRef.current = next.revision;
setState(next);
};
void api.invoke(ipcChannels.scenePlayerTokensSession.getState, {}).then((res) => {
if (res.state.revision < localRevRef.current) return;
localRevRef.current = res.state.revision;
setState(res.state);
applyServer(res.state);
});
return api.on(ipcChannels.scenePlayerTokensSession.stateChanged, ({ state: next }) => {
if (next.revision < localRevRef.current) return;
localRevRef.current = next.revision;
setState(next);
applyServer(next);
});
}, [api]);
const dispatch = useCallback(
(event: ScenePlayerTokensSessionEvent) => {
if (event.kind !== 'seedBottom' && event.kind !== 'show') {
setState((prev) => {
const next = applyEvent(prev, event);
localRevRef.current = Math.max(localRevRef.current, next.revision);
return next;
});
setState((prev) => applyEvent(prev, event));
}
void api.invoke(ipcChannels.scenePlayerTokensSession.dispatch, { event });
},