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/
|
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
|
||||||
|
|||||||
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 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') {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
},
|
||||||
})
|
})
|
||||||
@@ -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
@@ -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>
|
||||||
|
|
||||||
|
|||||||
Vendored
+24
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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: {
|
||||||
|
|||||||
Reference in new issue
Block a user