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));
}
}