This commit is contained in:
2026-10-17 22:11:06 +07:00
parent 35e579701a
commit 688d900cdc
13 changed files with 192 additions and 15 deletions
+6
View File
@@ -34,6 +34,12 @@ x64/ARM64 и Windows x64/ARM64; macOS ожидает сборки на Mac. Ар
[фактические результаты и открытая матрица](docs/SCREEN-008_RESULTS.md). [фактические результаты и открытая матрица](docs/SCREEN-008_RESULTS.md).
SCREEN-008 остаётся IN_PROGRESS до сборок macOS и аппаратной приёмки. SCREEN-008 остаётся IN_PROGRESS до сборок macOS и аппаратной приёмки.
В исходниках после упаковки кандидата согласованы названия режимов с сайтом,
переведены статусы игр и добавлен отдельный статус назначения звука сервером.
Игра для подключения управления явно указана рядом с кнопкой. Архивы 0.2.0-rc.1
не пересобирались и пока не содержат этих исправлений; руководство описывает
текущие исходники. Для проверки изменений используйте запуск из исходников ниже.
Новые задачи: `Новая задача: <описание>`. Новые задачи: `Новая задача: <описание>`.
Commit и push выполняет только пользователь. Вложенные `.git` не изменять. Commit и push выполняет только пользователь. Вложенные `.git` не изменять.
+18
View File
@@ -207,6 +207,24 @@ SCREEN-009 (LAN-пульт) отменена владельцем; ID не пе
по ролям и однозначный выбор внешнего дисплея. Названия кнопок и поведение по ролям и однозначный выбор внешнего дисплея. Названия кнопок и поведение
сверены с текущим UI; относительные ссылки и git diff --check — PASS. сверены с текущим UI; относительные ссылки и git diff --check — PASS.
Код не менялся, новые аппаратные испытания не выполнялись. Код не менялся, новые аппаратные испытания не выполнялись.
- Согласование интерфейса с сервером и сайтом (2026-10-17): названия режимов
приведены к сайту («Введение», «Успехов», «Закрывашка», «Спасибо за игру»),
статусы игр переведены. Рядом с подключением указано имя локальной игры;
выбор серверной игры для загрузки её не меняет. Добавлено пояснение, что
отключение управления не удаляет серверную привязку.
- Назначение звука: OnlineState.soundEnabled (boolean | null) содержит последнее
подтверждение сервера отдельно от локального mute. До подтверждения неизвестно,
без опроса помечается как последнее состояние; при новой сессии/отключении
сбрасывается. Отрицательное значение означает «Экран не назначен для звука»:
наличие другого назначенного экрана серверный флаг не доказывает.
- Проверки согласования: type-check, ESLint, Stylelint, 74 теста, сборка и GUI smoke —
PASS. GUI проверил переведённый статус, независимые выборы игр и статус звука;
юнит-тест — смену назначения, mute, обрыв связи и сброс сессии. Два ранних
GUI-прогона остановились на селекторе нового теста; исправлен выбор видимого
элемента Naive UI, итоговый прогон PASS. Ссылки документации (28) и
git diff --check — PASS. Руководство и чек-лист обновлены. Архивы кандидата
не пересобирались; отличие текущих исходников от архивов явно описано в README.
Бэкенд, сайт, protobuf и зависимости не менялись.
- Открыто: получить macOS x64/arm64 через workflow после commit/push владельцем; - Открыто: получить macOS x64/arm64 через workflow после commit/push владельцем;
выполнить аппаратную матрицу, включая оба Linux-дистрибутива, час офлайн, выполнить аппаратную матрицу, включая оба Linux-дистрибутива, час офлайн,
проектор/звук/телефон и физические замеры на M1. Статус IN_PROGRESS до получения проектор/звук/телефон и физические замеры на M1. Статус IN_PROGRESS до получения
+4 -2
View File
@@ -22,8 +22,10 @@
- [ ] В ОС выбран нужный аудиовыход. Пробный звук слышен физически, mute выключен, - [ ] В ОС выбран нужный аудиовыход. Пробный звук слышен физически, mute выключен,
громкость основного канала и музыки подходит для помещения. громкость основного канала и музыки подходит для помещения.
- [ ] После подключения HDMI аудиовыход проверен повторно. - [ ] После подключения HDMI аудиовыход проверен повторно.
- [ ] Для телефона подключено управление выбранной игрой, проверен номер экрана, - [ ] Перед подключением телефона проверено название в строке «Игра для управления»;
связь с сервером и новая команда с сайта. Звук назначен нужному экрану. подключено управление этой игрой, проверен номер экрана,
связь с сервером и новая команда с сайта. Проверено назначение звука экрану
и отдельно локальное выключение звука.
- [ ] Перед стартом выбрана нужная сцена, музыка и таймер в ожидаемом состоянии. - [ ] Перед стартом выбрана нужная сцена, музыка и таймер в ожидаемом состоянии.
- [ ] Компьютер подключён к питанию; крышка остаётся открытой. - [ ] Компьютер подключён к питанию; крышка остаётся открытой.
+1 -1
View File
@@ -19,7 +19,7 @@
## Команды с телефона ## Команды с телефона
1. Нажать «Подключить управление выбранной игрой», записать номер экрана. 1. Нажать «Подключить управление этой игрой», записать номер экрана.
Открыть показ на внешнем дисплее. Дождаться сообщения о связи. Открыть показ на внешнем дисплее. Дождаться сообщения о связи.
2. На телефоне открыть существующую страницу управления этой игрой. 2. На телефоне открыть существующую страницу управления этой игрой.
Убедиться, что экран появился в её списке. Назначить звук этому экрану Убедиться, что экран появился в её списке. Назначить звук этому экрану
+19
View File
@@ -64,6 +64,25 @@ macOS нет. Этот стенд не подтверждает Ubuntu/Debian,
Физический звук, час офлайн, телефон, сон, переподключение и замеры не выполнены. Физический звук, час офлайн, телефон, сон, переподключение и замеры не выполнены.
Этот Manjaro-стенд не заменяет Ubuntu/Debian из обязательной матрицы. Этот Manjaro-стенд не заменяет Ubuntu/Debian из обязательной матрицы.
## Проверка согласования интерфейса (2026-10-17)
Проверены текущие исходники после упаковки кандидата. Архивы 0.2.0-rc.1
не пересобирались и не включают следующие изменения интерфейса.
- Названия восьми режимов согласованы с сайтом, статусы игр показаны по-русски.
- Подключение явно указывает локально выбранную игру, независимо от игры
в серверном списке загрузки; отключение не обещает отвязку на сервере.
- Отдельно отображается подтверждённое сервером назначение звука.
Локальный mute его не изменяет; отсутствие текущего опроса явно обозначено.
- Type-check, ESLint, Stylelint, 74 теста, сборка, GUI smoke — PASS.
Новый GUI-сценарий выбирает серверную игру для подготовки при другом локальном
пакете, проверяет неизменность цели управления и перевод статуса. После выбора
нового локального пакета цель меняется; серверное назначение звука видно в UI.
- Проверены 28 ссылок документации и git diff --check — PASS.
Прежние ограничения GUI-стенда сохраняются: Linux x64, синтетический внешний
дисплей, тестовые no-sandbox/mute-audio/disable-gpu. Новых аппаратных результатов нет.
## Отчёт одного стенда ## Отчёт одного стенда
- Дата, версия приложения и SHA-256 архива: - Дата, версия приложения и SHA-256 архива:
+15 -4
View File
@@ -5,6 +5,10 @@
Телефон отправляет команды через существующий сервер сайта; без сети Телефон отправляет команды через существующий сервер сайта; без сети
управляйте показом с компьютера. управляйте показом с компьютера.
Руководство описывает текущие исходники. Ранее собранные архивы 0.2.0-rc.1
ещё содержат прежние подписи режимов и кнопки подключения и не отображают
новый отдельный статус назначения звука. Для этих исправлений нужна новая сборка.
Перед мероприятием используйте [чек-лист площадки](EVENT_CHECKLIST.md). Перед мероприятием используйте [чек-лист площадки](EVENT_CHECKLIST.md).
## Версия и установка ## Версия и установка
@@ -58,6 +62,7 @@ Windows 11, macOS 13+, Ubuntu 24.04 LTS или Debian 13. Эти требова
используйте «Отменить загрузку». Не выезжайте с незавершённой загрузкой. используйте «Отменить загрузку». Не выезжайте с незавершённой загрузкой.
4. В отдельном блоке **«Материалы показа» выберите подготовленную игру**. 4. В отдельном блоке **«Материалы показа» выберите подготовленную игру**.
Загрузка и выбор игры на сервере сами по себе не выбирают пакет для показа. Загрузка и выбор игры на сервере сами по себе не выбирают пакет для показа.
Статусы игр отображаются как «Черновик», «Идёт», «Пауза», «Завершена».
Сообщение «Тестовый режим: синтетические аудио и видео» означает, что игровая Сообщение «Тестовый режим: синтетические аудио и видео» означает, что игровая
копия не выбрана. копия не выбрана.
5. Откройте показ, проверьте изображения, вступление, видео и плейлисты. 5. Откройте показ, проверьте изображения, вступление, видео и плейлисты.
@@ -78,7 +83,7 @@ Windows 11, macOS 13+, Ubuntu 24.04 LTS или Debian 13. Эти требова
В «План показа» выберите шаг, используйте «Предыдущий шаг» / «Следующий шаг» В «План показа» выберите шаг, используйте «Предыдущий шаг» / «Следующий шаг»
либо переключатель «Режим показа». Доступны восемь режимов: «Обложка», либо переключатель «Режим показа». Доступны восемь режимов: «Обложка»,
«Вступление», «Завершение вступления», «Заключительное видео», «Завершение показа», «Введение», «Успехов», «Закрывашка», «Спасибо за игру»,
«Результаты», «Таймер» и «Текст». Переход к шагу или режиму сам не запускает «Результаты», «Таймер» и «Текст». Переход к шагу или режиму сам не запускает
медиа и таймер. Дождитесь готовности; отправленная команда ещё не означает медиа и таймер. Дождитесь готовности; отправленная команда ещё не означает
успешного воспроизведения — смотрите статус и фактическое изображение/звук. успешного воспроизведения — смотрите статус и фактическое изображение/звук.
@@ -123,19 +128,25 @@ Windows 11, macOS 13+, Ubuntu 24.04 LTS или Debian 13. Эти требова
## Управление с телефона ## Управление с телефона
1. Войдите и выберите готовую игру в «Материалы показа». 1. Войдите и выберите готовую игру в «Материалы показа».
2. Нажмите «Подключить управление выбранной игрой»; запишите показанный номер экрана. 2. Проверьте строку «Игра для управления»: это название локально выбранного
пакета, независимо от игры в списке загрузки выше. Нажмите «Подключить управление
этой игрой»; запишите показанный номер экрана.
Откройте показ и дождитесь «Связь с сервером есть». Откройте показ и дождитесь «Связь с сервером есть».
3. На телефоне откройте существующую страницу управления этой игрой на сайте. 3. На телефоне откройте существующую страницу управления этой игрой на сайте.
При необходимости назначьте звук экрану через сайт или кнопку При необходимости назначьте звук экрану через сайт или кнопку
«Назначить звук этому экрану». Это отключает звук других экранов игры «Назначить звук этому экрану». Это отключает звук других экранов игры
по существующим правилам сервера. по существующим правилам сервера. Статус назначения звука показывается отдельно
от локального переключателя «Выключить весь звук». До подтверждения сервером
назначение неизвестно; при закрытом показе или потере связи отображается
последнее подтверждённое состояние.
4. Отправьте новую команду. Первое состояние после подключения, открытия окна 4. Отправьте новую команду. Первое состояние после подключения, открытия окна
или восстановления связи не повторяет старые команды. или восстановления связи не повторяет старые команды.
Опрос сервера идёт раз в секунду при открытом показе. Быстрые команды между Опрос сервера идёт раз в секунду при открытом показе. Быстрые команды между
опросами могут объединяться. Локальные команды компьютера действуют только опросами могут объединяться. Локальные команды компьютера действуют только
на его показ и не рассылаются другим экранам. Новая команда телефона может на его показ и не рассылаются другим экранам. Новая команда телефона может
изменить локальный показ. «Отключить управление» прекращает серверное управление. изменить локальный показ. «Отключить управление» прекращает управление приложением,
но сохраняет привязку экрана на сервере. Для отвязки используйте сайт.
При отсутствии интернета пользуйтесь локальным пультом; прямого подключения При отсутствии интернета пользуйтесь локальным пультом; прямого подключения
телефона к приложению по LAN нет. телефона к приложению по LAN нет.
+35
View File
@@ -541,3 +541,38 @@ test('вход без прав управления не сохраняет то
code: 'ENOENT', code: 'ENOENT',
}) })
}) })
test('назначение звука подтверждается сервером отдельно от локального mute и сбрасывается с сессией', async (t) => {
const f = await fixture(t)
await f.prepare()
assert.equal(f.online.state.soundEnabled, null)
f.controller.output(true)
f.model.remote.soundEnabled = false
await f.online.poll()
assert.equal(f.online.state.soundEnabled, false)
send(f.controller, { action: 'mute', enabled: false })
assert.equal(f.online.state.soundEnabled, false)
await f.online.act({ action: 'sound' })
assert.equal(f.online.state.soundEnabled, true)
f.model.remote.soundEnabled = true
await f.online.poll()
send(f.controller, { action: 'mute', enabled: true })
assert.equal(f.online.state.soundEnabled, true)
f.model.offline = true
await f.online.poll()
assert.equal(f.online.state.connection, 'offline')
assert.equal(f.online.state.soundEnabled, true)
f.model.offline = false
f.model.remote.soundEnabled = false
await f.online.poll()
assert.equal(f.online.state.soundEnabled, false)
await f.online.act({ action: 'connect' })
assert.equal(f.online.state.soundEnabled, null)
await f.online.poll()
await f.online.act({ action: 'disconnect' })
assert.equal(f.online.state.soundEnabled, null)
assert.equal(
f.requests.some((r) => r.path.endsWith('/unbind')),
false,
)
})
+7 -1
View File
@@ -73,6 +73,7 @@ export class Online {
canCancel: false, canCancel: false,
games: [], games: [],
screenId: null, screenId: null,
soundEnabled: null,
connection: 'disabled', connection: 'disabled',
error: '', error: '',
notice: '', notice: '',
@@ -220,6 +221,7 @@ export class Online {
this.generation++ this.generation++
this.screen = null this.screen = null
this.previous = null this.previous = null
this.state.soundEnabled = null
this.state.screenId = null this.state.screenId = null
this.state.connection = 'disabled' this.state.connection = 'disabled'
} }
@@ -489,13 +491,16 @@ export class Online {
this.detach() this.detach()
await this.persistSecrets() await this.persistSecrets()
return return
case 'sound': case 'sound': {
const generation = this.generation
if (!this.screen) throw new Error('Сначала подключите управление') if (!this.screen) throw new Error('Сначала подключите управление')
await this.api(`/api/screens/${this.screen.id}/sound`, { await this.api(`/api/screens/${this.screen.id}/sound`, {
screenID: this.screen.id, screenID: this.screen.id,
soundEnabled: true, soundEnabled: true,
}) })
if (generation === this.generation) this.state.soundEnabled = true
return return
}
case 'results': case 'results':
return this.refreshResults() return this.refreshResults()
default: default:
@@ -616,6 +621,7 @@ export class Online {
if (previous?.music && next.music && next.music.version < previous.music.version) if (previous?.music && next.music && next.music.version < previous.music.version)
next = { ...next, music: previous.music } next = { ...next, music: previous.music }
this.previous = next this.previous = next
this.state.soundEnabled = next.sound
if (this.state.connection === 'offline') this.state.error = '' if (this.state.connection === 'offline') this.state.error = ''
this.state.connection = 'connected' this.state.connection = 'connected'
if (!previous && this.initialSound) { if (!previous && this.initialSound) {
+43
View File
@@ -106,17 +106,60 @@ module.exports = async function onlineSmoke({
async () => !(await evaluate('window.screenBridge.onlineState()')).busy, async () => !(await evaluate('window.screenBridge.onlineState()')).busy,
'login complete', 'login complete',
) )
await poll(
() =>
evaluate(
`Array.from(document.querySelectorAll('.n-base-selection')).some(e => e.textContent.includes('Выберите игру для подготовки'))`,
),
'server game selector',
)
await evaluate(
`Array.from(document.querySelectorAll('.n-base-selection')).find(e => e.textContent.includes('Выберите игру для подготовки')).click()`,
)
await poll(
() =>
evaluate(
`Array.from(document.querySelectorAll('.n-base-select-option')).some(e => e.textContent.includes('HTTP smoke · Завершена'))`,
),
'translated game status',
)
await evaluate(
`Array.from(document.querySelectorAll('.n-base-select-option')).find(e => e.textContent.includes('HTTP smoke · Завершена')).click()`,
)
const selectedBeforeDownload = (await state()).catalog.name
await poll(
() =>
evaluate(
`document.querySelector('[data-testid="control-game"]')?.textContent.includes(${JSON.stringify(selectedBeforeDownload)})`,
),
'control target stays on local package despite server game selection',
)
assert.notEqual(selectedBeforeDownload, 'HTTP smoke')
await act({ action: 'download', gameId: 6 }) await act({ action: 'download', gameId: 6 })
const packages = await evaluate('window.screenBridge.packages()') const packages = await evaluate('window.screenBridge.packages()')
const pkg = packages.find((p) => p.name === 'HTTP smoke') const pkg = packages.find((p) => p.name === 'HTTP smoke')
assert.ok(pkg) assert.ok(pkg)
await evaluate(`window.screenBridge.selectPackage(${JSON.stringify(pkg.key)})`) await evaluate(`window.screenBridge.selectPackage(${JSON.stringify(pkg.key)})`)
await poll(
() =>
evaluate(
`document.querySelector('[data-testid="control-game"]')?.textContent.includes('HTTP smoke')`,
),
'control target follows selected local package',
)
await act({ action: 'connect' }) await act({ action: 'connect' })
await evaluate(`window.screenBridge.show(${displayId})`) await evaluate(`window.screenBridge.show(${displayId})`)
await poll( await poll(
async () => (await evaluate('window.screenBridge.onlineState()')).connection === 'connected', async () => (await evaluate('window.screenBridge.onlineState()')).connection === 'connected',
'HTTP baseline', 'HTTP baseline',
) )
await poll(
() =>
evaluate(
`document.querySelector('[data-testid="sound-assignment"]')?.textContent.includes('Экран назначен для звука')`,
),
'server sound assignment shown in UI',
)
assert.equal((await state()).channels.main.desired, 'stopped') assert.equal((await state()).channels.main.desired, 'stopped')
version++ version++
await poll( await poll(
+38 -3
View File
@@ -3,7 +3,7 @@ import { NAlert, NButton, NCard, NInput, NSelect, NSpace, useMessage } from 'nai
import { onMounted, onUnmounted, ref } from 'vue' import { onMounted, onUnmounted, ref } from 'vue'
import type { OnlineIntent, OnlineState } from '../types/online' import type { OnlineIntent, OnlineState } from '../types/online'
defineProps<{ outputOpen: boolean; selectedKey: string | null }>() defineProps<{ outputOpen: boolean; selectedKey: string | null; selectedName: string | null }>()
const emit = defineEmits<{ packagesChanged: [] }>() const emit = defineEmits<{ packagesChanged: [] }>()
const bridge = window.screenBridge const bridge = window.screenBridge
const message = useMessage() const message = useMessage()
@@ -16,6 +16,7 @@ const state = ref<OnlineState>({
canCancel: false, canCancel: false,
games: [], games: [],
screenId: null, screenId: null,
soundEnabled: null,
connection: 'disabled', connection: 'disabled',
error: '', error: '',
notice: '', notice: '',
@@ -27,6 +28,12 @@ const email = ref('')
const password = ref('') const password = ref('')
const gameId = ref<number | null>(null) const gameId = ref<number | null>(null)
const isBusy = ref(false) const isBusy = ref(false)
const gameStatusLabels = new Map([
['draft', 'Черновик'],
['run', 'Идёт'],
['pause', 'Пауза'],
['finish', 'Завершена'],
])
const connectionLabels = { const connectionLabels = {
disabled: 'Управление не подключено', disabled: 'Управление не подключено',
waiting: 'Ожидание открытия показа', waiting: 'Ожидание открытия показа',
@@ -134,7 +141,12 @@ onUnmounted(() => clearInterval(timer))
</n-space> </n-space>
<n-select <n-select
v-model:value="gameId" v-model:value="gameId"
:options="state.games.map((g) => ({ label: `${g.name} · ${g.status}`, value: g.id }))" :options="
state.games.map((g) => ({
label: `${g.name} · ${gameStatusLabels.get(g.status) ?? 'Неизвестный статус'}`,
value: g.id,
}))
"
:disabled="isBusy || state.busy" :disabled="isBusy || state.busy"
placeholder="Выберите игру для подготовки" placeholder="Выберите игру для подготовки"
:input-props="{ 'aria-label': 'Игра на сервере' }" :input-props="{ 'aria-label': 'Игра на сервере' }"
@@ -156,11 +168,13 @@ onUnmounted(() => clearInterval(timer))
Выбранную игру можно обновить после закрытия показа. Для обновления файлов по прежним Выбранную игру можно обновить после закрытия показа. Для обновления файлов по прежним
адресам загрузите материалы повторно. адресам загрузите материалы повторно.
</p> </p>
<p>Выбор игры выше используется для загрузки материалов.</p>
<p v-if="selectedKey" data-testid="control-game">Игра для управления: {{ selectedName }}</p>
<n-button <n-button
v-if="selectedKey" v-if="selectedKey"
:disabled="isBusy || state.busy" :disabled="isBusy || state.busy"
@click="run({ action: 'connect' })" @click="run({ action: 'connect' })"
>Подключить управление выбранной игрой</n-button >Подключить управление этой игрой</n-button
> >
</template> </template>
<p role="status"> <p role="status">
@@ -168,6 +182,23 @@ onUnmounted(() => clearInterval(timer))
}}{{ state.screenId ? ` · Экран №${state.screenId}` : '' }} }}{{ state.screenId ? ` · Экран №${state.screenId}` : '' }}
</p> </p>
<p v-if="state.screenId && !outputOpen">Опрос сервера начнётся после открытия показа.</p> <p v-if="state.screenId && !outputOpen">Опрос сервера начнётся после открытия показа.</p>
<template v-if="state.screenId">
<p role="status" data-testid="sound-assignment">
{{
state.soundEnabled === null
? 'Назначение звука пока неизвестно'
: state.soundEnabled
? 'Экран назначен для звука'
: 'Экран не назначен для звука'
}}
<template
v-if="state.soundEnabled !== null && (!outputOpen || state.connection !== 'connected')"
>
· Последнее подтверждённое состояние; сейчас назначение не проверяется.
</template>
</p>
<p>Переключатель «Выключить весь звук» управляет локальным звуком отдельно.</p>
</template>
<n-space v-if="state.screenId"> <n-space v-if="state.screenId">
<n-button <n-button
:disabled="isBusy || state.busy || !state.authenticated" :disabled="isBusy || state.busy || !state.authenticated"
@@ -181,6 +212,10 @@ onUnmounted(() => clearInterval(timer))
>Отключить управление</n-button >Отключить управление</n-button
> >
</n-space> </n-space>
<p v-if="state.screenId">
«Отключить управление» прекращает управление приложением; привязка экрана на сервере
сохраняется.
</p>
<n-alert v-if="state.error" type="error">{{ state.error }}</n-alert> <n-alert v-if="state.error" type="error">{{ state.error }}</n-alert>
<n-alert v-if="state.notice" type="info">{{ state.notice }}</n-alert> <n-alert v-if="state.notice" type="info">{{ state.notice }}</n-alert>
<template v-if="state.busy"> <template v-if="state.busy">
+1
View File
@@ -237,6 +237,7 @@ onUnmounted(() => {
<connection-panel <connection-panel
:output-open="state?.outputOpen ?? false" :output-open="state?.outputOpen ?? false"
:selected-key="state?.catalog.key ?? null" :selected-key="state?.catalog.key ?? null"
:selected-name="state?.catalog.name ?? null"
@packages-changed="refreshPackages" @packages-changed="refreshPackages"
/> />
<n-card title="Материалы показа"> <n-card title="Материалы показа">
+1
View File
@@ -10,6 +10,7 @@ export interface OnlineState {
busy: boolean busy: boolean
canCancel: boolean canCancel: boolean
games: { id: number; name: string; status: string }[] games: { id: number; name: string; status: string }[]
soundEnabled: boolean | null
screenId: string | null screenId: string | null
connection: 'disabled' | 'waiting' | 'connected' | 'offline' | 'expired' connection: 'disabled' | 'waiting' | 'connected' | 'offline' | 'expired'
error: string error: string
+4 -4
View File
@@ -2,10 +2,10 @@ import type { ScreenMode, TimerState } from '../types/screen.js'
export const modeLabels: Record<ScreenMode, string> = { export const modeLabels: Record<ScreenMode, string> = {
1: 'Обложка', 1: 'Обложка',
2: 'Вступление', 2: 'Введение',
3: 'Завершение вступления', 3: 'Успехов',
4: 'Заключительное видео', 4: 'Закрывашка',
5: 'Завершение показа', 5: 'Спасибо за игру',
6: 'Результаты', 6: 'Результаты',
7: 'Таймер', 7: 'Таймер',
8: 'Текст', 8: 'Текст',