up
This commit is contained in:
@@ -265,3 +265,35 @@
|
||||
сервер и физический проектор/звук не проверялись. Замена байтов по прежнему
|
||||
URL требует ручной повторной загрузки; точность позиции ограничена API сервера.
|
||||
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.
|
||||
Тестовый режим сохраняет отдельный выбор WAV/видео; игровые пакеты выбираются
|
||||
в разделе «Материалы показа». В блоке «Игра и сервер» доступны вход, список игр и загрузка материалов.
|
||||
В блоке «Игра и сервер» доступны вход, выбор игры и подготовка материалов.
|
||||
|
||||
Для постоянной связи войдите, выберите игру в серверном списке и нажмите
|
||||
«Подключить выбранную игру». Готовый пакет заранее не требуется: сначала сохраняются
|
||||
@@ -189,7 +188,7 @@ URL, независимо от исходного имени файла. Лок
|
||||
|
||||
## Локальный проигрыватель SCREEN-004
|
||||
|
||||
На пульте выберите готовый пакет или «Тестовый режим SCREEN-001», внешний дисплей
|
||||
Подключите игру в блоке «Игра и сервер», выберите внешний дисплей
|
||||
и откройте показ. В основном канале доступны вступление и заключительное видео;
|
||||
их выбор останавливает прежнее основное медиа. Музыка воспроизводится отдельным
|
||||
каналом без автоматического приглушения. Звук выводится только окном показа.
|
||||
@@ -234,17 +233,17 @@ URL, независимо от исходного имени файла. Лок
|
||||
|
||||
## Пульт и режимы SCREEN-005
|
||||
|
||||
Пульт включает готовый пакет, план шагов, переключатель восьми режимов, текст,
|
||||
таймер, предпросмотр и прежние независимые каналы медиа/музыки. План экранов редактируется в пульте и сохраняется на сервере выбранной игры;
|
||||
Пульт включает подключение игры через сервер, план шагов с восемью режимами, текстом и параметрами таймера,
|
||||
предпросмотр и прежние независимые каналы медиа/музыки. План экранов редактируется в пульте и сохраняется на сервере выбранной игры;
|
||||
последняя подтверждённая копия хранится отдельно от пакета для офлайн-показа.
|
||||
Онлайн-переход меняет общий шаг, офлайн-переход остаётся локальным и не отправляется
|
||||
позже. Восстановление сети само не переключает показ. Выбор шага применяет его параметры без автоматического
|
||||
позже. При восстановлении сети применяется актуальное состояние сервера. Выбор шага применяет его параметры без автоматического
|
||||
запуска звука/таймера; окончание медиа не переключает шаг. Пуск основного ресурса
|
||||
показывает соответствующий режим вступления или видео. Уход в другую сцену
|
||||
останавливает основной канал, сохраняя независимую музыку.
|
||||
|
||||
Текст и параметры таймера редактируются как черновик; только «Показать текст» /
|
||||
«Показать таймер» публикуют их. Текст отображается как обычный текст, без HTML.
|
||||
Режим, текст и параметры таймера задаются только в редакторе шагов плана.
|
||||
Сохранение черновика не меняет показ; для перехода нажмите «Показать» у шага. Текст отображается как обычный текст, без HTML.
|
||||
Таймер вычисляется в основном процессе по монотонному deadline; интервалы только
|
||||
обновляют снимок. Пауза сохраняет точный остаток, сброс возвращает полную длительность.
|
||||
Таймер продолжает идти за другой сценой и при закрытом окне; смена пакета сбрасывает
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
- [ ] Выбран архив своей ОС/архитектуры, SHA-256 совпал, приложение запускается.
|
||||
- [ ] В «Игра и сервер» выполнен вход, нужная игра загружена до завершения.
|
||||
- [ ] В «Материалы показа» выбрана именно игра, а не тестовый режим.
|
||||
- [ ] В блоке «Игра и сервер» подключена нужная игра; подготовка материалов завершена.
|
||||
- [ ] Проверены обложка, шаги, вступление, видео, текст, таймер и нужные плейлисты.
|
||||
- [ ] Пробный показ и перезапуск без интернета подтвердили готовность локальной копии.
|
||||
- [ ] Если нужны результаты, сохранён доступный снимок и проверена его дата.
|
||||
|
||||
@@ -12,8 +12,8 @@ SCREEN-004 остаётся IN_PROGRESS до результатов владел
|
||||
на основном дисплее компьютера. Ручная проверка не подставляет синтетический монитор.
|
||||
3. Команда создаст отдельный временный каталог данных и пакет «Offline smoke»:
|
||||
WAV-вступление, H.264/AAC-видео и два четырёхсекундных тональных трека.
|
||||
Путь данных печатается как SCREEN004_ACCEPTANCE_READY. Откройте пакет через
|
||||
«Материалы показа», выберите внешний дисплей и нажмите «Открыть показ».
|
||||
Путь данных печатается как SCREEN004_ACCEPTANCE_READY. Пакет выбирается автоматически
|
||||
проверочным сценарием; выберите внешний дисплей и нажмите «Открыть показ».
|
||||
4. Отключите сеть после запуска. Не используйте --mute-audio для физической проверки.
|
||||
Установите подходящую громкость и проверьте нужный аудиовыход ОС. Параметры
|
||||
--no-sandbox/--disable-gpu, применённые в автоматическом smoke текущей среды,
|
||||
|
||||
@@ -25,10 +25,11 @@ SCREEN-001/SCREEN-004 остаётся открытой; эта инструкц
|
||||
При закрытом окне предпросмотр недоступен; скрытый/свёрнутый пульт не захватывает кадры.
|
||||
- Проверьте обложку, затемнённый фон и кириллицу без интернета. На проекторе нет
|
||||
кнопок, сообщений пульта, диагностических статусов и исходных путей файлов.
|
||||
- Введите текст с переносами строк, кириллицей и `<script>текст</script>`.
|
||||
До «Показать текст» сцена не меняется; после применения это обычный текст,
|
||||
- В редакторе плана задайте текст шага с переносами строк, кириллицей и `<script>текст</script>`.
|
||||
До перехода к сохранённому шагу сцена не меняется; после перехода это обычный текст,
|
||||
без выполнения HTML/JavaScript.
|
||||
- Настройте таймер на 10 секунд. «Показать таймер» применяет параметры без старта.
|
||||
- В редакторе плана настройте таймерный шаг на 10 секунд и сохраните план.
|
||||
«Показать» у шага применяет параметры без старта.
|
||||
Проверьте старт, паузу, ожидание на паузе, продолжение и сброс. При переходе
|
||||
к обложке отсчёт продолжается; при возвращении виден актуальный остаток.
|
||||
В конце остаётся 00:00, без сигнала и автоматической смены режима.
|
||||
|
||||
+11
-14
@@ -68,7 +68,6 @@ Windows 11, macOS 13+, Ubuntu 24.04 LTS или Debian 13. Эти требова
|
||||
включая воспроизведение. До готовности нового файла прежнее содержимое канала
|
||||
сохраняется. Все медиа читаются только с локального диска.
|
||||
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 без сигнала и автоматического перехода.
|
||||
- После завершения игры при доступной сети нажмите «Обновить снимок результатов»
|
||||
в «Игра и сервер» (кнопка доступна после подключения управления).
|
||||
Выберите режим «Результаты» и проверьте дату снимка. Офлайн показывается
|
||||
Покажите шаг «Результаты» и проверьте дату снимка. Офлайн показывается
|
||||
последний сохранённый снимок, а не обновляемые серверные результаты.
|
||||
Неудачное обновление сохраняет прежний снимок.
|
||||
|
||||
@@ -179,7 +177,7 @@ Windows 11, macOS 13+, Ubuntu 24.04 LTS или Debian 13. Эти требова
|
||||
| Проектор отключился, изменилось разрешение или произошёл сон | Прочитайте причину в пульте, восстановите дисплей/выход звука, выберите внешний экран и явно откройте показ. Медиа остановлены, работающий таймер сохранён на паузе. |
|
||||
| Нет звука | Проверьте выход и mute ОС, кабель/USB/HDMI, «Выключить весь звук», громкость канала, открытое окно и статус проигрывателя. Для серверных команд проверьте назначение звука экрану. |
|
||||
| Ошибка подготовки сцены | Проверьте сообщение и нажмите «Повторить подготовку». При повторении проверьте пакет до продолжения игры. |
|
||||
| Пакет повреждён или отсутствует файл | При доступной сети закройте показ и загрузите материалы заново. Для удаления выберите другой пакет или тестовый режим, затем «Удалить материалы» у нужной игры. Без сети восстановить отсутствующие байты нельзя. |
|
||||
| Пакет повреждён или отсутствует файл | При доступной сети нажмите «Повторить подготовку». Для полной повторной загрузки закройте показ и загрузите материалы заново. Без сети восстановить отсутствующие байты нельзя. |
|
||||
| Истёк вход или сессия экрана | Повторно войдите или подключите управление по сообщению пульта. Готовые материалы остаются на диске. |
|
||||
| Приложение перезапущено | Выбранная игра и сохранённые результаты восстанавливаются. Откройте показ вручную: онлайн применяется текущее состояние сервера, офлайн запускайте медиа локально. |
|
||||
| Пульт пуст или зависает при запуске | Запишите ОС, версию, симптомы и сообщения запуска, передайте отчёт. Не считайте такой запуск успешным. |
|
||||
@@ -203,8 +201,7 @@ Windows 11, macOS 13+, Ubuntu 24.04 LTS или Debian 13. Эти требова
|
||||
|
||||
«Выйти» отключает серверное управление, но не удаляет материалы и не прерывает
|
||||
локальное воспроизведение. Для завершения остановите медиа, закройте показ
|
||||
и приложение. «Удалить материалы» используйте только для пакетов, которые
|
||||
больше не нужны: это удаление локальной копии.
|
||||
и приложение. Скачанные материалы сохраняются в локальном хранилище.
|
||||
|
||||
## Журналы и обращение за помощью
|
||||
|
||||
|
||||
+9
-15
@@ -214,6 +214,7 @@ app.whenReady().then(async () => {
|
||||
'control UI',
|
||||
)
|
||||
if (manual) {
|
||||
await evaluate(`window.screenBridge.selectPackage(${JSON.stringify(offlinePackage.key)})`)
|
||||
console.log(
|
||||
'SCREEN004_ACCEPTANCE_READY',
|
||||
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 })
|
||||
await command({ action: 'pause', channel: 'main' })
|
||||
await evaluate("document.querySelector('.n-base-selection').click()")
|
||||
await poll(
|
||||
() =>
|
||||
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',
|
||||
)
|
||||
assert.equal(await evaluate('document.body.textContent.includes("Материалы показа")'), false)
|
||||
await evaluate(`window.screenBridge.selectPackage(${JSON.stringify(offlinePackage.key)})`)
|
||||
await poll(async () => {
|
||||
const s = await state()
|
||||
return (
|
||||
@@ -424,12 +417,13 @@ app.whenReady().then(async () => {
|
||||
if (index === 7)
|
||||
assert.ok(await inspect('document.body.textContent.includes("Кириллица: Ёжик и улика №7")'))
|
||||
}
|
||||
const publishedText = (await state()).scene.text
|
||||
await evaluate(
|
||||
`(() => { const input = document.querySelector('#scene-text'); input.value = 'Черновик ещё не показан'; input.dispatchEvent(new Event('input', {bubbles:true})) })()`,
|
||||
assert.equal(
|
||||
await evaluate(
|
||||
`!!document.querySelector(${JSON.stringify('#scene-text, #timer-label, #timer-seconds, [aria-label="Режим показа"]')})`,
|
||||
),
|
||||
false,
|
||||
)
|
||||
await delay(100)
|
||||
assert.equal((await state()).scene.text, publishedText)
|
||||
assert.equal(await evaluate('document.body.textContent.includes("Текст и таймер")'), false)
|
||||
const safeText = 'Привет\n<script>window.__unsafe = true</script>'
|
||||
await command({ action: 'text', text: safeText })
|
||||
await sceneReady(8)
|
||||
|
||||
@@ -4,7 +4,6 @@ import { onMounted, onUnmounted, ref } from 'vue'
|
||||
import type { OnlineIntent, OnlineState } from '../types/online'
|
||||
|
||||
defineProps<{ outputOpen: boolean; selectedKey: string | null; selectedName: string | null }>()
|
||||
const emit = defineEmits<{ packagesChanged: [] }>()
|
||||
const bridge = window.screenBridge
|
||||
const message = useMessage()
|
||||
const isReady = ref(false)
|
||||
@@ -45,8 +44,6 @@ const connectionLabels = {
|
||||
let timer: ReturnType<typeof setInterval> | undefined
|
||||
let refreshing = false
|
||||
function accept(value: OnlineState) {
|
||||
if (state.value && value.packagesRevision !== state.value.packagesRevision)
|
||||
emit('packagesChanged')
|
||||
state.value = value
|
||||
isReady.value = true
|
||||
}
|
||||
@@ -72,7 +69,6 @@ async function run(intent: OnlineIntent) {
|
||||
if (intent.action === 'login') password.value = ''
|
||||
isBusy.value = false
|
||||
await refresh()
|
||||
emit('packagesChanged')
|
||||
}
|
||||
}
|
||||
async function cancel() {
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
NButton,
|
||||
NCard,
|
||||
NInputNumber,
|
||||
NInput,
|
||||
NSelect,
|
||||
NSpace,
|
||||
NSwitch,
|
||||
@@ -16,10 +15,8 @@ import type {
|
||||
Channel,
|
||||
HostInfo,
|
||||
Intent,
|
||||
PackageSummary,
|
||||
Playback,
|
||||
PreviewFrame,
|
||||
ScreenMode,
|
||||
State,
|
||||
ScreenStep,
|
||||
PlanStatus,
|
||||
@@ -27,7 +24,6 @@ import type {
|
||||
|
||||
import ConnectionPanel from './ConnectionPanel.vue'
|
||||
import ScreenPlanPanel from './ScreenPlanPanel.vue'
|
||||
import { modeLabels } from '../utils/presentation'
|
||||
import { useScreenTimer } from '../composables/useScreenTimer'
|
||||
|
||||
const message = useMessage()
|
||||
@@ -40,7 +36,6 @@ const originalPlan = ref('')
|
||||
const isPlanDirty = computed(() => JSON.stringify(planDraft.value) !== originalPlan.value)
|
||||
const bridge = window.screenBridge
|
||||
const state = ref<State | null>(null)
|
||||
const packages = ref<PackageSummary[]>([])
|
||||
const host = ref<HostInfo>({
|
||||
displays: [],
|
||||
notice: '',
|
||||
@@ -50,15 +45,8 @@ const host = ref<HostInfo>({
|
||||
const display = ref<number | null>(null)
|
||||
const positions = ref<Record<Channel, number | null>>({ main: 10, music: 10 })
|
||||
const isBusy = ref(false)
|
||||
const textDraft = ref('')
|
||||
const timerLabel = ref('Перерыв')
|
||||
const timerSeconds = ref<number | null>(300)
|
||||
const timerDisplay = useScreenTimer(() => state.value)
|
||||
const preview = ref<PreviewFrame | null>(null)
|
||||
const modeOptions = Object.entries(modeLabels).map(([key, label]) => ({
|
||||
label,
|
||||
value: Number(key) as ScreenMode,
|
||||
}))
|
||||
const previewURL = computed(() =>
|
||||
preview.value?.session === state.value?.session &&
|
||||
preview.value?.revision === state.value?.scene.revision
|
||||
@@ -110,10 +98,6 @@ const channels = ['main', 'music'] as const
|
||||
const playlist = computed(() =>
|
||||
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> = {
|
||||
idle: 'Ресурс не выбран',
|
||||
pending: 'Подготовка или выполнение команды',
|
||||
@@ -181,16 +165,6 @@ function acceptState(value: State) {
|
||||
isPlanEditing.value = false
|
||||
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 (
|
||||
!value.outputOpen ||
|
||||
value.session !== state.value?.session ||
|
||||
@@ -221,30 +195,6 @@ async function send(intent: Intent) {
|
||||
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() {
|
||||
if (!bridge) return
|
||||
host.value = await bridge.host()
|
||||
@@ -269,7 +219,6 @@ onMounted(async () => {
|
||||
if (!bridge) throw new Error('Откройте пульт в приложении Electron')
|
||||
unsubscribe = bridge.subscribe(acceptState)
|
||||
acceptState(await bridge.state())
|
||||
packages.value = await bridge.packages()
|
||||
await refreshHost()
|
||||
})
|
||||
previewTimer = setInterval(() => {
|
||||
@@ -301,29 +250,7 @@ onUnmounted(() => {
|
||||
:output-open="state?.outputOpen ?? false"
|
||||
:selected-key="state?.catalog.key ?? 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-space vertical>
|
||||
<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)"
|
||||
@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 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-button
|
||||
v-if="state.scene.status === 'error'"
|
||||
@@ -429,70 +354,6 @@ onUnmounted(() => {
|
||||
<p>Кадр реального окна показа, без второго звука.</p>
|
||||
</n-card>
|
||||
</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"
|
||||
>Выключить весь звук
|
||||
<n-switch
|
||||
|
||||
@@ -110,7 +110,7 @@ const modeOptions = Object.entries(modeLabels).map(([mode, label]) => ({
|
||||
</n-space>
|
||||
</template>
|
||||
<template v-else>
|
||||
<p v-if="!steps.length">План экранов ещё не создан. Используйте переключатель режимов.</p>
|
||||
<p v-if="!steps.length">План экранов ещё не создан. Создайте план, чтобы настроить экраны.</p>
|
||||
<n-space
|
||||
v-for="(step, index) in steps"
|
||||
:key="index"
|
||||
|
||||
Reference in New Issue
Block a user