up
This commit is contained in:
@@ -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 не выполнялись.
|
||||||
|
|||||||
@@ -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; интервалы только
|
||||||
обновляют снимок. Пауза сохраняет точный остаток, сброс возвращает полную длительность.
|
обновляют снимок. Пауза сохраняет точный остаток, сброс возвращает полную длительность.
|
||||||
Таймер продолжает идти за другой сценой и при закрытом окне; смена пакета сбрасывает
|
Таймер продолжает идти за другой сценой и при закрытом окне; смена пакета сбрасывает
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
- [ ] Выбран архив своей ОС/архитектуры, SHA-256 совпал, приложение запускается.
|
- [ ] Выбран архив своей ОС/архитектуры, SHA-256 совпал, приложение запускается.
|
||||||
- [ ] В «Игра и сервер» выполнен вход, нужная игра загружена до завершения.
|
- [ ] В «Игра и сервер» выполнен вход, нужная игра загружена до завершения.
|
||||||
- [ ] В «Материалы показа» выбрана именно игра, а не тестовый режим.
|
- [ ] В блоке «Игра и сервер» подключена нужная игра; подготовка материалов завершена.
|
||||||
- [ ] Проверены обложка, шаги, вступление, видео, текст, таймер и нужные плейлисты.
|
- [ ] Проверены обложка, шаги, вступление, видео, текст, таймер и нужные плейлисты.
|
||||||
- [ ] Пробный показ и перезапуск без интернета подтвердили готовность локальной копии.
|
- [ ] Пробный показ и перезапуск без интернета подтвердили готовность локальной копии.
|
||||||
- [ ] Если нужны результаты, сохранён доступный снимок и проверена его дата.
|
- [ ] Если нужны результаты, сохранён доступный снимок и проверена его дата.
|
||||||
|
|||||||
@@ -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 текущей среды,
|
||||||
|
|||||||
@@ -25,10 +25,11 @@ SCREEN-001/SCREEN-004 остаётся открытой; эта инструкц
|
|||||||
При закрытом окне предпросмотр недоступен; скрытый/свёрнутый пульт не захватывает кадры.
|
При закрытом окне предпросмотр недоступен; скрытый/свёрнутый пульт не захватывает кадры.
|
||||||
- Проверьте обложку, затемнённый фон и кириллицу без интернета. На проекторе нет
|
- Проверьте обложку, затемнённый фон и кириллицу без интернета. На проекторе нет
|
||||||
кнопок, сообщений пульта, диагностических статусов и исходных путей файлов.
|
кнопок, сообщений пульта, диагностических статусов и исходных путей файлов.
|
||||||
- Введите текст с переносами строк, кириллицей и `<script>текст</script>`.
|
- В редакторе плана задайте текст шага с переносами строк, кириллицей и `<script>текст</script>`.
|
||||||
До «Показать текст» сцена не меняется; после применения это обычный текст,
|
До перехода к сохранённому шагу сцена не меняется; после перехода это обычный текст,
|
||||||
без выполнения HTML/JavaScript.
|
без выполнения HTML/JavaScript.
|
||||||
- Настройте таймер на 10 секунд. «Показать таймер» применяет параметры без старта.
|
- В редакторе плана настройте таймерный шаг на 10 секунд и сохраните план.
|
||||||
|
«Показать» у шага применяет параметры без старта.
|
||||||
Проверьте старт, паузу, ожидание на паузе, продолжение и сброс. При переходе
|
Проверьте старт, паузу, ожидание на паузе, продолжение и сброс. При переходе
|
||||||
к обложке отсчёт продолжается; при возвращении виден актуальный остаток.
|
к обложке отсчёт продолжается; при возвращении виден актуальный остаток.
|
||||||
В конце остаётся 00:00, без сигнала и автоматической смены режима.
|
В конце остаётся 00:00, без сигнала и автоматической смены режима.
|
||||||
|
|||||||
+11
-14
@@ -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
@@ -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,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() {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user