diff --git a/backend/ipc.js b/backend/ipc.js index 62a404f..1e0ee53 100644 --- a/backend/ipc.js +++ b/backend/ipc.js @@ -1,6 +1,7 @@ const fs = require('fs') const registerMenu = require('./menu.js') const serial = require('./serial/serial.js').sharedInstance +const { shell } = require('electron'); const { openFolderDialog, @@ -138,6 +139,25 @@ module.exports = function registerIPCHandlers(win, ipcMain, app, dialog) { registerMenu(win, state) }) + ipcMain.handle('launch-app', async (event, urlScheme) => { + // Launch an external app with a custom protocol + return new Promise((resolve, reject) => { + try { + shell.openExternal(urlScheme).then(() => { + resolve(true); // App opened successfully + }).catch(() => { + resolve(false); // App not installed + }); + } catch (err) { + reject(err); + } + }); + }); + + ipcMain.handle('open-url', async (event, url) => { + shell.openExternal(url); + }); + win.on('close', (event) => { console.log('BrowserWindow', 'close') event.preventDefault() diff --git a/preload.js b/preload.js index fbc1579..daef453 100644 --- a/preload.js +++ b/preload.js @@ -2,7 +2,7 @@ console.log('preload') const { contextBridge, ipcRenderer } = require('electron') const path = require('path') const shortcuts = require('./backend/shortcuts.js').shortcuts.global -const { emit, platform } = require('process') +const { platform } = require('process') const SerialBridge = require('./backend/serial/serial-bridge.js') const Disk = { @@ -85,6 +85,21 @@ const Window = { getShortcuts: () => shortcuts } +/** + * Launches an app using the provided URL scheme (e.g. myapp://). If the app is not installed, it will + * fallback to open the provided fallback URL. + * @param {string} url The URL scheme to use to launch the app + * @param {string} fallbackUrl The URL to open if the app is not installed + */ +async function launchApp(url, fallbackUrl) { + const success = await ipcRenderer.invoke('launch-app', url); + + if (!success) { + await ipcRenderer.invoke('open-url', fallbackUrl); // Fallback to open a URL in the default browser + } +} + +contextBridge.exposeInMainWorld('launchApp', launchApp) contextBridge.exposeInMainWorld('BridgeSerial', SerialBridge) contextBridge.exposeInMainWorld('BridgeDisk', Disk) contextBridge.exposeInMainWorld('BridgeWindow', Window) \ No newline at end of file diff --git a/ui/arduino/media/install-package.svg b/ui/arduino/media/install-package.svg new file mode 100644 index 0000000..f26feff --- /dev/null +++ b/ui/arduino/media/install-package.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ui/arduino/store.js b/ui/arduino/store.js index 6fdf059..5b8e45d 100644 --- a/ui/arduino/store.js +++ b/ui/arduino/store.js @@ -116,6 +116,10 @@ async function store(state, emitter) { updateMenu() }) + emitter.on('launch-app', async (url, fallbackUrl) => { + window.launchApp(url, fallbackUrl) + }) + // CONNECTION DIALOG emitter.on('open-connection-dialog', async () => { log('open-connection-dialog') diff --git a/ui/arduino/views/components/toolbar.js b/ui/arduino/views/components/toolbar.js index a0b8a45..ad72a4f 100644 --- a/ui/arduino/views/components/toolbar.js +++ b/ui/arduino/views/components/toolbar.js @@ -69,6 +69,17 @@ function Toolbar(state, emit) { disabled: !_canSave, onClick: () => emit('save') })} + + ${!window.BridgeWindow.isLinux() ? html`
` : ''} + + ${!window.BridgeWindow.isLinux() ? Button({ + icon: 'install-package.svg', + label: `Add Package`, + onClick: () => { + if(state.isConnected) emit('disconnect') // Package installer requires exclusive access to the serial port + emit('launch-app', 'micropython-package-installer://', 'https://github.com/arduino/lab-micropython-package-installer/releases/latest') + } + }) : '' }
@@ -88,7 +99,6 @@ function Toolbar(state, emit) { square: true, onClick: () => emit('change-view', 'file-manager') })} -
`