db1168a46c
Отдельное окно countdown с «Показать игрокам»; на превью — move/resize/rotate, на презентации тот же layout. Co-authored-by: Cursor <cursoragent@cursor.com>
132 lines
3.8 KiB
TypeScript
132 lines
3.8 KiB
TypeScript
import {
|
|
clampSessionTimerDurationMs,
|
|
clampSessionTimerLayout,
|
|
createInitialSessionTimerState,
|
|
getSessionTimerRemainingMs,
|
|
type SessionTimerEvent,
|
|
type SessionTimerState,
|
|
} from '../../shared/types/sessionTimer';
|
|
|
|
export class TimerSessionStore {
|
|
private state: SessionTimerState = createInitialSessionTimerState();
|
|
|
|
getState(): SessionTimerState {
|
|
return this.state;
|
|
}
|
|
|
|
clear(): SessionTimerState {
|
|
this.state = createInitialSessionTimerState();
|
|
return this.state;
|
|
}
|
|
|
|
dispatch(event: SessionTimerEvent, now = Date.now()): SessionTimerState {
|
|
switch (event.kind) {
|
|
case 'setDuration': {
|
|
const durationMs = clampSessionTimerDurationMs(event.durationMs);
|
|
if (this.state.running) {
|
|
const remaining = getSessionTimerRemainingMs(this.state, now);
|
|
this.state = {
|
|
...this.state,
|
|
revision: this.state.revision + 1,
|
|
durationMs,
|
|
remainingMs: Math.min(remaining, durationMs),
|
|
endsAt: now + Math.min(remaining, durationMs),
|
|
};
|
|
} else {
|
|
this.state = {
|
|
...this.state,
|
|
revision: this.state.revision + 1,
|
|
durationMs,
|
|
remainingMs: durationMs,
|
|
endsAt: null,
|
|
running: false,
|
|
};
|
|
}
|
|
return this.state;
|
|
}
|
|
case 'start': {
|
|
const remaining = getSessionTimerRemainingMs(this.state, now);
|
|
if (remaining <= 0) {
|
|
// Restart from full duration if already at zero.
|
|
const durationMs = this.state.durationMs;
|
|
this.state = {
|
|
...this.state,
|
|
revision: this.state.revision + 1,
|
|
remainingMs: durationMs,
|
|
running: true,
|
|
endsAt: now + durationMs,
|
|
};
|
|
return this.state;
|
|
}
|
|
this.state = {
|
|
...this.state,
|
|
revision: this.state.revision + 1,
|
|
running: true,
|
|
endsAt: now + remaining,
|
|
remainingMs: remaining,
|
|
};
|
|
return this.state;
|
|
}
|
|
case 'pause': {
|
|
if (!this.state.running) return this.state;
|
|
const remaining = getSessionTimerRemainingMs(this.state, now);
|
|
this.state = {
|
|
...this.state,
|
|
revision: this.state.revision + 1,
|
|
running: false,
|
|
endsAt: null,
|
|
remainingMs: remaining,
|
|
};
|
|
return this.state;
|
|
}
|
|
case 'reset': {
|
|
this.state = {
|
|
...this.state,
|
|
revision: this.state.revision + 1,
|
|
running: false,
|
|
endsAt: null,
|
|
remainingMs: this.state.durationMs,
|
|
};
|
|
return this.state;
|
|
}
|
|
case 'setVisibleToPlayers': {
|
|
if (this.state.visibleToPlayers === event.visible) return this.state;
|
|
this.state = {
|
|
...this.state,
|
|
revision: this.state.revision + 1,
|
|
visibleToPlayers: event.visible,
|
|
};
|
|
return this.state;
|
|
}
|
|
case 'layout.set': {
|
|
const layout = clampSessionTimerLayout(event.layout);
|
|
this.state = {
|
|
...this.state,
|
|
revision: this.state.revision + 1,
|
|
layout,
|
|
};
|
|
return this.state;
|
|
}
|
|
default: {
|
|
const _exhaustive: never = event;
|
|
return _exhaustive;
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Когда running и время вышло — зафиксировать 00:00 (вызывается из UI/main по желанию). */
|
|
settleIfExpired(now = Date.now()): SessionTimerState {
|
|
if (!this.state.running) return this.state;
|
|
const remaining = getSessionTimerRemainingMs(this.state, now);
|
|
if (remaining > 0) return this.state;
|
|
this.state = {
|
|
...this.state,
|
|
revision: this.state.revision + 1,
|
|
running: false,
|
|
endsAt: null,
|
|
remainingMs: 0,
|
|
};
|
|
return this.state;
|
|
}
|
|
}
|