Polish Electron desktop shell
This commit is contained in:
1 parent
70cf131c47
commit
32cc3ecec3
8 files changed
+249
-8
No files matched your search
@@ -51,6 +51,23 @@ npm run build # production bundle to dist/
|
||||
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
|
||||
|
||||
- Vue 3 (`<script setup>`) + Pinia state
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 191 KiB |
+116
-7
@@ -4,9 +4,17 @@ import path from 'node:path'
|
||||
|
||||
const isDev = !app.isPackaged
|
||||
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
|
||||
|
||||
function sendAppCommand(command: string): void {
|
||||
if (!mainWindow || mainWindow.isDestroyed()) return
|
||||
mainWindow.webContents.send('ruwio:menu-command', command)
|
||||
}
|
||||
|
||||
function createMainWindow(): void {
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1280,
|
||||
@@ -14,6 +22,7 @@ function createMainWindow(): void {
|
||||
minWidth: 1024,
|
||||
minHeight: 700,
|
||||
title: 'RUWIO',
|
||||
icon: appIconPath,
|
||||
show: false,
|
||||
backgroundColor: '#14171f',
|
||||
webPreferences: {
|
||||
@@ -55,13 +64,38 @@ function createMenu(): void {
|
||||
{
|
||||
label: 'File',
|
||||
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' },
|
||||
{ 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' },
|
||||
{ label: 'Export...', accelerator: 'CmdOrCtrl+E', enabled: false },
|
||||
{
|
||||
label: 'Export...',
|
||||
accelerator: 'CmdOrCtrl+E',
|
||||
click: () => sendAppCommand('export-project'),
|
||||
},
|
||||
{ type: 'separator' },
|
||||
process.platform === 'darwin' ? { role: 'close' } : { role: 'quit' },
|
||||
],
|
||||
@@ -69,13 +103,27 @@ function createMenu(): void {
|
||||
{
|
||||
label: 'Edit',
|
||||
submenu: [
|
||||
{ role: 'undo' },
|
||||
{ role: 'redo' },
|
||||
{
|
||||
label: 'Undo',
|
||||
accelerator: 'CmdOrCtrl+Z',
|
||||
click: () => sendAppCommand('undo'),
|
||||
},
|
||||
{
|
||||
label: 'Redo',
|
||||
accelerator: 'CmdOrCtrl+Shift+Z',
|
||||
click: () => sendAppCommand('redo'),
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{ role: 'cut' },
|
||||
{ role: 'copy' },
|
||||
{ role: 'paste' },
|
||||
{ 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: 'toggleDevTools' },
|
||||
{ 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: 'zoomIn' },
|
||||
{ role: 'zoomOut' },
|
||||
@@ -99,6 +184,30 @@ function createMenu(): void {
|
||||
{ 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') {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { contextBridge } from 'electron'
|
||||
import { contextBridge, ipcRenderer } from 'electron'
|
||||
|
||||
contextBridge.exposeInMainWorld('ruwioDesktop', {
|
||||
platform: process.platform,
|
||||
@@ -7,4 +7,11 @@ contextBridge.exposeInMainWorld('ruwioDesktop', {
|
||||
chrome: process.versions.chrome,
|
||||
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)
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -38,6 +38,7 @@
|
||||
"build": {
|
||||
"appId": "app.ruwio.desktop",
|
||||
"productName": "RUWIO",
|
||||
"icon": "icon.png",
|
||||
"directories": {
|
||||
"output": "release"
|
||||
},
|
||||
@@ -46,6 +47,12 @@
|
||||
"dist-electron/**/*",
|
||||
"package.json"
|
||||
],
|
||||
"extraResources": [
|
||||
{
|
||||
"from": "build/icon.png",
|
||||
"to": "build/icon.png"
|
||||
}
|
||||
],
|
||||
"linux": {
|
||||
"target": [
|
||||
"AppImage"
|
||||
|
||||
+76
@@ -220,6 +220,12 @@ async function saveProject() {
|
||||
await markClean()
|
||||
}
|
||||
|
||||
async function saveProjectAs() {
|
||||
currentProjectId.value = null
|
||||
currentProjectName.value = null
|
||||
await saveProject()
|
||||
}
|
||||
|
||||
function openProjectNameDrawer() {
|
||||
// Clicking the project name in the topbar toggles the document
|
||||
// drawer — edits the name inline alongside the rest of the document
|
||||
@@ -546,12 +552,80 @@ function onChromeClick(e: MouseEvent) {
|
||||
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(() => {
|
||||
document.addEventListener('keydown', onKeyDown)
|
||||
document.addEventListener('contextmenu', onDocumentContextMenu)
|
||||
document.addEventListener('paste', onPaste)
|
||||
document.addEventListener('click', onChromeClick)
|
||||
window.addEventListener('resize', onWindowResize)
|
||||
removeDesktopMenuHandler = window.ruwioDesktop?.onMenuCommand?.((command) => {
|
||||
void runDesktopMenuCommand(command)
|
||||
}) ?? null
|
||||
void clipart.initialize()
|
||||
bootWelcome()
|
||||
})
|
||||
@@ -561,6 +635,8 @@ onBeforeUnmount(() => {
|
||||
document.removeEventListener('paste', onPaste)
|
||||
document.removeEventListener('click', onChromeClick)
|
||||
window.removeEventListener('resize', onWindowResize)
|
||||
removeDesktopMenuHandler?.()
|
||||
removeDesktopMenuHandler = null
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
Vendored
+24
@@ -1,5 +1,26 @@
|
||||
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 {
|
||||
interface Window {
|
||||
ruwioDesktop?: {
|
||||
@@ -9,6 +30,9 @@ declare global {
|
||||
chrome?: string
|
||||
node?: string
|
||||
}
|
||||
onMenuCommand?: (
|
||||
callback: (command: RuwioDesktopMenuCommand) => void,
|
||||
) => () => void
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import vue from '@vitejs/plugin-vue'
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
export default defineConfig({
|
||||
base: './',
|
||||
plugins: [vue()],
|
||||
|
||||
server: {
|
||||
|
||||
Reference in new issue
Block a user