Polish Electron desktop shell

This commit is contained in:
th committed 2026-06-05 17:34:50 +02:00
1 parent 70cf131c47
commit 32cc3ecec3
8 files changed
+249 -8

No files matched your search

+17
View File
@@ -51,6 +51,23 @@ npm run build # production bundle to dist/
npm run pack # zip dist/ into dist-archive/ npm run pack # zip dist/ into dist-archive/
``` ```
## Electron desktop build
RUWIO can also run as an Electron desktop app. The desktop shell uses the
same Vite-built app from `dist/` plus the compiled Electron entry points in
`dist-electron/`.
```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: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.
## Stack ## Stack
- Vue 3 (`<script setup>`) + Pinia state - Vue 3 (`<script setup>`) + Pinia state
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

+116 -7
View File
@@ -4,9 +4,17 @@ import path from 'node:path'
const isDev = !app.isPackaged const isDev = !app.isPackaged
const devServerUrl = process.env.VITE_DEV_SERVER_URL ?? 'http://localhost:5200' const devServerUrl = process.env.VITE_DEV_SERVER_URL ?? 'http://localhost:5200'
const appIconPath = isDev
? path.join(process.cwd(), 'build', 'icon.png')
: path.join(process.resourcesPath, 'build', 'icon.png')
let mainWindow: BrowserWindowInstance | null = null let mainWindow: BrowserWindowInstance | null = null
function sendAppCommand(command: string): void {
if (!mainWindow || mainWindow.isDestroyed()) return
mainWindow.webContents.send('ruwio:menu-command', command)
}
function createMainWindow(): void { function createMainWindow(): void {
mainWindow = new BrowserWindow({ mainWindow = new BrowserWindow({
width: 1280, width: 1280,
@@ -14,6 +22,7 @@ function createMainWindow(): void {
minWidth: 1024, minWidth: 1024,
minHeight: 700, minHeight: 700,
title: 'RUWIO', title: 'RUWIO',
icon: appIconPath,
show: false, show: false,
backgroundColor: '#14171f', backgroundColor: '#14171f',
webPreferences: { webPreferences: {
@@ -55,13 +64,38 @@ function createMenu(): void {
{ {
label: 'File', label: 'File',
submenu: [ submenu: [
{ label: 'New Project', accelerator: 'CmdOrCtrl+N', enabled: false }, {
{ label: 'Open...', accelerator: 'CmdOrCtrl+O', enabled: false }, label: 'New Project',
accelerator: 'CmdOrCtrl+N',
click: () => sendAppCommand('new-project'),
},
{
label: 'Open...',
accelerator: 'CmdOrCtrl+O',
click: () => sendAppCommand('open-project'),
},
{
label: 'Recent Projects',
accelerator: 'CmdOrCtrl+Shift+O',
click: () => sendAppCommand('recent-projects'),
},
{ type: 'separator' }, { type: 'separator' },
{ label: 'Save', accelerator: 'CmdOrCtrl+S', enabled: false }, {
{ label: 'Save As...', accelerator: 'CmdOrCtrl+Shift+S', enabled: false }, label: 'Save',
accelerator: 'CmdOrCtrl+S',
click: () => sendAppCommand('save-project'),
},
{
label: 'Save As...',
accelerator: 'CmdOrCtrl+Shift+S',
click: () => sendAppCommand('save-project-as'),
},
{ type: 'separator' }, { type: 'separator' },
{ label: 'Export...', accelerator: 'CmdOrCtrl+E', enabled: false }, {
label: 'Export...',
accelerator: 'CmdOrCtrl+E',
click: () => sendAppCommand('export-project'),
},
{ type: 'separator' }, { type: 'separator' },
process.platform === 'darwin' ? { role: 'close' } : { role: 'quit' }, process.platform === 'darwin' ? { role: 'close' } : { role: 'quit' },
], ],
@@ -69,13 +103,27 @@ function createMenu(): void {
{ {
label: 'Edit', label: 'Edit',
submenu: [ submenu: [
{ role: 'undo' }, {
{ role: 'redo' }, label: 'Undo',
accelerator: 'CmdOrCtrl+Z',
click: () => sendAppCommand('undo'),
},
{
label: 'Redo',
accelerator: 'CmdOrCtrl+Shift+Z',
click: () => sendAppCommand('redo'),
},
{ type: 'separator' }, { type: 'separator' },
{ role: 'cut' }, { role: 'cut' },
{ role: 'copy' }, { role: 'copy' },
{ role: 'paste' }, { role: 'paste' },
{ role: 'selectAll' }, { role: 'selectAll' },
{ type: 'separator' },
{
label: 'Copy Current Frame',
accelerator: 'CmdOrCtrl+Alt+C',
click: () => sendAppCommand('copy-current-frame'),
},
], ],
}, },
{ {
@@ -85,6 +133,43 @@ function createMenu(): void {
{ role: 'forceReload' }, { role: 'forceReload' },
{ role: 'toggleDevTools' }, { role: 'toggleDevTools' },
{ type: 'separator' }, { type: 'separator' },
{
label: 'Play / Pause',
accelerator: 'Space',
click: () => sendAppCommand('toggle-playback'),
},
{
label: 'Stop',
accelerator: 'Escape',
click: () => sendAppCommand('stop-playback'),
},
{
label: 'Jump to Start',
accelerator: 'R',
click: () => sendAppCommand('rewind'),
},
{
label: 'Fullscreen Preview',
accelerator: 'Shift+Space',
click: () => sendAppCommand('fullscreen-preview'),
},
{ type: 'separator' },
{
label: 'Toggle Side Panels',
accelerator: 'Tab',
click: () => sendAppCommand('toggle-panels'),
},
{
label: 'Media Library',
accelerator: 'CmdOrCtrl+M',
click: () => sendAppCommand('media-library'),
},
{
label: 'Document Settings',
accelerator: 'CmdOrCtrl+,',
click: () => sendAppCommand('document-settings'),
},
{ type: 'separator' },
{ role: 'resetZoom' }, { role: 'resetZoom' },
{ role: 'zoomIn' }, { role: 'zoomIn' },
{ role: 'zoomOut' }, { role: 'zoomOut' },
@@ -99,6 +184,30 @@ function createMenu(): void {
{ role: 'zoom' }, { role: 'zoom' },
], ],
}, },
{
label: 'Help',
submenu: [
{
label: 'RUWIO Help',
accelerator: 'F1',
click: () => sendAppCommand('help'),
},
{
label: 'About RUWIO',
click: () => sendAppCommand('about'),
},
...(isDev
? [
{ type: 'separator' as const },
{
label: 'Open Test Runner',
accelerator: 'CmdOrCtrl+Shift+Alt+T',
click: () => sendAppCommand('test-runner'),
},
]
: []),
],
},
] ]
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
+8 -1
View File
@@ -1,4 +1,4 @@
import { contextBridge } from 'electron' import { contextBridge, ipcRenderer } from 'electron'
contextBridge.exposeInMainWorld('ruwioDesktop', { contextBridge.exposeInMainWorld('ruwioDesktop', {
platform: process.platform, platform: process.platform,
@@ -7,4 +7,11 @@ contextBridge.exposeInMainWorld('ruwioDesktop', {
chrome: process.versions.chrome, chrome: process.versions.chrome,
node: process.versions.node, node: process.versions.node,
}, },
onMenuCommand(callback: (command: string) => void) {
const listener = (_event: Electron.IpcRendererEvent, command: string) => callback(command)
ipcRenderer.on('ruwio:menu-command', listener)
return () => {
ipcRenderer.removeListener('ruwio:menu-command', listener)
}
},
}) })
+7
View File
@@ -38,6 +38,7 @@
"build": { "build": {
"appId": "app.ruwio.desktop", "appId": "app.ruwio.desktop",
"productName": "RUWIO", "productName": "RUWIO",
"icon": "icon.png",
"directories": { "directories": {
"output": "release" "output": "release"
}, },
@@ -46,6 +47,12 @@
"dist-electron/**/*", "dist-electron/**/*",
"package.json" "package.json"
], ],
"extraResources": [
{
"from": "build/icon.png",
"to": "build/icon.png"
}
],
"linux": { "linux": {
"target": [ "target": [
"AppImage" "AppImage"
+76
View File
@@ -220,6 +220,12 @@ async function saveProject() {
await markClean() await markClean()
} }
async function saveProjectAs() {
currentProjectId.value = null
currentProjectName.value = null
await saveProject()
}
function openProjectNameDrawer() { function openProjectNameDrawer() {
// Clicking the project name in the topbar toggles the document // Clicking the project name in the topbar toggles the document
// drawer — edits the name inline alongside the rest of the document // drawer — edits the name inline alongside the rest of the document
@@ -546,12 +552,80 @@ function onChromeClick(e: MouseEvent) {
deselectAll() deselectAll()
} }
async function runDesktopMenuCommand(command: string) {
switch (command) {
case 'new-project':
await startNewProject()
break
case 'open-project':
await importFromFile()
break
case 'recent-projects':
welcomeOpen.value = true
break
case 'save-project':
await saveProject()
break
case 'save-project-as':
await saveProjectAs()
break
case 'export-project':
exportOpen.value = true
break
case 'undo':
history.undo()
break
case 'redo':
history.redo()
break
case 'copy-current-frame':
await copyStillToClipboard()
break
case 'toggle-playback':
store.togglePlay()
break
case 'stop-playback':
stop()
break
case 'rewind':
rewind()
break
case 'fullscreen-preview':
store.pause()
fullscreenOpen.value = true
break
case 'toggle-panels':
panelsHidden.value = !panelsHidden.value
break
case 'media-library':
clipart.toggle()
break
case 'document-settings':
settingsOpen.value = true
break
case 'help':
helpOpen.value = true
break
case 'about':
aboutOpen.value = true
break
case 'test-runner':
testsOpen.value = true
break
}
}
let removeDesktopMenuHandler: (() => void) | null = null
onMounted(() => { onMounted(() => {
document.addEventListener('keydown', onKeyDown) document.addEventListener('keydown', onKeyDown)
document.addEventListener('contextmenu', onDocumentContextMenu) document.addEventListener('contextmenu', onDocumentContextMenu)
document.addEventListener('paste', onPaste) document.addEventListener('paste', onPaste)
document.addEventListener('click', onChromeClick) document.addEventListener('click', onChromeClick)
window.addEventListener('resize', onWindowResize) window.addEventListener('resize', onWindowResize)
removeDesktopMenuHandler = window.ruwioDesktop?.onMenuCommand?.((command) => {
void runDesktopMenuCommand(command)
}) ?? null
void clipart.initialize() void clipart.initialize()
bootWelcome() bootWelcome()
}) })
@@ -561,6 +635,8 @@ onBeforeUnmount(() => {
document.removeEventListener('paste', onPaste) document.removeEventListener('paste', onPaste)
document.removeEventListener('click', onChromeClick) document.removeEventListener('click', onChromeClick)
window.removeEventListener('resize', onWindowResize) window.removeEventListener('resize', onWindowResize)
removeDesktopMenuHandler?.()
removeDesktopMenuHandler = null
}) })
</script> </script>
+24
View File
@@ -1,5 +1,26 @@
export {} export {}
type RuwioDesktopMenuCommand =
| 'new-project'
| 'open-project'
| 'recent-projects'
| 'save-project'
| 'save-project-as'
| 'export-project'
| 'undo'
| 'redo'
| 'copy-current-frame'
| 'toggle-playback'
| 'stop-playback'
| 'rewind'
| 'fullscreen-preview'
| 'toggle-panels'
| 'media-library'
| 'document-settings'
| 'help'
| 'about'
| 'test-runner'
declare global { declare global {
interface Window { interface Window {
ruwioDesktop?: { ruwioDesktop?: {
@@ -9,6 +30,9 @@ declare global {
chrome?: string chrome?: string
node?: string node?: string
} }
onMenuCommand?: (
callback: (command: RuwioDesktopMenuCommand) => void,
) => () => void
} }
} }
} }
+1
View File
@@ -3,6 +3,7 @@ import vue from '@vitejs/plugin-vue'
import { fileURLToPath, URL } from 'node:url' import { fileURLToPath, URL } from 'node:url'
export default defineConfig({ export default defineConfig({
base: './',
plugins: [vue()], plugins: [vue()],
server: { server: {