# Export Environment Copy Plan Date: 2026-06-07 ## Goal Make the export dialog copy describe what actually happens in each runtime: - **Browser/PWA:** MP4 uses the render server; WebM records through browser `MediaRecorder`. - **Electron desktop:** MP4 uses the desktop render bridge and bundled ffmpeg; WebM still records through the Chromium `MediaRecorder` runtime inside Electron. The current labels are accurate for the browser build, but misleading inside Electron: - `SERVER - FRAME-ACCURATE` on MP4, even when Electron is rendering locally. - `BROWSER - REALTIME` on WebM, even when the app is running as Electron. ## Current Code Primary files: - `src/components/ExportDialog.vue` - `src/i18n/messages.ts` - `src/lib/desktopRenderExport.ts` - `src/types/desktop.d.ts` Runtime detection already exists as an exported helper in `desktopRenderExport.ts`: ```ts export function hasDesktopRender(): boolean { return !!window.ruwioDesktop?.renderMp4 } ``` This is the right signal for MP4 copy because `ExportDialog.vue` routes MP4 through `renderMp4OnDesktop()` when `hasDesktopRender()` returns true, otherwise it calls `downloadFromServer()`. The MP4 copy should reuse this exact function rather than reimplementing the predicate, so the text can never drift from the route (see Implementation Plan step 1). `window.ruwioDesktop` is injected by the Electron preload before any app module loads and never changes during a session, so a runtime check is stable at module-init time — no reactivity is required to read it correctly. ## Proposed Copy Matrix ### MP4 Browser/PWA: - Badge: `Server · Frame-by-frame` - Description: `H.264 export via render server` - Progress: `Rendering MP4 with render server...` Electron: - Badge: `Desktop · Frame-by-frame` - Description: `H.264 export via bundled ffmpeg` - Progress: `Rendering MP4 in desktop app...` Rationale: both MP4 paths render deterministic frames through `server/render.html`. The key difference is where rendering happens: HTTP render server vs Electron desktop worker plus ffmpeg. Avoid `Frame-accurate` in the short badge. It sounds like a stronger quality claim than the dialog needs, and it obscures the real user-facing distinction: server vs desktop. The help text can explain frame-by-frame rendering if needed. Badge style: store badges in title case with a `·` middot separator, matching the existing sibling badges (`gifBadge: 'Browser · Frame-by-frame'`, `apngBadge: 'Browser · Lossless'`). The `.badge` CSS already applies `text-transform: uppercase`, so do **not** write the strings in all-caps or switch to a `-` hyphen separator — that would render MP4/WebM inconsistently next to GIF/APNG in the same column. ### WebM Browser/PWA: - Badge: `Browser · Realtime` - Description: `Recorded in real time with MediaRecorder` - Progress: `Rendering WebM in browser realtime...` Electron: - Badge: `Chromium · Realtime` - Description: `Recorded in real time by Electron's Chromium runtime` - Progress: `Rendering WebM in Chromium realtime...` Rationale: the implementation does not become a desktop ffmpeg pipeline in Electron. It still uses `MediaRecorder` from the renderer process. Calling that `browser` in the desktop app is imprecise, but `Chromium` is technically true and short enough for the badge. ## Implementation Plan 1. Add environment booleans in `ExportDialog.vue`. ```ts import { hasDesktopRender } from '@/lib/desktopRenderExport' const isDesktop = computed(() => !!window.ruwioDesktop) const hasDesktopMp4 = computed(() => hasDesktopRender()) ``` Use `hasDesktopMp4` for MP4 text. It wraps `hasDesktopRender()` — the *same* function the MP4 route already calls in `exportMp4Server()` — so the copy decision and the actual route share one predicate and can never diverge. Do not reimplement `!!window.ruwioDesktop?.renderMp4` inline. Use `isDesktop` for WebM text because WebM is still available through renderer Chromium even if MP4 desktop rendering is unavailable; there is no existing helper for that distinction, so a local boolean is fine. 2. Rename `exportMp4Server()` to `exportMp4()`. The current name is now misleading because the function already branches: - Desktop: `renderMp4OnDesktop(scene, ...)` - Browser/server: `downloadFromServer(scene, ...)` 3. Replace static MP4 bindings. Current: ```vue {{ t('export.mp4Badge') }} {{ t('export.mp4Desc') }} ``` Proposed: ```vue {{ t(hasDesktopMp4 ? 'export.mp4DesktopBadge' : 'export.mp4ServerBadge') }} {{ t(hasDesktopMp4 ? 'export.mp4DesktopDesc' : 'export.mp4ServerDesc') }} ``` 4. Replace static WebM bindings. Current: ```vue {{ t('export.webmBadge') }} {{ videoSupported ? t('export.webmDesc') : t('export.webmNotSupported') }} ``` Proposed: ```vue {{ t(isDesktop ? 'export.webmDesktopBadge' : 'export.webmBrowserBadge') }} {{ videoSupported ? t(isDesktop ? 'export.webmDesktopDesc' : 'export.webmBrowserDesc') : t('export.webmNotSupported') }} ``` 5. Split progress labels. MP4 currently uses: ```ts label: t('export.labelMp4') ``` Proposed: ```ts label: t(hasDesktopMp4.value ? 'export.labelMp4Desktop' : 'export.labelMp4Server') ``` WebM currently uses: ```ts label: t('export.labelWebm') ``` Proposed: ```ts label: t(isDesktop.value ? 'export.labelWebmDesktop' : 'export.labelWebmBrowser') ``` 6. Update `src/i18n/messages.ts` in both locales. English keys to add: ```ts mp4ServerBadge: 'Server · Frame-by-frame', mp4ServerDesc: 'H.264 export via render server', mp4DesktopBadge: 'Desktop · Frame-by-frame', mp4DesktopDesc: 'H.264 export via bundled ffmpeg', webmBrowserBadge: 'Browser · Realtime', webmBrowserDesc: 'Recorded in real time with MediaRecorder', webmDesktopBadge: 'Chromium · Realtime', webmDesktopDesc: "Recorded in real time by Electron's Chromium runtime", labelMp4Server: 'Rendering MP4 with render server...', labelMp4Desktop: 'Rendering MP4 in desktop app...', labelWebmBrowser: 'Rendering WebM in browser realtime...', labelWebmDesktop: 'Rendering WebM in Chromium realtime...', ``` Badges stay title case with a `·` separator; the `.badge` CSS uppercases them at render time (see the MP4 copy-matrix note above). German keys should mirror the meaning rather than preserve the old wording. 7. Fix the export-button help string `exportHelp` in `messages.ts`. This is separate from the help dialog and is easy to miss. Both locales currently hardcode the misleading claim: - `messages.ts:43` (en): "...MP4 needs the render server; WebM records in real time in the browser." - `messages.ts:1153` (de): "...MP4 braucht den Render-Server. WebM wird direkt im Browser in Echtzeit aufgenommen." Add runtime-branched variants (e.g. `exportHelpServer` / `exportHelpDesktop`) and select with the same `isDesktop` signal wherever `t('...exportHelp')` is consumed. This is exactly the "globally visible text saying MP4 always needs the render server" that must not be left in place. 8. Update help-dialog format text in `messages.ts`, and branch it now. The strings live at `help.content.export.formats.mp4` / `.webm` (`messages.ts:1081/1083` en, `:1923/1925` de) and `exportHelp` (handled in step 7). Browser help should describe: - MP4 as render-server frame-by-frame export. - WebM as browser `MediaRecorder` realtime recording. Electron help should describe: - MP4 as bundled-ffmpeg desktop frame-by-frame export. - WebM as Chromium `MediaRecorder` realtime recording. Wiring the branch is cheaper than the old draft assumed. `HelpDialog.vue` defines `CHAPTERS: Chapter[]` as a module-level `const` with static `bodyKey` strings, but `window.ruwioDesktop` is set at preload before that module loads and never changes, so a plain ternary on the `bodyKey` resolves correctly at module-init time without making `CHAPTERS` reactive: ```ts { bodyKey: window.ruwioDesktop?.renderMp4 ? 'help.content.export.formats.mp4Desktop' : 'help.content.export.formats.mp4' }, { bodyKey: window.ruwioDesktop ? 'help.content.export.formats.webmDesktop' : 'help.content.export.formats.webm' }, ``` Add the `*Desktop` keys in both locales and wire the branch in the same change. Do not defer this and leave the help dialog asserting MP4 always needs the render server. ## Verification Browser/Vite: - Open the export dialog. - MP4 badge renders `SERVER · FRAME-BY-FRAME` (stored title case, uppercased by CSS). - MP4 description mentions the render server. - WebM badge renders `BROWSER · REALTIME`. - WebM description mentions `MediaRecorder`. - Export-button help (`exportHelp`) and help dialog describe MP4 as a render-server export. Electron: - Open the export dialog. - MP4 badge renders `DESKTOP · FRAME-BY-FRAME`. - MP4 description mentions bundled ffmpeg. - WebM badge renders `CHROMIUM · REALTIME`. - WebM description mentions Electron Chromium. - Export-button help (`exportHelp`) and help dialog describe MP4 as a bundled-ffmpeg desktop export, not the render server. Build checks: ```bash npm run build npm run desktop:dir ``` Optional automated coverage: - Add a small in-app devtools test that stubs/removes `window.ruwioDesktop` and verifies the selected i18n keys or rendered badge text. `src/devtools/headless.ts` already reads `!!window.ruwioDesktop` (line 19) and is the natural home for it. - Add an Electron harness assertion that export dialog MP4 copy is desktop copy.