diff --git a/src/components/ScreenPage.vue b/src/components/ScreenPage.vue index 612b680..a8358a8 100644 --- a/src/components/ScreenPage.vue +++ b/src/components/ScreenPage.vue @@ -29,10 +29,14 @@ const mediaError = ref('') const loading = ref(true) const binding = ref(false) const bindSuccess = ref(false) +const timerNow = ref(Date.now()) +const timerRemainingAtSync = ref(0) +const timerSyncedAt = ref(Date.now()) const introAudioElement = ref() const conclusionVideoElement = ref() let appliedCommandVersion = 0 let pollTimer: number | null = null +let timerTick: number | null = null const bindingFlow = computed(() => Boolean(route.query.screenId && route.query.accessToken)) const qrValue = computed(() => { @@ -49,6 +53,15 @@ const gameOptions = computed(() => })), ) const screenMode = computed(() => state.value?.mode || 'SCREEN_MODE_COVER') +const timerRemainingSeconds = computed(() => { + if (state.value?.timerState !== 'SCREEN_TIMER_STATE_RUNNING') return timerRemainingAtSync.value + return Math.max(0, timerRemainingAtSync.value - Math.floor((timerNow.value - timerSyncedAt.value) / 1000)) +}) +const timerDisplay = computed(() => { + const minutes = Math.floor(timerRemainingSeconds.value / 60) + const seconds = timerRemainingSeconds.value % 60 + return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}` +}) const introAudio = computed(() => applicationFileURL(state.value?.introAudio)) const conclusionVideo = computed(() => applicationFileURL(state.value?.conclusionVideo)) const screenBackground = computed(() => { @@ -60,6 +73,11 @@ function saveSession() { sessionStorage.setItem(sessionKey, JSON.stringify({ screenId: screenId.value, accessToken: accessToken.value })) } +function syncTimer(value: ScreenState | undefined) { + timerRemainingAtSync.value = Math.max(0, Math.floor(value?.timerRemainingSeconds || 0)) + timerSyncedAt.value = Date.now() +} + function readSession() { try { const saved = JSON.parse(sessionStorage.getItem(sessionKey) || '{}') as { @@ -89,6 +107,7 @@ async function loadState() { const res = await client.GetScreenState({ screenId: screenId.value, accessToken: accessToken.value }) if (res.error) throw new Error(res.error) state.value = res.state + syncTimer(state.value) if (state.value?.gameId && state.value.mode === 'SCREEN_MODE_RESULTS') await loadResults() } catch (err) { message.error(err instanceof Error ? err.message : 'Не удалось получить состояние экрана') @@ -191,6 +210,9 @@ onMounted(async () => { if (!screenId.value || !accessToken.value) await createSession() await loadState() pollTimer = window.setInterval(loadState, 1000) + timerTick = window.setInterval(() => { + timerNow.value = Date.now() + }, 250) } catch (err) { message.error(err instanceof Error ? err.message : 'Не удалось подготовить экран') } finally { @@ -200,6 +222,7 @@ onMounted(async () => { onUnmounted(() => { if (pollTimer != null) window.clearInterval(pollTimer) + if (timerTick != null) window.clearInterval(timerTick) if (resultsScrollTimer != null) window.clearInterval(resultsScrollTimer) }) @@ -244,6 +267,10 @@ onUnmounted(() => {
Видео закрывашки не задано
Успехов
Спасибо за игру!
+
+

{{ state.timerLabel || 'Перерыв' }}

+
{{ timerDisplay }}
+

Результаты

@@ -382,6 +409,27 @@ onUnmounted(() => { font-weight: 700; } +.timer-screen { + align-items: center; + display: flex; + flex-direction: column; + gap: 24px; + justify-content: center; + max-width: 90vw; +} + +.timer-label { + font-size: clamp(32px, 6vw, 88px); + margin: 0; +} + +.timer-value { + font-size: clamp(96px, 24vw, 360px); + font-variant-numeric: tabular-nums; + font-weight: 700; + line-height: 0.9; +} + .screen-media { max-width: 90vw; width: 900px;