diff --git a/animatr_reference/Screenshot from 2026-06-07 10-34-26.png b/animatr_reference/Screenshot from 2026-06-07 10-34-26.png new file mode 100644 index 0000000..d6b169b Binary files /dev/null and b/animatr_reference/Screenshot from 2026-06-07 10-34-26.png differ diff --git a/dev_docs/09_export-environment-copy-plan.md b/dev_docs/09_export-environment-copy-plan.md new file mode 100644 index 0000000..c18c789 --- /dev/null +++ b/dev_docs/09_export-environment-copy-plan.md @@ -0,0 +1,293 @@ +# 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. diff --git a/package.json b/package.json index 3f29cfa..f1d4de8 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ruwio", "private": true, - "version": "1.3.1", + "version": "1.3.2", "type": "module", "main": "dist-electron/main.cjs", "scripts": { diff --git a/src/version.ts b/src/version.ts index 8113aad..4754c66 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1,2 +1,2 @@ -export const APP_VERSION = '1.3.1' +export const APP_VERSION = '1.3.2' export const APP_NAME = 'RUWIO'