diff --git a/.resinci.json b/.resinci.json index 4f85d729..56dc4038 100644 --- a/.resinci.json +++ b/.resinci.json @@ -62,6 +62,12 @@ "depends": [ "polkit-1-auth-agent | policykit-1-gnome | polkit-kde-1" ] + }, + "protocols": { + "name": "etcher", + "schemes": [ + "etcher" + ] } } } diff --git a/electron-builder.yml b/electron-builder.yml index 180bb7fd..c75fb5e4 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -91,3 +91,7 @@ deb: rpm: depends: - util-linux +protocols: + name: etcher + schemes: + - etcher diff --git a/lib/gui/app/components/source-selector/source-selector.tsx b/lib/gui/app/components/source-selector/source-selector.tsx index 42d06a6f..f3d053ed 100644 --- a/lib/gui/app/components/source-selector/source-selector.tsx +++ b/lib/gui/app/components/source-selector/source-selector.tsx @@ -17,6 +17,7 @@ import { faFile, faLink } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { sourceDestination } from 'etcher-sdk'; +import { ipcRenderer, IpcRendererEvent } from 'electron'; import * as _ from 'lodash'; import { GPTPartition, MBRPartition } from 'partitioninfo'; import * as path from 'path'; @@ -237,6 +238,7 @@ export class SourceSelector extends React.Component< this.openImageSelector = this.openImageSelector.bind(this); this.openURLSelector = this.openURLSelector.bind(this); this.reselectImage = this.reselectImage.bind(this); + this.onSelectImage = this.onSelectImage.bind(this); this.onDrop = this.onDrop.bind(this); this.showSelectedImageDetails = this.showSelectedImageDetails.bind(this); this.afterSelected = props.afterSelected.bind(this); @@ -246,10 +248,22 @@ export class SourceSelector extends React.Component< this.unsubscribe = observe(() => { this.setState(getState()); }); + ipcRenderer.on('select-image', this.onSelectImage); + ipcRenderer.send('source-selector-ready'); } public componentWillUnmount() { this.unsubscribe(); + ipcRenderer.removeListener('select-image', this.onSelectImage); + } + + private async onSelectImage(_event: IpcRendererEvent, imagePath: string) { + const isURL = + _.startsWith(imagePath, 'https://') || _.startsWith(imagePath, 'http://'); + await this.selectImageByPath({ + imagePath, + SourceType: isURL ? sourceDestination.Http : sourceDestination.File, + }); } private reselectImage() { diff --git a/lib/gui/etcher.ts b/lib/gui/etcher.ts index e98b475e..76cf50b5 100644 --- a/lib/gui/etcher.ts +++ b/lib/gui/etcher.ts @@ -17,6 +17,7 @@ import { delay } from 'bluebird'; import * as electron from 'electron'; import { autoUpdater } from 'electron-updater'; +import { platform } from 'os'; import * as _ from 'lodash'; import * as path from 'path'; import * as semver from 'semver'; @@ -28,6 +29,8 @@ import * as settings from './app/models/settings'; import * as analytics from './app/modules/analytics'; import { buildWindowMenu } from './menu'; +const customProtocol = 'etcher'; +const scheme = `${customProtocol}://`; const updatablePackageTypes = ['appimage', 'nsis', 'dmg']; const packageUpdatable = _.includes(updatablePackageTypes, packageType); let packageUpdated = false; @@ -54,6 +57,44 @@ async function checkForUpdates(interval: number) { } } +function getCommandLineURL(argv: string[]): string | undefined { + argv = argv.slice(electron.app.isPackaged ? 1 : 2); + if (argv.length) { + const value = argv[argv.length - 1]; + // Take into account electron arguments + if (value.startsWith('--')) { + return; + } + // https://stackoverflow.com/questions/10242115/os-x-strange-psn-command-line-parameter-when-launched-from-finder + if (platform() === 'darwin' && value.startsWith('-psn_')) { + return; + } + return value; + } +} + +const sourceSelectorReady = new Promise((resolve) => { + electron.ipcMain.on('source-selector-ready', resolve); +}); + +async function selectImageURL(url?: string) { + // 'data:,' is the default chromedriver url that is passed as last argument when running spectron tests + if (url !== undefined && url !== 'data:,') { + url = url.startsWith(scheme) ? url.slice(scheme.length) : url; + await sourceSelectorReady; + electron.BrowserWindow.getAllWindows().forEach((window) => { + window.webContents.send('select-image', url); + }); + } +} + +// This will catch clicks on links such as Open in Etcher +// We need to listen to the event before everything else otherwise the event won't be fired +electron.app.on('open-url', async (event, data) => { + event.preventDefault(); + await selectImageURL(data); +}); + async function createMainWindow() { const fullscreen = Boolean(await settings.get('fullscreen')); const defaultWidth = 800; @@ -87,6 +128,8 @@ async function createMainWindow() { }, }); + electron.app.setAsDefaultProtocolClient(customProtocol); + buildWindowMenu(mainWindow); mainWindow.setFullScreen(true); @@ -133,6 +176,7 @@ async function createMainWindow() { } } }); + return mainWindow; } electron.app.allowRendererProcessReuse = false; @@ -145,14 +189,24 @@ electron.app.on('window-all-closed', electron.app.quit); // make use of it to ensure the browser window is completely destroyed. // See https://github.com/electron/electron/issues/5273 electron.app.on('before-quit', () => { + electron.app.releaseSingleInstanceLock(); process.exit(EXIT_CODES.SUCCESS); }); async function main(): Promise { - if (electron.app.isReady()) { - await createMainWindow(); + if (!electron.app.requestSingleInstanceLock()) { + electron.app.quit(); } else { - electron.app.on('ready', createMainWindow); + await electron.app.whenReady(); + const window = await createMainWindow(); + electron.app.on('second-instance', async (_event, argv) => { + if (window.isMinimized()) { + window.restore(); + } + window.focus(); + await selectImageURL(getCommandLineURL(argv)); + }); + await selectImageURL(getCommandLineURL(process.argv)); } }