Compare commits
7
Commits
196aa2e8ba
...
b18e66c28d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b18e66c28d | ||
|
|
f469ab7444 | ||
|
|
8d296c963e | ||
|
|
00ee563311 | ||
|
|
fa8c52b239 | ||
|
|
7bf18636d6 | ||
|
|
ba58ebb48d |
No files matched your search
@@ -60,13 +60,15 @@ same Vite-built app from `dist/` plus the compiled Electron entry points in
|
||||
```bash
|
||||
npm install
|
||||
npm run desktop:dev # start Vite, then launch Electron against localhost
|
||||
npm run desktop:pack # build an unpacked desktop app into release/
|
||||
npm run desktop:dir # build an unpacked desktop app into release/
|
||||
npm run desktop:pack # build platform package artifacts into release/
|
||||
npm run desktop:build # build platform installer/package artifacts into release/
|
||||
```
|
||||
|
||||
`desktop:pack` is the quickest compile check because it produces an unpacked
|
||||
app without creating an installer. `desktop:build` uses `electron-builder` and
|
||||
targets AppImage on Linux, NSIS on Windows, and DMG on macOS.
|
||||
`desktop:dir` is the quickest compile check because it produces an unpacked app
|
||||
without creating an installer. `desktop:pack` and `desktop:build` use
|
||||
`electron-builder` and target AppImage on Linux, NSIS on Windows, and DMG on
|
||||
macOS.
|
||||
|
||||
Desktop MP4 export runs inside Electron and uses ffmpeg from
|
||||
`resources/bin/<platform>/` in packaged builds. The build scripts run
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 41 KiB |
@@ -2,7 +2,7 @@
|
||||
|
||||
Date: 2026-06-05
|
||||
|
||||
Source review: `dev_docs/security-review.md`
|
||||
Source review: `dev_docs/02_security-review.md`
|
||||
|
||||
## Goal
|
||||
|
||||
@@ -19,7 +19,7 @@ Close the security issues from the review without changing RUWIO's core authorin
|
||||
|
||||
Files likely touched:
|
||||
|
||||
- `dev_docs/security-review.md`
|
||||
- `dev_docs/02_security-review.md`
|
||||
- `package.json`
|
||||
- `server/package.json`
|
||||
- `server/server.js`
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Date: 2026-06-05
|
||||
|
||||
This document supersedes `electron-rendering-integration-plan.md` for ongoing
|
||||
This document supersedes `04_electron-rendering-integration-plan.md` for ongoing
|
||||
work. It is self-contained: you do not need the original plan to continue from
|
||||
here. It records what is already implemented, what was found during review, and
|
||||
the concrete remaining TODOs in priority order.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
Date: 2026-06-06
|
||||
|
||||
This document is a follow-on to `electron-rendering-integration-plan_2.md`.
|
||||
This document is a follow-on to `05_electron-rendering-integration-plan_2.md`.
|
||||
That plan tracked findings **#1–#6** (all resolved). A second review of the
|
||||
uncommitted desktop-render changes surfaced a new set of issues, numbered
|
||||
**#7–#13** here to avoid collision. Two of them (#7, #8) are user-facing data
|
||||
@@ -351,6 +351,6 @@ and/or a desktop check asserting a clean result instead of an ffmpeg crash.
|
||||
`*://*/*` network block (plan_2 #5) does not drop them — no action needed.
|
||||
|
||||
## Suggested doc bookkeeping
|
||||
When these land, update `electron-rendering-integration-plan_2.md`:
|
||||
When these land, update `05_electron-rendering-integration-plan_2.md`:
|
||||
- correct the "Partial output file is removed on failure/cancel" note (#7),
|
||||
- and `electron-rendering-manual-checks.md` with the new verify scripts.
|
||||
- and `06_electron-rendering-manual-checks.md` with the new verify scripts.
|
||||
@@ -12,7 +12,7 @@ It builds on what already exists — do not re-invent these:
|
||||
- the Node/Electron **render harnesses** (`scripts/verify-*.cjs`),
|
||||
- the **server** tests (`server/{server.smoke,render.e2e}.test.cjs`),
|
||||
- the **shared validation** unit tests (`shared/renderValidation.test.cjs`),
|
||||
- the manual checklist (`electron-rendering-manual-checks.md`).
|
||||
- the manual checklist (`06_electron-rendering-manual-checks.md`).
|
||||
|
||||
> **TL;DR**
|
||||
> 1. **Don't fork the suite per build.** The frontend is ~99% shared and gated
|
||||
@@ -321,7 +321,7 @@ Phased so each phase is independently valuable and CI-wireable.
|
||||
- A root `test` script aggregating the CI-safe layers, and a first
|
||||
`.github/workflows/test.yml` (matrix: lint/build, node `test:*`,
|
||||
`test:render-*` with ffmpeg/Chromium, Playwright browser + electron jobs).
|
||||
- Update `electron-rendering-manual-checks.md` to point at the new automated checks
|
||||
- Update `06_electron-rendering-manual-checks.md` to point at the new automated checks
|
||||
and list what still needs a HiDPI/macOS box (#10, D10).
|
||||
|
||||
### Proposed `package.json` scripts (final shape)
|
||||
@@ -361,6 +361,6 @@ Phased so each phase is independently valuable and CI-wireable.
|
||||
run under both hosts.
|
||||
- Driving real native OS menus / native save dialogs in automation (brittle); we use
|
||||
the IPC menu-fire hook and the dialog stub instead, and keep one **manual** pass in
|
||||
`electron-rendering-manual-checks.md` for true native behaviour and macOS roles (D10).
|
||||
`06_electron-rendering-manual-checks.md` for true native behaviour and macOS roles (D10).
|
||||
- The offscreen-rendering rewrite (deferred per the fix-plan); if it lands later,
|
||||
the §4 capture matrix is exactly the harness that re-validates it.
|
||||
@@ -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.
|
||||
+16
-1
@@ -10,6 +10,7 @@ const { validateRenderRequest } = require('../shared/renderValidation.cjs') as {
|
||||
}
|
||||
|
||||
const isDev = !app.isPackaged
|
||||
const isE2E = process.env.RUWIO_E2E === '1'
|
||||
const devServerUrl = process.env.VITE_DEV_SERVER_URL ?? 'http://localhost:5200'
|
||||
const devServerOrigin = new URL(devServerUrl).origin
|
||||
const appIconPath = isDev
|
||||
@@ -43,6 +44,12 @@ function sendAppCommand(command: string): void {
|
||||
mainWindow.webContents.send('ruwio:menu-command', command)
|
||||
}
|
||||
|
||||
declare global {
|
||||
// Playwright's Electron runner can evaluate in the main process and call this
|
||||
// env-gated hook without expanding the preload bridge surface.
|
||||
var __ruwioTestMenuCommand: ((command: string) => void) | undefined
|
||||
}
|
||||
|
||||
function sendRenderProgress(
|
||||
target: Electron.WebContents,
|
||||
event: { jobId: string; progress: number; done: boolean; error?: string },
|
||||
@@ -101,7 +108,12 @@ async function runRenderMp4(event: IpcMainInvokeEvent, payload: RenderMp4Payload
|
||||
defaultPath: sanitizeMp4Name(payload?.suggestedName),
|
||||
filters: [{ name: 'MP4 Video', extensions: ['mp4'] }],
|
||||
}
|
||||
const saveResult = window
|
||||
const e2eSavePath = isE2E
|
||||
? process.env.RUWIO_E2E_SAVE_PATH || path.join(app.getPath('temp'), saveOptions.defaultPath)
|
||||
: ''
|
||||
const saveResult = e2eSavePath
|
||||
? { canceled: false, filePath: e2eSavePath }
|
||||
: window
|
||||
? await dialog.showSaveDialog(window, saveOptions)
|
||||
: await dialog.showSaveDialog(saveOptions)
|
||||
if (saveResult.canceled || !saveResult.filePath) return null
|
||||
@@ -379,6 +391,9 @@ app.setName('RUWIO')
|
||||
app.whenReady().then(() => {
|
||||
ipcMain.handle('ruwio:render-mp4', handleRenderMp4)
|
||||
ipcMain.handle('ruwio:render-cancel', handleRenderCancel)
|
||||
if (isE2E) {
|
||||
globalThis.__ruwioTestMenuCommand = (command: string) => sendAppCommand(command)
|
||||
}
|
||||
createMenu()
|
||||
createMainWindow()
|
||||
|
||||
|
||||
+5
-4
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "ruwio",
|
||||
"private": true,
|
||||
"version": "1.3.0",
|
||||
"version": "1.3.2",
|
||||
"type": "module",
|
||||
"main": "dist-electron/main.cjs",
|
||||
"scripts": {
|
||||
@@ -10,7 +10,8 @@
|
||||
"desktop:dev": "concurrently -k \"vite\" \"wait-on http://localhost:5200 && npm run desktop:start\"",
|
||||
"desktop:start": "tsc -p tsconfig.electron.json && electron .",
|
||||
"desktop:build": "npm run build && tsc -p tsconfig.electron.json && npm run stage:ffmpeg -- --optional && electron-builder",
|
||||
"desktop:pack": "npm run build && tsc -p tsconfig.electron.json && npm run stage:ffmpeg -- --optional && electron-builder --dir",
|
||||
"desktop:pack": "npm run build && tsc -p tsconfig.electron.json && npm run stage:ffmpeg -- --optional && electron-builder",
|
||||
"desktop:dir": "npm run build && tsc -p tsconfig.electron.json && npm run stage:ffmpeg -- --optional && electron-builder --dir",
|
||||
"preview": "vite preview",
|
||||
"type-check": "vue-tsc --noEmit",
|
||||
"pack": "bash scripts/pack.sh",
|
||||
@@ -18,7 +19,7 @@
|
||||
"test:render-server": "node --test server/server.smoke.test.cjs",
|
||||
"test:render-server:e2e": "node --test server/render.e2e.test.cjs",
|
||||
"test:render-desktop": "tsc -p tsconfig.electron.json && node scripts/verify-desktop-render.cjs",
|
||||
"test:capture-fidelity": "tsc -p tsconfig.electron.json && electron scripts/verify-capture-fidelity.cjs",
|
||||
"test:capture-fidelity": "tsc -p tsconfig.electron.json && node scripts/run-electron-harness.cjs scripts/verify-capture-fidelity.cjs",
|
||||
"stage:ffmpeg": "node scripts/stage-ffmpeg.cjs"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -45,7 +46,7 @@
|
||||
"appId": "app.ruwio.desktop",
|
||||
"productName": "RUWIO",
|
||||
"icon": "icon.png",
|
||||
"npmRebuild": false,
|
||||
"beforeBuild": "scripts/electron-before-build.cjs",
|
||||
"directories": {
|
||||
"output": "release"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = async function beforeBuild() {
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
const { spawnSync } = require('node:child_process')
|
||||
|
||||
const electron = require('electron')
|
||||
const args = process.argv.slice(2)
|
||||
|
||||
if (!args.length) {
|
||||
console.error('usage: node scripts/run-electron-harness.cjs <script> [args...]')
|
||||
process.exit(2)
|
||||
}
|
||||
|
||||
const env = { ...process.env }
|
||||
delete env.ELECTRON_RUN_AS_NODE
|
||||
env.ELECTRON_DISABLE_SANDBOX = '1'
|
||||
|
||||
const res = spawnSync(electron, ['--no-sandbox', ...args], {
|
||||
stdio: 'inherit',
|
||||
env,
|
||||
argv0: electron,
|
||||
})
|
||||
|
||||
process.exit(res.status ?? 1)
|
||||
@@ -1,4 +1,4 @@
|
||||
// Phase A verification harness (electron-rendering-integration-plan_2.md, finding #1/#2).
|
||||
// Phase A verification harness (05_electron-rendering-integration-plan_2.md, finding #1/#2).
|
||||
//
|
||||
// Reproduces renderWorker.cts's window-creation + capturePage path against a
|
||||
// scene with one moving + fading layer, then checks that captured frames are
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Runs the desktop-render verification harnesses (Phase A/B of
|
||||
// electron-rendering-integration-plan_2.md) under Electron and aggregates the
|
||||
// 05_electron-rendering-integration-plan_2.md) under Electron and aggregates the
|
||||
// results. Each harness exits 0 on PASS, non-zero on FAIL.
|
||||
//
|
||||
// npm run test:render-desktop (compiles electron TS first, then runs this)
|
||||
@@ -11,6 +11,10 @@ const { spawnSync } = require('node:child_process')
|
||||
const path = require('node:path')
|
||||
|
||||
const electron = require('electron') // resolves to the electron binary path
|
||||
const electronEnv = { ...process.env }
|
||||
delete electronEnv.ELECTRON_RUN_AS_NODE
|
||||
electronEnv.ELECTRON_DISABLE_SANDBOX = '1'
|
||||
|
||||
const harnesses = [
|
||||
['capture', 'verify-capture.cjs', 'hidden-window capturePage produces advancing, non-blank frames (#1/#2)'],
|
||||
['capture-fidelity', 'verify-capture-fidelity.cjs', 'capturePage matches render.html canvas pixels'],
|
||||
@@ -26,8 +30,9 @@ const harnesses = [
|
||||
let failures = 0
|
||||
for (const [name, file, desc] of harnesses) {
|
||||
process.stdout.write(`\n=== ${name}: ${desc} ===\n`)
|
||||
const res = spawnSync(electron, [path.join(__dirname, file)], {
|
||||
const res = spawnSync(electron, ['--no-sandbox', path.join(__dirname, file)], {
|
||||
stdio: ['ignore', 'inherit', 'ignore'], // drop Electron's chatty stderr
|
||||
env: electronEnv,
|
||||
})
|
||||
if (res.status !== 0) {
|
||||
failures += 1
|
||||
|
||||
+7
-3
@@ -28,7 +28,6 @@ import ClipartDrawer from './components/ClipartDrawer.vue'
|
||||
import SceneDrawer from './components/SceneDrawer.vue'
|
||||
import DocumentDrawer from './components/DocumentDrawer.vue'
|
||||
import DocumentSettings from './components/DocumentSettings.vue'
|
||||
import ExportDialog from './components/ExportDialog.vue'
|
||||
import FullscreenPreview from './components/FullscreenPreview.vue'
|
||||
import PropertiesPanel from './components/PropertiesPanel.vue'
|
||||
import SvgLayerPanel from './components/SvgLayerPanel.vue'
|
||||
@@ -46,7 +45,6 @@ import { usePlaybackLoop } from './composables/usePlaybackLoop'
|
||||
import { useTimeFormat } from './composables/useTimeFormat'
|
||||
import { useProjectName } from './composables/useProjectName'
|
||||
import { shouldChromeClickDeselect } from './lib/chromeClick'
|
||||
import { renderSceneToStillImage } from './lib/stillImageExport'
|
||||
import { ProjectFileError, pickProjectFile } from './lib/projectFile'
|
||||
import { readAnyProjectFile } from './lib/ruwFile'
|
||||
import {
|
||||
@@ -70,9 +68,11 @@ const welcomeOpen = ref(false)
|
||||
const fullscreenOpen = ref(false)
|
||||
const aboutOpen = ref(false)
|
||||
const helpOpen = ref(false)
|
||||
const isDesktop = computed(() => !!window.ruwioDesktop)
|
||||
const TestOverlay = import.meta.env.DEV
|
||||
? defineAsyncComponent(() => import('./devtools/TestOverlay.vue'))
|
||||
: null
|
||||
const ExportDialog = defineAsyncComponent(() => import('./components/ExportDialog.vue'))
|
||||
const TIMELINE_HEIGHT_KEY = 'animatr:timelineHeight'
|
||||
const DEFAULT_TIMELINE_HEIGHT = 220
|
||||
const MIN_TIMELINE_HEIGHT = 120
|
||||
@@ -313,6 +313,7 @@ async function copyStillToClipboard() {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const { renderSceneToStillImage } = await import('./lib/stillImageExport')
|
||||
const scene = JSON.parse(JSON.stringify(store.scene))
|
||||
const blob = await renderSceneToStillImage(scene, store.playhead, 'png')
|
||||
// ClipboardItem is widely available in modern Chromium / Firefox
|
||||
@@ -745,7 +746,10 @@ onBeforeUnmount(() => {
|
||||
</button>
|
||||
<button
|
||||
class="export-btn"
|
||||
v-tooltip="{ label: t('app.export'), help: t('app.exportHelp') }"
|
||||
v-tooltip="{
|
||||
label: t('app.export'),
|
||||
help: t(isDesktop ? 'app.exportHelpDesktop' : 'app.exportHelpServer'),
|
||||
}"
|
||||
@click="exportOpen = true"
|
||||
>
|
||||
<PhExport :size="14" weight="bold" />
|
||||
|
||||
@@ -12,18 +12,11 @@ import {
|
||||
PhGif,
|
||||
} from '@phosphor-icons/vue'
|
||||
import { useSceneStore } from '@/stores/scene'
|
||||
import { downloadExampleHtml, downloadRuntime, downloadSceneExport } from '@/lib/exportPlayer'
|
||||
import { downloadSceneAsVideo, pickMimeType } from '@/lib/videoExport'
|
||||
import { downloadSceneAsGif } from '@/lib/gifExport'
|
||||
import { downloadSceneAsApng, downloadSceneAsApngPreview } from '@/lib/apngExport'
|
||||
import { hasDesktopRender, renderMp4OnDesktop } from '@/lib/desktopRenderExport'
|
||||
import { downloadFromServer } from '@/lib/serverExport'
|
||||
import { downloadRuwFile } from '@/lib/ruwFile'
|
||||
import { downloadSceneAsStillImage } from '@/lib/stillImageExport'
|
||||
import { useProjectName } from '@/composables/useProjectName'
|
||||
import type { ButtonAsset, Layer } from '@/types/scene'
|
||||
import { Modal } from '@/components/ui'
|
||||
import { t } from '@/i18n'
|
||||
import { hasDesktopRender } from '@/lib/desktopRenderExport'
|
||||
|
||||
const props = defineProps<{ modelValue: boolean }>()
|
||||
const emit = defineEmits<{ 'update:modelValue': [value: boolean] }>()
|
||||
@@ -39,7 +32,22 @@ interface ProgressState {
|
||||
}
|
||||
const progress = ref<ProgressState | null>(null)
|
||||
|
||||
const videoSupported = pickMimeType() !== null
|
||||
const VIDEO_MIME_CANDIDATES = [
|
||||
'video/webm;codecs=vp9',
|
||||
'video/webm;codecs=vp8',
|
||||
'video/webm',
|
||||
'video/mp4;codecs=h264',
|
||||
'video/mp4',
|
||||
]
|
||||
|
||||
function canRecordVideo(): boolean {
|
||||
if (typeof MediaRecorder === 'undefined') return false
|
||||
return VIDEO_MIME_CANDIDATES.some((mime) => MediaRecorder.isTypeSupported?.(mime))
|
||||
}
|
||||
|
||||
const videoSupported = canRecordVideo()
|
||||
const isDesktop = computed(() => !!window.ruwioDesktop)
|
||||
const hasDesktopMp4 = computed(() => hasDesktopRender())
|
||||
|
||||
// Opt-in: strip layers that are hidden in the editor (`editorHidden`)
|
||||
// from the exported scene. Default off — editor-hide is a workflow
|
||||
@@ -198,17 +206,20 @@ function activeSceneClone() {
|
||||
return scene
|
||||
}
|
||||
|
||||
function exportScene() {
|
||||
async function exportScene() {
|
||||
const { downloadSceneExport } = await import('@/lib/exportPlayer')
|
||||
downloadSceneExport(projectClone())
|
||||
emit('update:modelValue', false)
|
||||
}
|
||||
|
||||
function exportRuntime() {
|
||||
async function exportRuntime() {
|
||||
const { downloadRuntime } = await import('@/lib/exportPlayer')
|
||||
downloadRuntime()
|
||||
emit('update:modelValue', false)
|
||||
}
|
||||
|
||||
function exportRuwBundle() {
|
||||
async function exportRuwBundle() {
|
||||
const { downloadRuwFile } = await import('@/lib/ruwFile')
|
||||
// Re-import target: .ruw is the round-trippable format the welcome
|
||||
// dialog + file-picker accept. Pull the project via projectForSave
|
||||
// so the editor-session UI state (zoom, toggles, …) rides along.
|
||||
@@ -219,7 +230,8 @@ function exportRuwBundle() {
|
||||
emit('update:modelValue', false)
|
||||
}
|
||||
|
||||
function exportHtml() {
|
||||
async function exportHtml() {
|
||||
const { downloadExampleHtml } = await import('@/lib/exportPlayer')
|
||||
downloadExampleHtml(projectClone())
|
||||
emit('update:modelValue', false)
|
||||
}
|
||||
@@ -234,6 +246,7 @@ async function exportStillImage(format: 'png' | 'jpg') {
|
||||
const opts = targetSizeChanged.value
|
||||
? { targetWidth: targetWidth.value, targetHeight: targetHeight.value }
|
||||
: undefined
|
||||
const { downloadSceneAsStillImage } = await import('@/lib/stillImageExport')
|
||||
await downloadSceneAsStillImage(
|
||||
activeSceneClone(),
|
||||
store.playhead,
|
||||
@@ -250,11 +263,12 @@ async function exportWebm() {
|
||||
progress.value = {
|
||||
progress: 0,
|
||||
controller,
|
||||
label: t('export.labelWebm'),
|
||||
label: t(isDesktop.value ? 'export.labelWebmDesktop' : 'export.labelWebmBrowser'),
|
||||
error: null,
|
||||
}
|
||||
store.pause()
|
||||
try {
|
||||
const { downloadSceneAsVideo } = await import('@/lib/videoExport')
|
||||
await downloadSceneAsVideo(activeSceneClone(), {
|
||||
signal: controller.signal,
|
||||
onProgress: (p) => {
|
||||
@@ -283,6 +297,7 @@ async function exportGif() {
|
||||
}
|
||||
store.pause()
|
||||
try {
|
||||
const { downloadSceneAsGif } = await import('@/lib/gifExport')
|
||||
await downloadSceneAsGif(
|
||||
activeSceneClone(),
|
||||
frameExportOpts(controller, (p) => {
|
||||
@@ -311,6 +326,7 @@ async function exportApng() {
|
||||
}
|
||||
store.pause()
|
||||
try {
|
||||
const { downloadSceneAsApng } = await import('@/lib/apngExport')
|
||||
await downloadSceneAsApng(
|
||||
activeSceneClone(),
|
||||
frameExportOpts(controller, (p) => {
|
||||
@@ -339,6 +355,7 @@ async function exportApngPreview() {
|
||||
}
|
||||
store.pause()
|
||||
try {
|
||||
const { downloadSceneAsApngPreview } = await import('@/lib/apngExport')
|
||||
await downloadSceneAsApngPreview(
|
||||
activeSceneClone(),
|
||||
frameExportOpts(controller, (p) => {
|
||||
@@ -356,7 +373,7 @@ async function exportApngPreview() {
|
||||
}
|
||||
}
|
||||
|
||||
async function exportMp4Server() {
|
||||
async function exportMp4() {
|
||||
if (progress.value) return
|
||||
const controller = new AbortController()
|
||||
const base = (projectName.value ?? 'animation').toString()
|
||||
@@ -364,13 +381,14 @@ async function exportMp4Server() {
|
||||
progress.value = {
|
||||
progress: 0,
|
||||
controller,
|
||||
label: t('export.labelMp4'),
|
||||
label: t(hasDesktopMp4.value ? 'export.labelMp4Desktop' : 'export.labelMp4Server'),
|
||||
error: null,
|
||||
}
|
||||
store.pause()
|
||||
try {
|
||||
const scene = activeSceneClone()
|
||||
if (hasDesktopRender()) {
|
||||
const { renderMp4OnDesktop } = await import('@/lib/desktopRenderExport')
|
||||
const result = await renderMp4OnDesktop(scene, {
|
||||
signal: controller.signal,
|
||||
fps: targetFps.value,
|
||||
@@ -384,6 +402,7 @@ async function exportMp4Server() {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
const { downloadFromServer } = await import('@/lib/serverExport')
|
||||
await downloadFromServer(scene, {
|
||||
signal: controller.signal,
|
||||
fps: targetFps.value,
|
||||
@@ -577,14 +596,18 @@ function onUpdate(v: boolean) {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button class="opt" @click="exportMp4Server">
|
||||
<button class="opt" @click="exportMp4">
|
||||
<div class="opt-icon"><PhFilmSlate :size="20" /></div>
|
||||
<div class="opt-text">
|
||||
<div class="opt-title">
|
||||
MP4 Video
|
||||
<span class="badge accent">{{ t('export.mp4Badge') }}</span>
|
||||
<span class="badge accent">
|
||||
{{ t(hasDesktopMp4 ? 'export.mp4DesktopBadge' : 'export.mp4ServerBadge') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="opt-desc">
|
||||
{{ t(hasDesktopMp4 ? 'export.mp4DesktopDesc' : 'export.mp4ServerDesc') }}
|
||||
</div>
|
||||
<div class="opt-desc">{{ t('export.mp4Desc') }}</div>
|
||||
</div>
|
||||
<PhDownloadSimple :size="14" class="opt-arrow" />
|
||||
</button>
|
||||
@@ -594,10 +617,16 @@ function onUpdate(v: boolean) {
|
||||
<div class="opt-text">
|
||||
<div class="opt-title">
|
||||
WebM Video
|
||||
<span class="badge">{{ t('export.webmBadge') }}</span>
|
||||
<span class="badge">
|
||||
{{ t(isDesktop ? 'export.webmDesktopBadge' : 'export.webmBrowserBadge') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="opt-desc">
|
||||
{{ videoSupported ? t('export.webmDesc') : t('export.webmNotSupported') }}
|
||||
{{
|
||||
videoSupported
|
||||
? t(isDesktop ? 'export.webmDesktopDesc' : 'export.webmBrowserDesc')
|
||||
: t('export.webmNotSupported')
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
<PhDownloadSimple :size="14" class="opt-arrow" />
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
} from '@phosphor-icons/vue'
|
||||
import { Modal } from '@/components/ui'
|
||||
import { t } from '@/i18n'
|
||||
import { hasDesktopRender } from '@/lib/desktopRenderExport'
|
||||
|
||||
const props = defineProps<{ modelValue: boolean }>()
|
||||
const emit = defineEmits<{ 'update:modelValue': [value: boolean] }>()
|
||||
@@ -63,6 +64,9 @@ interface Chapter {
|
||||
titleKey: string
|
||||
}
|
||||
|
||||
const isDesktop = !!window.ruwioDesktop
|
||||
const hasDesktopMp4 = hasDesktopRender()
|
||||
|
||||
const CHAPTERS: Chapter[] = [
|
||||
{
|
||||
id: 'getting-started',
|
||||
@@ -509,8 +513,16 @@ const CHAPTERS: Chapter[] = [
|
||||
{ bodyKey: 'help.content.export.formats.runtime' },
|
||||
{ bodyKey: 'help.content.export.formats.scene' },
|
||||
{ bodyKey: 'help.content.export.formats.html' },
|
||||
{ bodyKey: 'help.content.export.formats.mp4' },
|
||||
{ bodyKey: 'help.content.export.formats.webm' },
|
||||
{
|
||||
bodyKey: hasDesktopMp4
|
||||
? 'help.content.export.formats.mp4Desktop'
|
||||
: 'help.content.export.formats.mp4',
|
||||
},
|
||||
{
|
||||
bodyKey: isDesktop
|
||||
? 'help.content.export.formats.webmDesktop'
|
||||
: 'help.content.export.formats.webm',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
+44
-20
@@ -39,8 +39,10 @@ export const en = {
|
||||
untitledProject: 'Untitled project',
|
||||
saveProjectDirtyHelp:
|
||||
'Your project has unsaved changes. Ctrl/Cmd+S writes them to browser storage.',
|
||||
exportHelp:
|
||||
'Save as runtime script, HTML, or video. MP4 needs the render server; WebM records in real time in the browser.',
|
||||
exportHelpServer:
|
||||
'Save as runtime script, HTML, or video. MP4 uses the render server; WebM records in real time with MediaRecorder.',
|
||||
exportHelpDesktop:
|
||||
"Save as runtime script, HTML, or video. MP4 renders in the desktop app with bundled ffmpeg; WebM records in Electron's Chromium runtime.",
|
||||
replaceDialog: {
|
||||
title: 'Replace project?',
|
||||
message:
|
||||
@@ -509,10 +511,14 @@ export const en = {
|
||||
sceneDesc: 'Scene data only — requires the runtime to be loaded',
|
||||
exampleTitle: 'Example HTML',
|
||||
exampleDesc: 'Standalone HTML — just open it in a browser',
|
||||
mp4Badge: 'Server · Frame-accurate',
|
||||
mp4Desc: 'H.264, any resolution — requires render server',
|
||||
webmBadge: 'Browser · Realtime',
|
||||
webmDesc: 'No server — recorded in real time',
|
||||
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",
|
||||
webmNotSupported: 'Browser does not support MediaRecorder',
|
||||
gifBadge: 'Browser · Frame-by-frame',
|
||||
gifDesc: '256-colour palette, broadest compatibility',
|
||||
@@ -524,8 +530,10 @@ export const en = {
|
||||
cancel: 'Cancel',
|
||||
failed: 'Export failed',
|
||||
back: 'Back',
|
||||
labelWebm: 'Rendering WebM (browser, realtime)…',
|
||||
labelMp4: 'Rendering MP4 (server)…',
|
||||
labelWebmBrowser: 'Rendering WebM in browser realtime...',
|
||||
labelWebmDesktop: 'Rendering WebM in Chromium realtime...',
|
||||
labelMp4Server: 'Rendering MP4 with render server...',
|
||||
labelMp4Desktop: 'Rendering MP4 in desktop app...',
|
||||
labelGif: 'Rendering GIF (browser, frame-by-frame)…',
|
||||
labelApng: 'Rendering APNG (browser, frame-by-frame)…',
|
||||
labelApngPreview: 'Rendering APNG + preview page…',
|
||||
@@ -1078,9 +1086,13 @@ play
|
||||
html:
|
||||
'<strong>HTML example:</strong> A quick standalone page with everything wired up.',
|
||||
mp4:
|
||||
'<strong>MP4 video:</strong> Frame-accurate server render. This needs the render server configured in Settings.',
|
||||
'<strong>MP4 video:</strong> Frame-by-frame H.264 export via the render server.',
|
||||
mp4Desktop:
|
||||
'<strong>MP4 video:</strong> Frame-by-frame H.264 export in the desktop app with bundled ffmpeg.',
|
||||
webm:
|
||||
'<strong>WebM video:</strong> Browser-side realtime recording. This depends on <code>MediaRecorder</code> support.',
|
||||
'<strong>WebM video:</strong> Browser realtime recording with <code>MediaRecorder</code>.',
|
||||
webmDesktop:
|
||||
"<strong>WebM video:</strong> Realtime recording with <code>MediaRecorder</code> in Electron's Chromium runtime.",
|
||||
},
|
||||
skipHiddenTitle: 'Skip hidden layers',
|
||||
skipHiddenBody:
|
||||
@@ -1149,8 +1161,10 @@ export const de: DeepPartial<Messages> = {
|
||||
untitledProject: 'Unbenanntes Projekt',
|
||||
saveProjectDirtyHelp:
|
||||
'Dein Projekt hat ungespeicherte Änderungen. Mit Ctrl/Cmd+S wird es im Browser gespeichert.',
|
||||
exportHelp:
|
||||
'Als Runtime, HTML oder Video exportieren. MP4 braucht den Render-Server. WebM wird direkt im Browser in Echtzeit aufgenommen.',
|
||||
exportHelpServer:
|
||||
'Als Runtime, HTML oder Video exportieren. MP4 nutzt den Render-Server. WebM wird in Echtzeit mit MediaRecorder aufgenommen.',
|
||||
exportHelpDesktop:
|
||||
'Als Runtime, HTML oder Video exportieren. MP4 rendert in der Desktop-App mit gebündeltem ffmpeg. WebM wird in Electrons Chromium-Runtime aufgenommen.',
|
||||
replaceDialog: {
|
||||
title: 'Projekt ersetzen?',
|
||||
message:
|
||||
@@ -1363,10 +1377,14 @@ export const de: DeepPartial<Messages> = {
|
||||
sceneDesc: 'Nur Projektdaten: Die Runtime muss schon geladen sein.',
|
||||
exampleTitle: 'HTML-Beispiel',
|
||||
exampleDesc: 'Eigenständige HTML-Datei: Einfach im Browser öffnen.',
|
||||
mp4Badge: 'Server · Frame-genau',
|
||||
mp4Desc: 'H.264 in jeder Auflösung: braucht den Render-Server.',
|
||||
webmBadge: 'Browser · Echtzeit',
|
||||
webmDesc: 'Ohne Server: direkt im Browser aufgenommen.',
|
||||
mp4ServerBadge: 'Server · Frame-für-Frame',
|
||||
mp4ServerDesc: 'H.264-Export über den Render-Server',
|
||||
mp4DesktopBadge: 'Desktop · Frame-für-Frame',
|
||||
mp4DesktopDesc: 'H.264-Export über gebündeltes ffmpeg',
|
||||
webmBrowserBadge: 'Browser · Echtzeit',
|
||||
webmBrowserDesc: 'Echtzeit-Aufnahme mit MediaRecorder',
|
||||
webmDesktopBadge: 'Chromium · Echtzeit',
|
||||
webmDesktopDesc: 'Echtzeit-Aufnahme durch Electrons Chromium-Runtime',
|
||||
webmNotSupported: 'Dieser Browser unterstützt MediaRecorder nicht.',
|
||||
gifBadge: 'Browser · Frame-für-Frame',
|
||||
gifDesc: '256-Farben-Palette, maximale Kompatibilität.',
|
||||
@@ -1378,8 +1396,10 @@ export const de: DeepPartial<Messages> = {
|
||||
cancel: 'Abbrechen',
|
||||
failed: 'Export fehlgeschlagen',
|
||||
back: 'Zurück',
|
||||
labelWebm: 'WebM wird gerendert (Browser, Echtzeit)…',
|
||||
labelMp4: 'MP4 wird gerendert (Server)…',
|
||||
labelWebmBrowser: 'WebM wird in Browser-Echtzeit gerendert...',
|
||||
labelWebmDesktop: 'WebM wird in Chromium-Echtzeit gerendert...',
|
||||
labelMp4Server: 'MP4 wird mit dem Render-Server gerendert...',
|
||||
labelMp4Desktop: 'MP4 wird in der Desktop-App gerendert...',
|
||||
labelGif: 'GIF wird gerendert (Browser, frame-für-frame)…',
|
||||
labelApng: 'APNG wird gerendert (Browser, frame-für-frame)…',
|
||||
labelApngPreview: 'APNG + Vorschau werden gerendert…',
|
||||
@@ -1920,9 +1940,13 @@ play
|
||||
html:
|
||||
'<strong>HTML-Beispiel:</strong> Eine schnelle eigenständige Seite mit allem fertig verdrahtet.',
|
||||
mp4:
|
||||
'<strong>MP4-Video:</strong> Frame-genauer Server-Render. Dafür muss der Render-Server in den Einstellungen konfiguriert sein.',
|
||||
'<strong>MP4-Video:</strong> Frame-für-Frame-H.264-Export über den Render-Server.',
|
||||
mp4Desktop:
|
||||
'<strong>MP4-Video:</strong> Frame-für-Frame-H.264-Export in der Desktop-App mit gebündeltem ffmpeg.',
|
||||
webm:
|
||||
'<strong>WebM-Video:</strong> Browser-seitige Echtzeit-Aufnahme. Das hängt von <code>MediaRecorder</code> ab.',
|
||||
'<strong>WebM-Video:</strong> Echtzeit-Aufnahme im Browser mit <code>MediaRecorder</code>.',
|
||||
webmDesktop:
|
||||
'<strong>WebM-Video:</strong> Echtzeit-Aufnahme mit <code>MediaRecorder</code> in Electrons Chromium-Runtime.',
|
||||
},
|
||||
skipHiddenTitle: 'Versteckte Ebenen auslassen',
|
||||
skipHiddenBody:
|
||||
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
export const APP_VERSION = '1.3.0'
|
||||
export const APP_VERSION = '1.3.2'
|
||||
export const APP_NAME = 'RUWIO'
|
||||
@@ -5,6 +5,60 @@ import { fileURLToPath, URL } from 'node:url'
|
||||
export default defineConfig({
|
||||
base: './',
|
||||
plugins: [vue()],
|
||||
build: {
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks(id) {
|
||||
if (id.includes('/src/i18n/')) return 'i18n'
|
||||
if (
|
||||
id.includes('/src/components/CanvasStage.vue') ||
|
||||
id.includes('/src/components/AssetSidebar.vue') ||
|
||||
id.includes('/src/components/PropertiesPanel.vue') ||
|
||||
id.includes('/src/components/ActionEditor.vue') ||
|
||||
id.includes('/src/components/FontPicker.vue') ||
|
||||
id.includes('/src/components/RangeSlider.vue') ||
|
||||
id.includes('/src/components/SvgLayerPanel.vue') ||
|
||||
id.includes('/src/stores/scene.ts') ||
|
||||
id.includes('/src/types/scene.ts') ||
|
||||
id.includes('/src/lib/render.ts') ||
|
||||
id.includes('/src/lib/interpolate.ts') ||
|
||||
id.includes('/src/lib/wiggle.ts') ||
|
||||
id.includes('/src/lib/kaleido.ts') ||
|
||||
id.includes('/src/lib/clipBounds.ts') ||
|
||||
id.includes('/src/lib/actions.ts') ||
|
||||
id.includes('/src/lib/sceneOrder.ts') ||
|
||||
id.includes('/src/composables/useButtonRuntime.ts')
|
||||
) {
|
||||
return 'editor'
|
||||
}
|
||||
if (
|
||||
id.includes('/src/components/Timeline.vue') ||
|
||||
id.includes('/src/lib/timelineSnap.ts') ||
|
||||
id.includes('/src/composables/useAudioWaveform.ts')
|
||||
) {
|
||||
return 'timeline'
|
||||
}
|
||||
if (!id.includes('/node_modules/')) return undefined
|
||||
if (id.includes('/@phosphor-icons/vue/')) return 'icons'
|
||||
if (id.includes('/konva/') || id.includes('/vue-konva/')) return 'canvas'
|
||||
if (
|
||||
id.includes('/gifenc/') ||
|
||||
id.includes('/upng-js/')
|
||||
) {
|
||||
return 'encoders'
|
||||
}
|
||||
if (
|
||||
id.includes('/vue/') ||
|
||||
id.includes('/@vue/') ||
|
||||
id.includes('/pinia/')
|
||||
) {
|
||||
return 'framework'
|
||||
}
|
||||
return 'vendor'
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
server: {
|
||||
port: 5200,
|
||||
|
||||
Reference in new issue
Block a user