From e85d899fd7fd4534bfbc3431e4b20c61a5671d26 Mon Sep 17 00:00:00 2001 From: Fedorov Vladimir Date: Sun, 18 Oct 2026 04:03:44 +0700 Subject: [PATCH] up --- COMPLETED_TASKS.md | 17 +++++ README.md | 5 +- docs/SCREEN-004_ACCEPTANCE.md | 9 ++- docs/USER_GUIDE.md | 5 ++ scripts/smoke.cjs | 53 ++++++++++++- src/components/ControlPage.vue | 115 ++++++++++++++++++----------- src/components/ScreenPlanPanel.vue | 15 ++-- 7 files changed, 160 insertions(+), 59 deletions(-) diff --git a/COMPLETED_TASKS.md b/COMPLETED_TASKS.md index d5b5188..c0d8e52 100644 --- a/COMPLETED_TASKS.md +++ b/COMPLETED_TASKS.md @@ -297,3 +297,20 @@ 33 локальные Markdown-ссылки и git diff --check — PASS. - Границы: изменения только evening_detective_screen. Архивы не пересобирались; физический проектор не проверялся. Commit/push не выполнялись. + +## SCREEN-015 — Управление вступлением и видео через план + +- Статус: `DONE`. +- Дата: 2026-10-18. +- Результат: удалён отдельный блок «Вступление и видео» и ручной выбор медиа. + Файл определяется режимом шага. В активном шаге «Введение»/«Закрывашка» доступны + управление воспроизведением, громкость, перемотка, позиция, длительность, + статус, ошибка и прежние программные замеры основного канала. +- Воспроизведение и перемотка требуют открытого показа и выбранного файла. + Музыка и общий mute сохранены отдельно. Серверный API и типы команд не менялись. +- Проверки: type-check, lint, lint:css, сборка, Electron GUI smoke — PASS. + GUI проверяет отсутствие блока, запуск/паузу/стоп в обоих медийных шагах, + перемотку и громкость на паузе, независимость музыки, план и синхронизацию. + Документация обновлена; локальные Markdown-ссылки и git diff --check — PASS. +- Границы: изменения только evening_detective_screen. Архивы не пересобирались; + физический проектор не проверялся. Commit/push не выполнялись. diff --git a/README.md b/README.md index d312884..c2763d5 100644 --- a/README.md +++ b/README.md @@ -189,8 +189,9 @@ URL, независимо от исходного имени файла. Лок ## Локальный проигрыватель SCREEN-004 Подключите игру в блоке «Игра и сервер», выберите внешний дисплей -и откройте показ. В основном канале доступны вступление и заключительное видео; -их выбор останавливает прежнее основное медиа. Музыка воспроизводится отдельным +и откройте показ. В активном шаге «Введение» или «Закрывашка» доступны управление воспроизведением, +громкость, перемотка и статус основного канала. Файл определяется режимом шага; +отдельного блока выбора медиа нет. Смена шага останавливает прежнее основное медиа. Музыка воспроизводится отдельным каналом без автоматического приглушения. Звук выводится только окном показа. Начальная громкость каждого канала — 35%; общий mute не меняет сохранённые уровни. diff --git a/docs/SCREEN-004_ACCEPTANCE.md b/docs/SCREEN-004_ACCEPTANCE.md index 10edad1..89374db 100644 --- a/docs/SCREEN-004_ACCEPTANCE.md +++ b/docs/SCREEN-004_ACCEPTANCE.md @@ -22,11 +22,12 @@ SCREEN-004 остаётся IN_PROGRESS до результатов владел ## Сценарии -- Выберите вступление, дождитесь готовности, проверьте пуск с начала, паузу, +- Покажите шаг «Введение» в плане, дождитесь готовности; в активном шаге + проверьте «Запуск», паузу, продолжение, повтор с начала и остановку. Пауза не должна отматывать звук назад. - Перемотайте на середину в воспроизведении и на паузе; затем на конец. Проверьте фактическую позицию и сохранение состояния паузы/воспроизведения. -- Переключитесь на заключительное видео: вступление прекращается. Повторите +- Покажите шаг «Закрывашка»: вступление прекращается. В активном шаге повторите команды и перемотку, проверьте изображение и звук на внешнем дисплее. - Запустите музыку вместе с видео/вступлением: оба канала слышны независимо, автоматического приглушения нет. Изменение громкости одного канала не должно @@ -39,8 +40,8 @@ SCREEN-004 остаётся IN_PROGRESS до результатов владел поздний play не оживляет предыдущий ресурс. - Закройте окно показа и откройте снова. Проверьте оба канала: звук не начинается до явной команды, позиции сброшены. Пульт и технические ошибки не видны на проекторе. -- Проверьте отдельный тестовый режим SCREEN-001. Он не должен подменять выбранный - игровой пакет автоматически во время нормального воспроизведения. +- Убедитесь, что отдельного блока «Вступление и видео» и выбора медиа нет: + громкость, перемотка и статус находятся в активном медийном шаге плана. ## Измерение задержки diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index 2e0d0df..926fa66 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -125,6 +125,11 @@ Windows 11, macOS 13+, Ubuntu 24.04 LTS или Debian 13. Эти требова | «Следующий трек» | Переключает музыку, сохраняя паузу или воспроизведение. | | «Повторять плейлист» | После последнего трека возвращается к первому. | +Вступление и видео управляются в активном шаге «Введение» или «Закрывашка»: +«Запуск», «Пауза», «Продолжить», «Стоп», «Повтор», громкость и перемотка. +Здесь же видны позиция, длительность, статус и ошибка воспроизведения. +Файл определяется режимом шага; отдельного выбора медиа нет. +Кнопки воспроизведения и перемотка доступны при открытом показе и выбранном файле. Вступление и видео используют один основной канал и не звучат одновременно. Музыка независима, автоматически не приглушается; перед речью уменьшайте её громкость сами. Смена сцены останавливает основное медиа, музыка продолжает играть. diff --git a/scripts/smoke.cjs b/scripts/smoke.cjs index fe32d33..c656f11 100644 --- a/scripts/smoke.cjs +++ b/scripts/smoke.cjs @@ -257,10 +257,8 @@ app.whenReady().then(async () => { } } await checkDisplay(output) - await evaluate( - `Array.from(document.querySelectorAll('button')).find(b => b.textContent.includes('Пуск с начала')).click()`, - ) - await poll(async () => (await state()).channels.main.status === 'playing', 'UI fixture play') + await command({ action: 'play', channel: 'main' }) + await poll(async () => (await state()).channels.main.status === 'playing', 'fixture play') await command({ action: 'pause', channel: 'main' }) await command({ action: 'resume', channel: 'main' }) await command({ action: 'stop', channel: 'main' }) @@ -275,6 +273,7 @@ app.whenReady().then(async () => { assert.deepEqual(range, { status: 206, size: 100 }) await command({ action: 'pause', channel: 'main' }) assert.equal(await evaluate('document.body.textContent.includes("Материалы показа")'), false) + 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() @@ -403,6 +402,52 @@ app.whenReady().then(async () => { if (index === 0) assert.equal(await inspect('document.querySelector("img.cover").naturalWidth'), 320) else assert.equal(await inspect('document.querySelector("img.background").naturalWidth'), 320) + if (index === 1 || index === 3) { + const clickMedia = (label) => + evaluate(`(() => { + const row = document.querySelector('[data-step-index="${index}"]') + const button = Array.from(row.querySelectorAll('button')).find(b => b.textContent.trim() === ${JSON.stringify(label)}) + if (!button || button.disabled) throw new Error('Main control unavailable') + button.click() + })()`) + await clickMedia('Запуск') + await poll( + async () => (await state()).channels.main.status === 'playing', + 'plan media plays', + ) + await clickMedia('Пауза') + await poll( + async () => (await state()).channels.main.status === 'paused', + 'plan media paused', + ) + await evaluate(`(() => { + const input = document.querySelector('#position-main') + input.focus(); input.value = '1' + input.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText' })) + input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true })) + input.blur() + })()`) + await clickMedia('Перемотать') + await poll( + async () => Math.abs((await state()).channels.main.position - 1) < 0.1, + 'plan media seek', + ) + await evaluate(`(() => { + const input = document.querySelector('#volume-main') + input.focus(); input.value = '42' + input.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText' })) + input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true })) + input.blur() + })()`) + await poll(async () => (await state()).channels.main.volume === 42, 'plan media volume') + assert.equal((await state()).channels.main.desired, 'paused') + assert.equal((await state()).channels.music.desired, 'playing') + await clickMedia('Стоп') + await poll( + async () => (await state()).channels.main.status === 'stopped', + 'plan media stopped', + ) + } if (index === 2) assert.ok(await inspect('document.body.textContent.includes("Успехов")')) if (index === 4) assert.ok(await inspect('document.body.textContent.includes("Спасибо за игру!")')) diff --git a/src/components/ControlPage.vue b/src/components/ControlPage.vue index 0398134..8ebce77 100644 --- a/src/components/ControlPage.vue +++ b/src/components/ControlPage.vue @@ -94,7 +94,7 @@ async function refreshPreview() { } } -const channels = ['main', 'music'] as const +const channel = 'music' as const const playlist = computed(() => state.value?.catalog.playlists.find((p) => p.id === state.value?.channels.music.playlistId), ) @@ -296,6 +296,7 @@ onUnmounted(() => { :draft="planDraft" :is-editing="isPlanEditing" :is-busy="isBusy || isPlanSaving" + :can-play-main="state.outputOpen && !!state.channels.main.source" :can-edit="planStatus.key === state.catalog.key && planStatus.editable" :notice="planStatus.key === state.catalog.key ? planStatus.notice : ''" @edit="editPlan" @@ -311,7 +312,49 @@ onUnmounted(() => { @clear="planDraft = []" @update-step="(index, patch) => Object.assign(planDraft[index]!, patch)" @command="send" - /> + > + +

{{ sceneLabels[state.scene.status] }}

{{ state.timer.label }} · {{ timerDisplay }} · {{ timerLabels[state.timer.status] }} @@ -361,51 +404,36 @@ onUnmounted(() => { aria-label="Выключить весь звук" @update:value="send({ action: 'mute', enabled: $event })" /> - + - + + { >Повторить с начала Следующий трек() @@ -150,32 +151,36 @@ const modeOptions = Object.entries(modeLabels).map(([mode, label]) => ({ +