9.3 KiB
Electron Desktop Plan
Goal
Package RUWIO as a desktop app while keeping the existing Vue/Vite renderer as the primary application. The Electron layer should start small: host the built app, expose native filesystem dialogs, and later absorb the current server-side MP4 render workflow so desktop users do not need a separate render server.
Current App Shape
- The UI is a Vue 3 + Vite single-page app in
src/. - The production web build is produced by
npm run buildintodist/. - Project save/load is currently browser-driven:
- JSON project export/import lives in
src/lib/projectFile.ts. .ruwbundle export/import lives insrc/lib/ruwFile.ts.- User-triggered file downloads use
Blob, object URLs, and anchor clicks.
- JSON project export/import lives in
- Still/GIF/APNG style exports are implemented client-side.
- MP4 rendering is currently split out into
server/server.js, using Express, Puppeteer Core, Chromium, and ffmpeg. - The app already has PWA assets and static-host assumptions, which are compatible with an Electron renderer if paths are handled carefully.
Recommended Architecture
Use a standard Electron split:
electron/main.ts- Creates the main
BrowserWindow. - Loads the Vite dev server during development.
- Loads
dist/index.htmlin packaged builds. - Owns native dialogs, app menu, recent files, and privileged filesystem/process work.
- Creates the main
electron/preload.ts- Exposes a small typed API on
window.ruwioDesktop. - Uses
contextBridgeand IPC. - Does not expose Node primitives directly.
- Exposes a small typed API on
- Vue renderer
- Remains browser-compatible.
- Detects the desktop API when available.
- Falls back to current browser file picker/download behavior on the web.
Keep Electron as an optional target rather than replacing the browser build. The same app should continue to run with npm run dev, npm run build, and static hosting.
Package Additions
Add these development dependencies:
electronelectron-builderorelectron-forgeconcurrentlyfor local dev conveniencewait-onif the Electron dev command waits for Vite
Recommended scripts:
{
"dev": "vite",
"desktop:dev": "concurrently \"vite\" \"wait-on http://localhost:5200 && electron .\"",
"desktop:build": "npm run build && electron-builder",
"desktop:pack": "npm run build && electron-builder --dir"
}
The exact script shape can be adjusted once the Electron entry files exist.
Phase 1: Minimal Desktop Shell
- Add Electron source files under
electron/. - Configure TypeScript output for the Electron main/preload files.
- Create a secure
BrowserWindow:contextIsolation: truenodeIntegration: falsesandbox: trueif preload needs stay small enough to support it
- Load
http://localhost:5200in development. - Load the built
dist/index.htmlin production. - Set app name, icon, minimum window size, and basic lifecycle handling for macOS/Windows/Linux.
- Add a basic app menu with New/Open/Save/Save As/Export/Quit placeholders.
Acceptance check:
npm run desktop:devopens the existing app.npm run desktop:packcreates an unpacked app.- The normal browser dev/build flows still work.
Phase 2: Desktop File API
Add IPC-backed file operations instead of immediately rewriting the app's save/load model.
Expose a preload API like:
window.ruwioDesktop = {
openProject(): Promise<{ name: string; bytes: Uint8Array } | null>,
saveProject(options: { suggestedName: string; bytes: Uint8Array }): Promise<{ path: string } | null>,
saveJson(options: { suggestedName: string; text: string }): Promise<{ path: string } | null>,
exportBlob(options: { suggestedName: string; bytes: Uint8Array }): Promise<{ path: string } | null>
}
Renderer changes:
- Add a small desktop adapter module, for example
src/lib/desktopBridge.ts. - In
downloadProjectanddownloadRuwFile, use the desktop bridge when present and fall back to browser downloads otherwise. - For loading, keep the current
<input type="file">path as the web fallback and add desktop open dialog support for Electron. - Preserve existing
.ruwbundle logic. The desktop API should move bytes to and from disk, not duplicate parsing/packing rules.
Acceptance check:
- Open
.ruwand legacy.jsonprojects from a native dialog. - Save
.ruwprojects through a native save dialog. - Browser import/export behavior remains unchanged.
Phase 3: Native Menus And App State
Wire menu actions to renderer commands through IPC.
Planned menu commands:
- New Project
- Open
- Save
- Save As
- Export HTML/JS bundle
- Export PNG/GIF/APNG/MP4 as available
- Undo/Redo if the existing history store can expose commands
- Toggle fullscreen/preview/test overlay where appropriate
Implementation notes:
- The main process should not mutate project state directly.
- The renderer should own project state and respond to menu events.
- Track the current file path in the desktop bridge or a small Pinia/composable integration.
- Add a dirty-state signal so the main process can confirm before closing a window with unsaved work.
Acceptance check:
- Menu shortcuts trigger the same code paths as toolbar buttons.
- Closing the app warns when there are unsaved changes.
- Recent files can be added after the open/save path is stable.
Phase 4: Desktop MP4 Rendering
Replace the external render server requirement for desktop builds.
Preferred approach:
- Move the core render job orchestration out of
server/server.jsinto a reusable Node module. - Keep the existing Express server as a thin wrapper around that module for web/server deployments.
- Call the same render module from Electron main via IPC.
- Stream progress back to the renderer.
- Save the resulting MP4 through a native save dialog.
Rendering dependencies:
- Bundle or locate Chromium. Electron already ships Chromium, but Puppeteer against Electron's browser runtime needs careful validation. The safer first version is to keep Puppeteer or Playwright with an explicit executable path.
- Bundle or locate ffmpeg. For a consumer desktop app, prefer an embedded ffmpeg binary per platform or document a clear install requirement until bundling is solved.
- Keep temporary frames/pipes in the OS temp directory, as the current server does.
Acceptance check:
- MP4 export works offline in the packaged desktop app.
- Progress, cancel, and failure states match the current server export UX.
- The existing
server/deployment still runs.
Phase 5: Packaging And Release
Configure packaging metadata:
- App id, product name, copyright.
- Icons for macOS, Windows, and Linux.
- File associations for
.ruw. - Build targets:
- macOS:
.dmgor.zip - Windows:
nsis - Linux:
AppImagefirst, optionally.deb
- macOS:
- Include
dist/, Electron compiled files, static assets, and any render binaries. - Exclude source maps and development-only files unless needed for debugging.
Later release tasks:
- Code signing and notarization for macOS.
- Windows signing.
- Auto-update channel if RUWIO will ship outside app stores.
- CI builds for all target platforms.
Acceptance check:
- A clean checkout can produce a packaged desktop artifact.
- The artifact launches without a local dev server.
- File associations open
.ruwprojects when double-clicked.
Security Rules
- Keep Node access out of the Vue renderer.
- Use
contextBridgewith a narrow allowlist. - Validate IPC payloads in the main process.
- Do not pass arbitrary file paths from renderer to main without checks.
- Restrict navigation to the app origin in development and local files in production.
- Disable or guard
window.open. - Keep the render process isolated from arbitrary project content where possible, especially if imported SVG/HTML assets can contain script-like payloads.
Risk Areas
- Asset paths under
file://may behave differently than Vite dev paths. Test fonts, icons, manifest assets, imported images, audio, and exported runtime files in packaged builds. - Browser download code is synchronous from the user's perspective, while native save dialogs are async and cancellable. UI states should handle cancellation cleanly.
- MP4 export is the largest unknown because Chromium and ffmpeg packaging varies by platform.
- Large
.ruwfiles may put pressure on IPC if sent as huge arrays. If this becomes a problem, switch to main-process file handles or temporary files. - Existing in-browser tests need an Electron smoke layer because packaged-app failures may not appear in Vite.
Suggested Implementation Order
- Add Electron shell and dev/build scripts.
- Package the current app without native integrations.
- Add preload typings and a tiny
desktopBridge. - Move project open/save to native dialogs.
- Add native export save paths for existing browser-generated blobs.
- Add app menu and unsaved-change handling.
- Refactor
server/server.jsrender logic into a reusable Node module. - Add desktop MP4 export through Electron main.
- Configure packaging icons, file associations, and platform targets.
- Add CI/package smoke checks.
First Pull Request Scope
Keep the first PR intentionally small:
- Electron main/preload files.
- Desktop dev and pack scripts.
- Production loading of
dist/index.html. - Basic secure window configuration.
- No changes to project save/load or rendering behavior yet.
That gives RUWIO a launchable desktop shell before any app behavior changes, making the rest of the migration easier to test in small increments.