This commit is contained in:
2026-10-18 03:32:50 +07:00
parent 3e2608eae3
commit 9c5419b9bf
10 changed files with 73 additions and 193 deletions
+32
View File
@@ -265,3 +265,35 @@
сервер и физический проектор/звук не проверялись. Замена байтов по прежнему сервер и физический проектор/звук не проверялись. Замена байтов по прежнему
URL требует ручной повторной загрузки; точность позиции ограничена API сервера. URL требует ручной повторной загрузки; точность позиции ограничена API сервера.
Commit/push не выполнялись. Commit/push не выполнялись.
## SCREEN-013 — Убрать блок «Материалы показа»
- Статус: `DONE`.
- Дата: 2026-10-18.
- Результат: с пульта удалён блок выбора локального пакета, тестового режима
и удаления материалов. Убраны его обработчики, список пакетов и неиспользуемое
событие ConnectionPanel. В настройки ничего не переносилось.
- Подключение игры через сервер, автоматический кеш и воспроизведение с диска
сохранены. Руководство, README и инструкции приёмки обновлены; smoke выбирает
тестовый пакет через существующий IPC и проверяет отсутствие блока в интерфейсе.
- Проверки: type-check, lint, lint:css, сборка, Electron GUI smoke
(локальный показ, связь, дозагрузка, план и устойчивость) и git diff --check — PASS.
- Границы: изменения только evening_detective_screen. Архивы не пересобирались;
физический проектор не проверялся. Commit/push не выполнялись.
## SCREEN-014 — Настройка экранов только через план
- Статус: `DONE`.
- Дата: 2026-10-18.
- Результат: удалены блок «Текст и таймер», отдельный переключатель режима
и используемые только ими поля/обработчики. Режим, текст, подпись и длительность
таймера настраиваются в редакторе шагов. Пустой план предлагает создать план.
- Управление таймером сохранено в активном шаге; статус и остаток таймера показаны
рядом с планом, включая отсчёт в фоне. Предпросмотр, музыка, дисплей, API,
типы команд и серверная синхронизация сохранены.
- Проверки: type-check, lint, lint:css, сборка и Electron GUI smoke — PASS.
Проверены отсутствие удалённых элементов, редактирование текста/таймера,
сохранение, переходы, старт/пауза таймера, локальный показ и синхронизация.
33 локальные Markdown-ссылки и git diff --check — PASS.
- Границы: изменения только evening_detective_screen. Архивы не пересобирались;
физический проектор не проверялся. Commit/push не выполнялись.
+7 -8
View File
@@ -81,8 +81,7 @@ npm run dev
и назначьте экран компьютера основным. В блоке «Дисплей показа» выберите и назначьте экран компьютера основным. В блоке «Дисплей показа» выберите
внешний дисплей и нажмите «Открыть показ». Аудио и видео — синтетические локальные файлы длительностью внешний дисплей и нажмите «Открыть показ». Аудио и видео — синтетические локальные файлы длительностью
60 секунд; бинарники находятся в игнорируемом fixtures/ и не входят в Git. 60 секунд; бинарники находятся в игнорируемом fixtures/ и не входят в Git.
Тестовый режим сохраняет отдельный выбор WAV/видео; игровые пакеты выбираются В блоке «Игра и сервер» доступны вход, выбор игры и подготовка материалов.
в разделе «Материалы показа». В блоке «Игра и сервер» доступны вход, список игр и загрузка материалов.
Для постоянной связи войдите, выберите игру в серверном списке и нажмите Для постоянной связи войдите, выберите игру в серверном списке и нажмите
«Подключить выбранную игру». Готовый пакет заранее не требуется: сначала сохраняются «Подключить выбранную игру». Готовый пакет заранее не требуется: сначала сохраняются
@@ -189,7 +188,7 @@ URL, независимо от исходного имени файла. Лок
## Локальный проигрыватель SCREEN-004 ## Локальный проигрыватель SCREEN-004
На пульте выберите готовый пакет или «Тестовый режим SCREEN-001», внешний дисплей Подключите игру в блоке «Игра и сервер», выберите внешний дисплей
и откройте показ. В основном канале доступны вступление и заключительное видео; и откройте показ. В основном канале доступны вступление и заключительное видео;
их выбор останавливает прежнее основное медиа. Музыка воспроизводится отдельным их выбор останавливает прежнее основное медиа. Музыка воспроизводится отдельным
каналом без автоматического приглушения. Звук выводится только окном показа. каналом без автоматического приглушения. Звук выводится только окном показа.
@@ -234,17 +233,17 @@ URL, независимо от исходного имени файла. Лок
## Пульт и режимы SCREEN-005 ## Пульт и режимы SCREEN-005
Пульт включает готовый пакет, план шагов, переключатель восьми режимов, текст, Пульт включает подключение игры через сервер, план шагов с восемью режимами, текстом и параметрами таймера,
таймер, предпросмотр и прежние независимые каналы медиа/музыки. План экранов редактируется в пульте и сохраняется на сервере выбранной игры; предпросмотр и прежние независимые каналы медиа/музыки. План экранов редактируется в пульте и сохраняется на сервере выбранной игры;
последняя подтверждённая копия хранится отдельно от пакета для офлайн-показа. последняя подтверждённая копия хранится отдельно от пакета для офлайн-показа.
Онлайн-переход меняет общий шаг, офлайн-переход остаётся локальным и не отправляется Онлайн-переход меняет общий шаг, офлайн-переход остаётся локальным и не отправляется
позже. Восстановление сети само не переключает показ. Выбор шага применяет его параметры без автоматического позже. При восстановлении сети применяется актуальное состояние сервера. Выбор шага применяет его параметры без автоматического
запуска звука/таймера; окончание медиа не переключает шаг. Пуск основного ресурса запуска звука/таймера; окончание медиа не переключает шаг. Пуск основного ресурса
показывает соответствующий режим вступления или видео. Уход в другую сцену показывает соответствующий режим вступления или видео. Уход в другую сцену
останавливает основной канал, сохраняя независимую музыку. останавливает основной канал, сохраняя независимую музыку.
Текст и параметры таймера редактируются как черновик; только «Показать текст» / Режим, текст и параметры таймера задаются только в редакторе шагов плана.
«Показать таймер» публикуют их. Текст отображается как обычный текст, без HTML. Сохранение черновика не меняет показ; для перехода нажмите «Показать» у шага. Текст отображается как обычный текст, без HTML.
Таймер вычисляется в основном процессе по монотонному deadline; интервалы только Таймер вычисляется в основном процессе по монотонному deadline; интервалы только
обновляют снимок. Пауза сохраняет точный остаток, сброс возвращает полную длительность. обновляют снимок. Пауза сохраняет точный остаток, сброс возвращает полную длительность.
Таймер продолжает идти за другой сценой и при закрытом окне; смена пакета сбрасывает Таймер продолжает идти за другой сценой и при закрытом окне; смена пакета сбрасывает
+1 -1
View File
@@ -8,7 +8,7 @@
- [ ] Выбран архив своей ОС/архитектуры, SHA-256 совпал, приложение запускается. - [ ] Выбран архив своей ОС/архитектуры, SHA-256 совпал, приложение запускается.
- [ ] В «Игра и сервер» выполнен вход, нужная игра загружена до завершения. - [ ] В «Игра и сервер» выполнен вход, нужная игра загружена до завершения.
- [ ] В «Материалы показа» выбрана именно игра, а не тестовый режим. - [ ] В блоке «Игра и сервер» подключена нужная игра; подготовка материалов завершена.
- [ ] Проверены обложка, шаги, вступление, видео, текст, таймер и нужные плейлисты. - [ ] Проверены обложка, шаги, вступление, видео, текст, таймер и нужные плейлисты.
- [ ] Пробный показ и перезапуск без интернета подтвердили готовность локальной копии. - [ ] Пробный показ и перезапуск без интернета подтвердили готовность локальной копии.
- [ ] Если нужны результаты, сохранён доступный снимок и проверена его дата. - [ ] Если нужны результаты, сохранён доступный снимок и проверена его дата.
+2 -2
View File
@@ -12,8 +12,8 @@ SCREEN-004 остаётся IN_PROGRESS до результатов владел
на основном дисплее компьютера. Ручная проверка не подставляет синтетический монитор. на основном дисплее компьютера. Ручная проверка не подставляет синтетический монитор.
3. Команда создаст отдельный временный каталог данных и пакет «Offline smoke»: 3. Команда создаст отдельный временный каталог данных и пакет «Offline smoke»:
WAV-вступление, H.264/AAC-видео и два четырёхсекундных тональных трека. WAV-вступление, H.264/AAC-видео и два четырёхсекундных тональных трека.
Путь данных печатается как SCREEN004_ACCEPTANCE_READY. Откройте пакет через Путь данных печатается как SCREEN004_ACCEPTANCE_READY. Пакет выбирается автоматически
«Материалы показа», выберите внешний дисплей и нажмите «Открыть показ». проверочным сценарием; выберите внешний дисплей и нажмите «Открыть показ».
4. Отключите сеть после запуска. Не используйте --mute-audio для физической проверки. 4. Отключите сеть после запуска. Не используйте --mute-audio для физической проверки.
Установите подходящую громкость и проверьте нужный аудиовыход ОС. Параметры Установите подходящую громкость и проверьте нужный аудиовыход ОС. Параметры
--no-sandbox/--disable-gpu, применённые в автоматическом smoke текущей среды, --no-sandbox/--disable-gpu, применённые в автоматическом smoke текущей среды,
+4 -3
View File
@@ -25,10 +25,11 @@ SCREEN-001/SCREEN-004 остаётся открытой; эта инструкц
При закрытом окне предпросмотр недоступен; скрытый/свёрнутый пульт не захватывает кадры. При закрытом окне предпросмотр недоступен; скрытый/свёрнутый пульт не захватывает кадры.
- Проверьте обложку, затемнённый фон и кириллицу без интернета. На проекторе нет - Проверьте обложку, затемнённый фон и кириллицу без интернета. На проекторе нет
кнопок, сообщений пульта, диагностических статусов и исходных путей файлов. кнопок, сообщений пульта, диагностических статусов и исходных путей файлов.
- Введите текст с переносами строк, кириллицей и `<script>текст</script>`. - В редакторе плана задайте текст шага с переносами строк, кириллицей и `<script>текст</script>`.
До «Показать текст» сцена не меняется; после применения это обычный текст, До перехода к сохранённому шагу сцена не меняется; после перехода это обычный текст,
без выполнения HTML/JavaScript. без выполнения HTML/JavaScript.
- Настройте таймер на 10 секунд. «Показать таймер» применяет параметры без старта. - В редакторе плана настройте таймерный шаг на 10 секунд и сохраните план.
«Показать» у шага применяет параметры без старта.
Проверьте старт, паузу, ожидание на паузе, продолжение и сброс. При переходе Проверьте старт, паузу, ожидание на паузе, продолжение и сброс. При переходе
к обложке отсчёт продолжается; при возвращении виден актуальный остаток. к обложке отсчёт продолжается; при возвращении виден актуальный остаток.
В конце остаётся 00:00, без сигнала и автоматической смены режима. В конце остаётся 00:00, без сигнала и автоматической смены режима.
+11 -14
View File
@@ -68,7 +68,6 @@ Windows 11, macOS 13+, Ubuntu 24.04 LTS или Debian 13. Эти требова
включая воспроизведение. До готовности нового файла прежнее содержимое канала включая воспроизведение. До готовности нового файла прежнее содержимое канала
сохраняется. Все медиа читаются только с локального диска. сохраняется. Все медиа читаются только с локального диска.
6. Перед выездом дождитесь полной подготовки без ошибок и проверьте игру без сети. 6. Перед выездом дождитесь полной подготовки без ошибок и проверьте игру без сети.
«Материалы показа» позволяет выбирать ранее сохранённые игры и тестовый режим.
Офлайн доступны только фактически скачанные файлы. Офлайн доступны только фактически скачанные файлы.
Если ОС не предоставляет защищённое хранилище, приложение предупреждает: Если ОС не предоставляет защищённое хранилище, приложение предупреждает:
@@ -83,7 +82,7 @@ Windows 11, macOS 13+, Ubuntu 24.04 LTS или Debian 13. Эти требова
## Проведение показа ## Проведение показа
В «План экранов» нажмите «Показать» у нужного шага, используйте «Предыдущий шаг» / «Следующий шаг» В «План экранов» нажмите «Показать» у нужного шага, используйте «Предыдущий шаг» / «Следующий шаг»
либо переключатель «Режим показа». Доступны восемь режимов: «Обложка», для переходов по плану. В редакторе шагов доступны восемь режимов: «Обложка»,
«Введение», «Успехов», «Закрывашка», «Спасибо за игру», «Введение», «Успехов», «Закрывашка», «Спасибо за игру»,
«Результаты», «Таймер» и «Текст». Переход к шагу или режиму сам не запускает «Результаты», «Таймер» и «Текст». Переход к шагу или режиму сам не запускает
медиа и таймер. Дождитесь готовности; отправленная команда ещё не означает медиа и таймер. Дождитесь готовности; отправленная команда ещё не означает
@@ -96,9 +95,8 @@ Windows 11, macOS 13+, Ubuntu 24.04 LTS или Debian 13. Эти требова
режим, текст, надпись и длительность таймера. «Сохранить план» отправляет план режим, текст, надпись и длительность таймера. «Сохранить план» отправляет план
на сервер; «Отменить» возвращает сохранённый вариант. Для удаления всего плана на сервер; «Отменить» возвращает сохранённый вариант. Для удаления всего плана
нажмите «Удалить план», затем «Сохранить план». При ошибке сохранения черновик нажмите «Удалить план», затем «Сохранить план». При ошибке сохранения черновик
остаётся в редакторе. Изменения черновика и сохранение сами не меняют проектор. Публикация текста остаётся в редакторе. Изменения черновика и сохранение сами не меняют проектор.
кнопкой «Показать текст» использует подходящий текстовый шаг общего плана Для показа сохранённого текста или таймера нажмите «Показать» у нужного шага.
или добавляет новый, поскольку сервер передаёт произвольный текст через план.
План общий с сайтом. При параллельных правках последнее сохранение План общий с сайтом. При параллельных правках последнее сохранение
заменяет его целиком. Во время редактирования фоновое обновление не меняет заменяет его целиком. Во время редактирования фоновое обновление не меняет
@@ -134,15 +132,15 @@ Windows 11, macOS 13+, Ubuntu 24.04 LTS или Debian 13. Эти требова
### Текст, таймер и результаты ### Текст, таймер и результаты
- В «Текст и таймер» заполните «Текст для показа» и нажмите «Показать текст». - В редакторе плана добавьте шаг «Текст», заполните текст и сохраните план.
До нажатия введённый текст не применяется. Переносы строк сохраняются. Нажмите «Показать» у этого шага. Переносы строк сохраняются.
- Заполните «Подпись таймера» и «Длительность, секунды», нажмите «Показать таймер». - Для шага «Таймер» задайте надпись и длительность в секундах, сохраните план
Затем используйте «Запустить таймер», «Пауза таймера», «Продолжить таймер» и покажите шаг. В активном шаге доступны «Старт», «Пауза», «Продолжить»
и «Сбросить таймер». При смене режима отсчёт продолжается в фоне; и «Сбросить». При смене шага отсчёт продолжается в фоне;
на нуле остаётся 00:00 без сигнала и автоматического перехода. на нуле остаётся 00:00 без сигнала и автоматического перехода.
- После завершения игры при доступной сети нажмите «Обновить снимок результатов» - После завершения игры при доступной сети нажмите «Обновить снимок результатов»
в «Игра и сервер» (кнопка доступна после подключения управления). в «Игра и сервер» (кнопка доступна после подключения управления).
Выберите режим «Результаты» и проверьте дату снимка. Офлайн показывается Покажите шаг «Результаты» и проверьте дату снимка. Офлайн показывается
последний сохранённый снимок, а не обновляемые серверные результаты. последний сохранённый снимок, а не обновляемые серверные результаты.
Неудачное обновление сохраняет прежний снимок. Неудачное обновление сохраняет прежний снимок.
@@ -179,7 +177,7 @@ Windows 11, macOS 13+, Ubuntu 24.04 LTS или Debian 13. Эти требова
| Проектор отключился, изменилось разрешение или произошёл сон | Прочитайте причину в пульте, восстановите дисплей/выход звука, выберите внешний экран и явно откройте показ. Медиа остановлены, работающий таймер сохранён на паузе. | | Проектор отключился, изменилось разрешение или произошёл сон | Прочитайте причину в пульте, восстановите дисплей/выход звука, выберите внешний экран и явно откройте показ. Медиа остановлены, работающий таймер сохранён на паузе. |
| Нет звука | Проверьте выход и mute ОС, кабель/USB/HDMI, «Выключить весь звук», громкость канала, открытое окно и статус проигрывателя. Для серверных команд проверьте назначение звука экрану. | | Нет звука | Проверьте выход и mute ОС, кабель/USB/HDMI, «Выключить весь звук», громкость канала, открытое окно и статус проигрывателя. Для серверных команд проверьте назначение звука экрану. |
| Ошибка подготовки сцены | Проверьте сообщение и нажмите «Повторить подготовку». При повторении проверьте пакет до продолжения игры. | | Ошибка подготовки сцены | Проверьте сообщение и нажмите «Повторить подготовку». При повторении проверьте пакет до продолжения игры. |
| Пакет повреждён или отсутствует файл | При доступной сети закройте показ и загрузите материалы заново. Для удаления выберите другой пакет или тестовый режим, затем «Удалить материалы» у нужной игры. Без сети восстановить отсутствующие байты нельзя. | | Пакет повреждён или отсутствует файл | При доступной сети нажмите «Повторить подготовку». Для полной повторной загрузки закройте показ и загрузите материалы заново. Без сети восстановить отсутствующие байты нельзя. |
| Истёк вход или сессия экрана | Повторно войдите или подключите управление по сообщению пульта. Готовые материалы остаются на диске. | | Истёк вход или сессия экрана | Повторно войдите или подключите управление по сообщению пульта. Готовые материалы остаются на диске. |
| Приложение перезапущено | Выбранная игра и сохранённые результаты восстанавливаются. Откройте показ вручную: онлайн применяется текущее состояние сервера, офлайн запускайте медиа локально. | | Приложение перезапущено | Выбранная игра и сохранённые результаты восстанавливаются. Откройте показ вручную: онлайн применяется текущее состояние сервера, офлайн запускайте медиа локально. |
| Пульт пуст или зависает при запуске | Запишите ОС, версию, симптомы и сообщения запуска, передайте отчёт. Не считайте такой запуск успешным. | | Пульт пуст или зависает при запуске | Запишите ОС, версию, симптомы и сообщения запуска, передайте отчёт. Не считайте такой запуск успешным. |
@@ -203,8 +201,7 @@ Windows 11, macOS 13+, Ubuntu 24.04 LTS или Debian 13. Эти требова
«Выйти» отключает серверное управление, но не удаляет материалы и не прерывает «Выйти» отключает серверное управление, но не удаляет материалы и не прерывает
локальное воспроизведение. Для завершения остановите медиа, закройте показ локальное воспроизведение. Для завершения остановите медиа, закройте показ
и приложение. «Удалить материалы» используйте только для пакетов, которые и приложение. Скачанные материалы сохраняются в локальном хранилище.
больше не нужны: это удаление локальной копии.
## Журналы и обращение за помощью ## Журналы и обращение за помощью
+8 -14
View File
@@ -214,6 +214,7 @@ app.whenReady().then(async () => {
'control UI', 'control UI',
) )
if (manual) { if (manual) {
await evaluate(`window.screenBridge.selectPackage(${JSON.stringify(offlinePackage.key)})`)
console.log( console.log(
'SCREEN004_ACCEPTANCE_READY', 'SCREEN004_ACCEPTANCE_READY',
JSON.stringify({ userData: app.getPath('userData'), electron: process.versions.electron }), JSON.stringify({ userData: app.getPath('userData'), electron: process.versions.electron }),
@@ -273,16 +274,8 @@ app.whenReady().then(async () => {
) )
assert.deepEqual(range, { status: 206, size: 100 }) assert.deepEqual(range, { status: 206, size: 100 })
await command({ action: 'pause', channel: 'main' }) await command({ action: 'pause', channel: 'main' })
await evaluate("document.querySelector('.n-base-selection').click()") assert.equal(await evaluate('document.body.textContent.includes("Материалы показа")'), false)
await poll( await evaluate(`window.screenBridge.selectPackage(${JSON.stringify(offlinePackage.key)})`)
() =>
evaluate(`(() => {
const option = Array.from(document.querySelectorAll('.n-base-select-option')).find(el => el.textContent.includes('Offline smoke'))
if (!option) return false
option.click(); return true
})()`),
'UI package choice',
)
await poll(async () => { await poll(async () => {
const s = await state() const s = await state()
return ( return (
@@ -424,12 +417,13 @@ app.whenReady().then(async () => {
if (index === 7) if (index === 7)
assert.ok(await inspect('document.body.textContent.includes("Кириллица: Ёжик и улика №7")')) assert.ok(await inspect('document.body.textContent.includes("Кириллица: Ёжик и улика №7")'))
} }
const publishedText = (await state()).scene.text assert.equal(
await evaluate( await evaluate(
`(() => { const input = document.querySelector('#scene-text'); input.value = 'Черновик ещё не показан'; input.dispatchEvent(new Event('input', {bubbles:true})) })()`, `!!document.querySelector(${JSON.stringify('#scene-text, #timer-label, #timer-seconds, [aria-label="Режим показа"]')})`,
),
false,
) )
await delay(100) assert.equal(await evaluate('document.body.textContent.includes("Текст и таймер")'), false)
assert.equal((await state()).scene.text, publishedText)
const safeText = 'Привет\n<script>window.__unsafe = true</script>' const safeText = 'Привет\n<script>window.__unsafe = true</script>'
await command({ action: 'text', text: safeText }) await command({ action: 'text', text: safeText })
await sceneReady(8) await sceneReady(8)
-4
View File
@@ -4,7 +4,6 @@ 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; selectedName: string | null }>() defineProps<{ outputOpen: boolean; selectedKey: string | null; selectedName: string | null }>()
const emit = defineEmits<{ packagesChanged: [] }>()
const bridge = window.screenBridge const bridge = window.screenBridge
const message = useMessage() const message = useMessage()
const isReady = ref(false) const isReady = ref(false)
@@ -45,8 +44,6 @@ const connectionLabels = {
let timer: ReturnType<typeof setInterval> | undefined let timer: ReturnType<typeof setInterval> | undefined
let refreshing = false let refreshing = false
function accept(value: OnlineState) { function accept(value: OnlineState) {
if (state.value && value.packagesRevision !== state.value.packagesRevision)
emit('packagesChanged')
state.value = value state.value = value
isReady.value = true isReady.value = true
} }
@@ -72,7 +69,6 @@ async function run(intent: OnlineIntent) {
if (intent.action === 'login') password.value = '' if (intent.action === 'login') password.value = ''
isBusy.value = false isBusy.value = false
await refresh() await refresh()
emit('packagesChanged')
} }
} }
async function cancel() { async function cancel() {
+6 -145
View File
@@ -4,7 +4,6 @@ import {
NButton, NButton,
NCard, NCard,
NInputNumber, NInputNumber,
NInput,
NSelect, NSelect,
NSpace, NSpace,
NSwitch, NSwitch,
@@ -16,10 +15,8 @@ import type {
Channel, Channel,
HostInfo, HostInfo,
Intent, Intent,
PackageSummary,
Playback, Playback,
PreviewFrame, PreviewFrame,
ScreenMode,
State, State,
ScreenStep, ScreenStep,
PlanStatus, PlanStatus,
@@ -27,7 +24,6 @@ import type {
import ConnectionPanel from './ConnectionPanel.vue' import ConnectionPanel from './ConnectionPanel.vue'
import ScreenPlanPanel from './ScreenPlanPanel.vue' import ScreenPlanPanel from './ScreenPlanPanel.vue'
import { modeLabels } from '../utils/presentation'
import { useScreenTimer } from '../composables/useScreenTimer' import { useScreenTimer } from '../composables/useScreenTimer'
const message = useMessage() const message = useMessage()
@@ -40,7 +36,6 @@ const originalPlan = ref('')
const isPlanDirty = computed(() => JSON.stringify(planDraft.value) !== originalPlan.value) const isPlanDirty = computed(() => JSON.stringify(planDraft.value) !== originalPlan.value)
const bridge = window.screenBridge const bridge = window.screenBridge
const state = ref<State | null>(null) const state = ref<State | null>(null)
const packages = ref<PackageSummary[]>([])
const host = ref<HostInfo>({ const host = ref<HostInfo>({
displays: [], displays: [],
notice: '', notice: '',
@@ -50,15 +45,8 @@ const host = ref<HostInfo>({
const display = ref<number | null>(null) const display = ref<number | null>(null)
const positions = ref<Record<Channel, number | null>>({ main: 10, music: 10 }) const positions = ref<Record<Channel, number | null>>({ main: 10, music: 10 })
const isBusy = ref(false) const isBusy = ref(false)
const textDraft = ref('')
const timerLabel = ref('Перерыв')
const timerSeconds = ref<number | null>(300)
const timerDisplay = useScreenTimer(() => state.value) const timerDisplay = useScreenTimer(() => state.value)
const preview = ref<PreviewFrame | null>(null) const preview = ref<PreviewFrame | null>(null)
const modeOptions = Object.entries(modeLabels).map(([key, label]) => ({
label,
value: Number(key) as ScreenMode,
}))
const previewURL = computed(() => const previewURL = computed(() =>
preview.value?.session === state.value?.session && preview.value?.session === state.value?.session &&
preview.value?.revision === state.value?.scene.revision preview.value?.revision === state.value?.scene.revision
@@ -110,10 +98,6 @@ const channels = ['main', 'music'] as const
const playlist = computed(() => const playlist = computed(() =>
state.value?.catalog.playlists.find((p) => p.id === state.value?.channels.music.playlistId), state.value?.catalog.playlists.find((p) => p.id === state.value?.channels.music.playlistId),
) )
const packageOptions = computed(() => [
{ label: 'Тестовый режим SCREEN-001', value: 'fixtures' },
...packages.value.map((p) => ({ label: p.name, value: p.key })),
])
const statusLabels: Record<Playback, string> = { const statusLabels: Record<Playback, string> = {
idle: 'Ресурс не выбран', idle: 'Ресурс не выбран',
pending: 'Подготовка или выполнение команды', pending: 'Подготовка или выполнение команды',
@@ -181,16 +165,6 @@ function acceptState(value: State) {
isPlanEditing.value = false isPlanEditing.value = false
planDraft.value = [] planDraft.value = []
} }
if (value.session !== state.value?.session || value.scene.text !== state.value?.scene.text)
textDraft.value = value.scene.text
if (
value.session !== state.value?.session ||
value.timer.label !== state.value?.timer.label ||
value.timer.durationMs !== state.value?.timer.durationMs
) {
timerLabel.value = value.timer.label
timerSeconds.value = value.timer.durationMs / 1000
}
if ( if (
!value.outputOpen || !value.outputOpen ||
value.session !== state.value?.session || value.session !== state.value?.session ||
@@ -221,30 +195,6 @@ async function send(intent: Intent) {
message.error(error instanceof Error ? error.message : 'Команда не выполнена') message.error(error instanceof Error ? error.message : 'Команда не выполнена')
} }
} }
function selectPackage(key: string) {
discardPlan(() => {
void changePackage(key)
})
}
async function changePackage(key: string) {
await run(async () => {
if (!bridge) return
try {
acceptState(await bridge.selectPackage(key === 'fixtures' ? null : key))
} finally {
packages.value = await bridge.packages()
}
})
}
async function refreshPackages() {
if (bridge) packages.value = await bridge.packages()
}
async function removePackage(key: string) {
await run(async () => {
await bridge?.onlineAction({ action: 'remove', key })
await refreshPackages()
})
}
async function refreshHost() { async function refreshHost() {
if (!bridge) return if (!bridge) return
host.value = await bridge.host() host.value = await bridge.host()
@@ -269,7 +219,6 @@ onMounted(async () => {
if (!bridge) throw new Error('Откройте пульт в приложении Electron') if (!bridge) throw new Error('Откройте пульт в приложении Electron')
unsubscribe = bridge.subscribe(acceptState) unsubscribe = bridge.subscribe(acceptState)
acceptState(await bridge.state()) acceptState(await bridge.state())
packages.value = await bridge.packages()
await refreshHost() await refreshHost()
}) })
previewTimer = setInterval(() => { previewTimer = setInterval(() => {
@@ -301,29 +250,7 @@ onUnmounted(() => {
: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" :selected-name="state?.catalog.name ?? null"
@packages-changed="refreshPackages"
/> />
<n-card title="Материалы показа">
<n-select
:value="state?.catalog.key ?? 'fixtures'"
:options="packageOptions"
:disabled="!bridge || isBusy || isPlanSaving"
:input-props="{ 'aria-label': 'Игра показа' }"
@update:value="selectPackage"
/>
<n-alert v-if="state && !state.catalog.key" type="info"
>Тестовый режим: синтетические аудио и видео.</n-alert
>
<n-space v-for="pkg in packages" :key="pkg.key">
<span>{{ pkg.name }}</span>
<n-button
:disabled="isBusy || pkg.key === state?.catalog.key"
@click="removePackage(pkg.key)"
>Удалить материалы</n-button
>
</n-space>
<p v-if="!packages.length">Готовых игровых пакетов на диске пока нет.</p>
</n-card>
<n-card v-if="bridge" title="Дисплей показа"> <n-card v-if="bridge" title="Дисплей показа">
<n-space vertical> <n-space vertical>
<n-alert v-if="host.notice" type="warning">{{ host.notice }}</n-alert> <n-alert v-if="host.notice" type="warning">{{ host.notice }}</n-alert>
@@ -385,15 +312,13 @@ onUnmounted(() => {
@update-step="(index, patch) => Object.assign(planDraft[index]!, patch)" @update-step="(index, patch) => Object.assign(planDraft[index]!, patch)"
@command="send" @command="send"
/> />
<label>Режим показа</label>
<n-select
:value="state.scene.mode"
:options="modeOptions"
:disabled="isBusy"
:input-props="{ 'aria-label': 'Режим показа' }"
@update:value="send({ action: 'mode', mode: $event })"
/>
<p role="status">{{ sceneLabels[state.scene.status] }}</p> <p role="status">{{ sceneLabels[state.scene.status] }}</p>
<p v-if="state.timer.durationMs > 0" role="status" class="timer-status">
{{ state.timer.label }} · {{ timerDisplay }} · {{ timerLabels[state.timer.status] }}
</p>
<p v-if="state.scene.mode !== 7 && state.timer.status === 'running'">
Таймер продолжает отсчёт в фоне.
</p>
<n-alert v-if="state.scene.error" type="error">{{ state.scene.error }}</n-alert> <n-alert v-if="state.scene.error" type="error">{{ state.scene.error }}</n-alert>
<n-button <n-button
v-if="state.scene.status === 'error'" v-if="state.scene.status === 'error'"
@@ -429,70 +354,6 @@ onUnmounted(() => {
<p>Кадр реального окна показа, без второго звука.</p> <p>Кадр реального окна показа, без второго звука.</p>
</n-card> </n-card>
</section> </section>
<n-card title="Текст и таймер">
<n-space vertical>
<label for="scene-text">Текст для показа</label>
<n-input
v-model:value="textDraft"
type="textarea"
:maxlength="10000"
:autosize="{ minRows: 2, maxRows: 6 }"
:input-props="{ id: 'scene-text' }"
/>
<n-button
:disabled="isBusy || !textDraft.trim()"
@click="send({ action: 'text', text: textDraft })"
>Показать текст</n-button
>
<label for="timer-label">Подпись таймера</label>
<n-input
v-model:value="timerLabel"
:maxlength="200"
:input-props="{ id: 'timer-label' }"
/>
<label for="timer-seconds">Длительность, секунды</label>
<n-input-number
v-model:value="timerSeconds"
:min="1"
:max="86400"
:precision="0"
:input-props="{ id: 'timer-seconds' }"
/>
<n-button
:disabled="isBusy || !timerSeconds"
@click="
send({ action: 'timer-set', label: timerLabel, durationSeconds: timerSeconds ?? 0 })
"
>Показать таймер</n-button
>
<p role="status" class="timer-status">
{{ state.timer.label }} · {{ timerDisplay }} · {{ timerLabels[state.timer.status] }}
</p>
<p v-if="state.scene.mode !== 7 && state.timer.status === 'running'">
Таймер продолжает отсчёт в фоне.
</p>
<n-space>
<n-button
:disabled="isBusy || state.timer.status === 'running'"
@click="send({ action: 'timer-start' })"
>Запустить таймер</n-button
>
<n-button
:disabled="isBusy || state.timer.status !== 'running'"
@click="send({ action: 'timer-pause' })"
>Пауза таймера</n-button
>
<n-button
:disabled="isBusy || state.timer.status !== 'paused'"
@click="send({ action: 'timer-resume' })"
>Продолжить таймер</n-button
>
<n-button :disabled="isBusy" @click="send({ action: 'timer-reset' })"
>Сбросить таймер</n-button
>
</n-space>
</n-space>
</n-card>
<label class="mute" <label class="mute"
>Выключить весь звук >Выключить весь звук
<n-switch <n-switch
+1 -1
View File
@@ -110,7 +110,7 @@ const modeOptions = Object.entries(modeLabels).map(([mode, label]) => ({
</n-space> </n-space>
</template> </template>
<template v-else> <template v-else>
<p v-if="!steps.length">План экранов ещё не создан. Используйте переключатель режимов.</p> <p v-if="!steps.length">План экранов ещё не создан. Создайте план, чтобы настроить экраны.</p>
<n-space <n-space
v-for="(step, index) in steps" v-for="(step, index) in steps"
:key="index" :key="index"