From 8b29b4f24ecc4f1195ae6e3c18e6ee662ff0b009 Mon Sep 17 00:00:00 2001 From: Fedorov Vladimir Date: Sat, 17 Oct 2026 18:54:57 +0700 Subject: [PATCH] up --- README.md | 2 ++ TASKS.md | 9 ++++++ docs/SCREEN-001_ACCEPTANCE.md | 12 ++++++-- electron/main.ts | 3 ++ scripts/smoke.cjs | 58 ++++++++++++++++++++++++++++++----- 5 files changed, 74 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 8c22905..4628fb8 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,8 @@ Commit и push выполняет только пользователь. Вло ## Запуск стенда SCREEN-001 Нужны Node.js 24.15+ и ffmpeg с libx264 в PATH. +В Linux приложение использует X11; для сессии Wayland нужен XWayland, чтобы +размещать показ на выбранном мониторе. ```sh npm ci diff --git a/TASKS.md b/TASKS.md index 7a48005..835f77f 100644 --- a/TASKS.md +++ b/TASKS.md @@ -39,6 +39,15 @@ SCREEN-009 (LAN-пульт) отменена владельцем; ID не пе PASS. В smoke проверено отсутствие LAN-панели и методов lan/approve в preload, локальные play/pause/stop/seek, Range и открытие окна без автоповтора. Frontend-стандарт прочитан; ручное ревью diff выполнено, нарушений не найдено. +- Исправление выбора дисплея: для Linux явно включён X11/XWayland, поскольку native + Wayland игнорирует глобальные координаты окон. Перед fullscreen повторно заданы + bounds выбранного дисплея. GUI smoke проверяет размещение и повторное открытие; + при наличии реального второго дисплея проверяет фактический монитор окна. + Type-check, lint, lint:css, тесты и GUI smoke — PASS. В текущей среде физически + подключён только eDP-1; второй дисплей в этом прогоне синтетический, аппаратная + проверка переноса остаётся открытой. Нужен полный перезапуск приложения. + Четыре Linux/Windows-архива пересобраны с исправлением; содержимое app.asar + сверено с текущим кодом, SHA256SUMS обновлён. Нарушений стандарта в diff не найдено. - Сборки: Linux/Windows x64/ARM64 пересобраны без LAN; содержимое всех четырёх app.asar сверено с проверенным кодом. LAN-модули, ws и тесты отсутствуют в поставке; release/SHA256SUMS обновлён. macOS нужно собирать на Mac или ручным workflow. diff --git a/docs/SCREEN-001_ACCEPTANCE.md b/docs/SCREEN-001_ACCEPTANCE.md index 33be0c1..bef7d3c 100644 --- a/docs/SCREEN-001_ACCEPTANCE.md +++ b/docs/SCREEN-001_ACCEPTANCE.md @@ -19,6 +19,11 @@ Electron загружается отдельной командой install-elec | Linux x64 | npm run dist -- --linux --x64 | release/*.tar.gz | | Linux ARM64 | npm run dist -- --linux --arm64 | release/*.tar.gz | +В Linux приложение явно использует X11, в сессии Wayland — через XWayland. +Нативный Wayland не позволяет надёжно задавать глобальные координаты окна: +[ограничение Electron](https://www.electronjs.org/docs/latest/api/browser-window#platform-notices). +Перед fullscreen окно явно размещается в границах выбранного дисплея. + Сборки macOS выполняйте на Mac. На Windows ARM используйте ARM-версию Node.js. Linux tar.gz распаковать с сохранением прав файлов; запуск — исполняемый файл в распакованном каталоге. Архивы Windows распаковать целиком, macOS .app перенести в Applications. @@ -52,8 +57,11 @@ build, 4 теста и GUI smoke — PASS, включая запуск кноп ## Автоматический smoke После генерации медиа выполните npm run smoke в графической сессии. Проверка -использует синтетический второй дисплей: IPC, Range, запуск/пауза/стоп/seek, -запрет IPC из окна показа и отсутствие автоповтора при открытии окна. Дополнительно +использует реальный второй дисплей при наличии, иначе синтетический: IPC, Range, +запуск/пауза/стоп/seek, запрет IPC из окна показа и отсутствие автоповтора. +Проверяются координаты выбранного дисплея перед fullscreen и при повторном открытии; +в Linux — режим X11. При реальном втором дисплее дополнительно проверяется, +на каком мониторе фактически оказалось полноэкранное окно. Дополнительно проверяется отсутствие LAN-методов в preload и сетевых параметров в данных пульта. Это не проверка реального проектора. Серверное управление проверяется в SCREEN-006. diff --git a/electron/main.ts b/electron/main.ts index 08ebe56..86e66bf 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -7,6 +7,8 @@ import { Controller } from './controller.js' import { parseRange } from './range.js' import type { HostInfo } from '../src/types/screen.js' +// Native Wayland запрещает размещение окна на выбранном дисплее по координатам. +if (process.platform === 'linux') app.commandLine.appendSwitch('ozone-platform', 'x11') app.commandLine.appendSwitch('autoplay-policy', 'no-user-gesture-required') protocol.registerSchemesAsPrivileged([ { @@ -79,6 +81,7 @@ function show(displayId: number) { output.once('ready-to-show', () => { if (!window.isDestroyed()) { window.show() + window.setBounds(display.bounds) window.setFullScreen(true) } }) diff --git a/scripts/smoke.cjs b/scripts/smoke.cjs index d6d9912..717c374 100644 --- a/scripts/smoke.cjs +++ b/scripts/smoke.cjs @@ -9,18 +9,39 @@ app.setPath( ), ) const delay = (ms) => new Promise((r) => setTimeout(r, ms)) +let targetDisplay +let isSyntheticDisplay = false +const boundsBeforeFullscreen = new Map() +const fullscreenEntered = new Set() +app.on('browser-window-created', (_event, window) => { + window.on('enter-full-screen', () => fullscreenEntered.add(window.id)) + let requestedBounds + const setBounds = window.setBounds.bind(window) + window.setBounds = (bounds, ...args) => { + requestedBounds = { ...bounds } + return setBounds(bounds, ...args) + } + const setFullScreen = window.setFullScreen.bind(window) + window.setFullScreen = (flag) => { + if (flag) boundsBeforeFullscreen.set(window.id, requestedBounds) + return setFullScreen(flag) + } +}) app.whenReady().then(() => { const original = screen.getAllDisplays.bind(screen) - screen.getAllDisplays = () => [ - ...original(), - { - ...original()[0], + const displays = original() + targetDisplay = displays.find((display) => display.id !== screen.getPrimaryDisplay().id) + if (!targetDisplay) { + isSyntheticDisplay = true + targetDisplay = { + ...displays[0], id: 424242, label: 'Synthetic test display', bounds: { x: 1280, y: 0, width: 1280, height: 720 }, size: { width: 1280, height: 720 }, - }, - ] + } + screen.getAllDisplays = () => [...original(), targetDisplay] + } }) require(root + '/dist-electron/electron/main.js') async function poll(check, label) { @@ -47,7 +68,7 @@ app.whenReady().then(async () => { ) const evaluate = (s) => control.webContents.executeJavaScript(s) assert.equal(await evaluate('typeof window.require'), 'undefined') - await evaluate('window.screenBridge.show(424242)') + await evaluate(`window.screenBridge.show(${targetDisplay.id})`) const output = await poll( () => BrowserWindow.getAllWindows().find((w) => w !== control), 'output', @@ -63,6 +84,15 @@ app.whenReady().then(async () => { )), 'metadata', ) + if (process.platform === 'linux') + assert.equal(app.commandLine.getSwitchValue('ozone-platform'), 'x11') + await poll(() => boundsBeforeFullscreen.has(output.id), 'output fullscreen requested') + assert.deepEqual(boundsBeforeFullscreen.get(output.id), targetDisplay.bounds) + if (!isSyntheticDisplay) { + await poll(() => fullscreenEntered.has(output.id), 'output entered fullscreen') + assert.ok(output.isFullScreen()) + assert.equal(screen.getDisplayMatching(output.getBounds()).id, targetDisplay.id) + } const range = await output.webContents.executeJavaScript( `fetch('screen-app://app/media/video', {headers: {Range: 'bytes=100-199'}}).then(async r => ({status:r.status, size:(await r.arrayBuffer()).byteLength, range:r.headers.get('content-range')}))`, ) @@ -117,7 +147,7 @@ app.whenReady().then(async () => { assert.equal(await evaluate('typeof window.screenBridge.lan'), 'undefined') assert.equal(await evaluate('typeof window.screenBridge.approve'), 'undefined') output.close() - await evaluate('window.screenBridge.show(424242)') + await evaluate(`window.screenBridge.show(${targetDisplay.id})`) const reopened = await poll( () => BrowserWindow.getAllWindows().find((w) => w !== control), 'reopened output', @@ -128,6 +158,13 @@ app.whenReady().then(async () => { (await reopened.webContents.executeJavaScript('!!document.querySelector("video")')), 'reopened ready', ) + await poll(() => boundsBeforeFullscreen.has(reopened.id), 'reopened fullscreen requested') + assert.deepEqual(boundsBeforeFullscreen.get(reopened.id), targetDisplay.bounds) + if (!isSyntheticDisplay) { + await poll(() => fullscreenEntered.has(reopened.id), 'reopened entered fullscreen') + assert.ok(reopened.isFullScreen()) + assert.equal(screen.getDisplayMatching(reopened.getBounds()).id, targetDisplay.id) + } assert.equal( await reopened.webContents.executeJavaScript('document.querySelector("video").paused'), true, @@ -136,6 +173,11 @@ app.whenReady().then(async () => { 'SCREEN_SMOKE_PASS', JSON.stringify({ range, + display: { + id: targetDisplay.id, + bounds: targetDisplay.bounds, + synthetic: isSyntheticDisplay, + }, seek: position, samples: (await evaluate('window.screenBridge.state()')).samples, electron: process.versions.electron,