From 9c5419b9bfd69e95e325acc869902b7e3395bb7a Mon Sep 17 00:00:00 2001 From: Fedorov Vladimir Date: Sun, 18 Oct 2026 03:32:50 +0700 Subject: [PATCH] up --- COMPLETED_TASKS.md | 32 ++++++ README.md | 15 ++- docs/EVENT_CHECKLIST.md | 2 +- docs/SCREEN-004_ACCEPTANCE.md | 4 +- docs/SCREEN-005_ACCEPTANCE.md | 7 +- docs/USER_GUIDE.md | 25 +++-- scripts/smoke.cjs | 24 ++--- src/components/ConnectionPanel.vue | 4 - src/components/ControlPage.vue | 151 ++--------------------------- src/components/ScreenPlanPanel.vue | 2 +- 10 files changed, 73 insertions(+), 193 deletions(-) diff --git a/COMPLETED_TASKS.md b/COMPLETED_TASKS.md index ce24209..d5b5188 100644 --- a/COMPLETED_TASKS.md +++ b/COMPLETED_TASKS.md @@ -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 не выполнялись. diff --git a/README.md b/README.md index 88be6bc..d312884 100644 --- a/README.md +++ b/README.md @@ -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; интервалы только обновляют снимок. Пауза сохраняет точный остаток, сброс возвращает полную длительность. Таймер продолжает идти за другой сценой и при закрытом окне; смена пакета сбрасывает diff --git a/docs/EVENT_CHECKLIST.md b/docs/EVENT_CHECKLIST.md index 72a3af4..0ff08b0 100644 --- a/docs/EVENT_CHECKLIST.md +++ b/docs/EVENT_CHECKLIST.md @@ -8,7 +8,7 @@ - [ ] Выбран архив своей ОС/архитектуры, SHA-256 совпал, приложение запускается. - [ ] В «Игра и сервер» выполнен вход, нужная игра загружена до завершения. -- [ ] В «Материалы показа» выбрана именно игра, а не тестовый режим. +- [ ] В блоке «Игра и сервер» подключена нужная игра; подготовка материалов завершена. - [ ] Проверены обложка, шаги, вступление, видео, текст, таймер и нужные плейлисты. - [ ] Пробный показ и перезапуск без интернета подтвердили готовность локальной копии. - [ ] Если нужны результаты, сохранён доступный снимок и проверена его дата. diff --git a/docs/SCREEN-004_ACCEPTANCE.md b/docs/SCREEN-004_ACCEPTANCE.md index f3fc0b4..10edad1 100644 --- a/docs/SCREEN-004_ACCEPTANCE.md +++ b/docs/SCREEN-004_ACCEPTANCE.md @@ -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 текущей среды, diff --git a/docs/SCREEN-005_ACCEPTANCE.md b/docs/SCREEN-005_ACCEPTANCE.md index 34e56cb..8845f5d 100644 --- a/docs/SCREEN-005_ACCEPTANCE.md +++ b/docs/SCREEN-005_ACCEPTANCE.md @@ -25,10 +25,11 @@ SCREEN-001/SCREEN-004 остаётся открытой; эта инструкц При закрытом окне предпросмотр недоступен; скрытый/свёрнутый пульт не захватывает кадры. - Проверьте обложку, затемнённый фон и кириллицу без интернета. На проекторе нет кнопок, сообщений пульта, диагностических статусов и исходных путей файлов. -- Введите текст с переносами строк, кириллицей и ``. - До «Показать текст» сцена не меняется; после применения это обычный текст, +- В редакторе плана задайте текст шага с переносами строк, кириллицей и ``. + До перехода к сохранённому шагу сцена не меняется; после перехода это обычный текст, без выполнения HTML/JavaScript. -- Настройте таймер на 10 секунд. «Показать таймер» применяет параметры без старта. +- В редакторе плана настройте таймерный шаг на 10 секунд и сохраните план. + «Показать» у шага применяет параметры без старта. Проверьте старт, паузу, ожидание на паузе, продолжение и сброс. При переходе к обложке отсчёт продолжается; при возвращении виден актуальный остаток. В конце остаётся 00:00, без сигнала и автоматической смены режима. diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index 9b9ade6..2e0d0df 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -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. Эти требова «Выйти» отключает серверное управление, но не удаляет материалы и не прерывает локальное воспроизведение. Для завершения остановите медиа, закройте показ -и приложение. «Удалить материалы» используйте только для пакетов, которые -больше не нужны: это удаление локальной копии. +и приложение. Скачанные материалы сохраняются в локальном хранилище. ## Журналы и обращение за помощью diff --git a/scripts/smoke.cjs b/scripts/smoke.cjs index 355bdc2..fe32d33 100644 --- a/scripts/smoke.cjs +++ b/scripts/smoke.cjs @@ -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' await command({ action: 'text', text: safeText }) await sceneReady(8) diff --git a/src/components/ConnectionPanel.vue b/src/components/ConnectionPanel.vue index 29e020a..ca1c8d4 100644 --- a/src/components/ConnectionPanel.vue +++ b/src/components/ConnectionPanel.vue @@ -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 | 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() { diff --git a/src/components/ControlPage.vue b/src/components/ControlPage.vue index 71435fa..0398134 100644 --- a/src/components/ControlPage.vue +++ b/src/components/ControlPage.vue @@ -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(null) -const packages = ref([]) const host = ref({ displays: [], notice: '', @@ -50,15 +45,8 @@ const host = ref({ const display = ref(null) const positions = ref>({ main: 10, music: 10 }) const isBusy = ref(false) -const textDraft = ref('') -const timerLabel = ref('Перерыв') -const timerSeconds = ref(300) const timerDisplay = useScreenTimer(() => state.value) const preview = ref(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 = { 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" /> - - - Тестовый режим: синтетические аудио и видео. - - {{ pkg.name }} - Удалить материалы - -

Готовых игровых пакетов на диске пока нет.

-
{{ host.notice }} @@ -385,15 +312,13 @@ onUnmounted(() => { @update-step="(index, patch) => Object.assign(planDraft[index]!, patch)" @command="send" /> - -

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

+

+ {{ state.timer.label }} · {{ timerDisplay }} · {{ timerLabels[state.timer.status] }} +

+

+ Таймер продолжает отсчёт в фоне. +

{{ state.scene.error }} {

Кадр реального окна показа, без второго звука.

- - - - - Показать текст - - - - - Показать таймер -

- {{ state.timer.label }} · {{ timerDisplay }} · {{ timerLabels[state.timer.status] }} -

-

- Таймер продолжает отсчёт в фоне. -

- - Запустить таймер - Пауза таймера - Продолжить таймер - Сбросить таймер - -
-