Refactor Electron's main process #951
64
src/main/Daemon.js
Normal file
|
@ -0,0 +1,64 @@
|
|||
/* eslint-disable no-console */
|
||||
import path from 'path';
|
||||
import { spawn, execSync } from 'child_process';
|
||||
|
||||
export default class Daemon {
|
||||
static path = process.env.LBRY_DAEMON || path.join(__static, 'daemon/lbrynet-daemon');
|
||||
subprocess;
|
||||
handlers;
|
||||
|
||||
constructor() {
|
||||
this.handlers = [];
|
||||
}
|
||||
|
||||
launch() {
|
||||
// Kill any running daemon
|
||||
if (process.platform === 'win32') {
|
||||
try {
|
||||
execSync('taskkill /im lbrynet-daemon.exe /t /f');
|
||||
} catch (error) {
|
||||
console.warn(error.message);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
execSync('pkill lbrynet-daemon');
|
||||
} catch (error) {
|
||||
console.warn(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Launching daemon:', Daemon.path);
|
||||
this.subprocess = spawn(Daemon.path);
|
||||
|
||||
this.subprocess.stdout.on('data', data => console.log(`Daemon: ${data}`));
|
||||
this.subprocess.stderr.on('data', data => console.error(`Daemon: ${data}`));
|
||||
this.subprocess.on('exit', () => this.fire('exit'));
|
||||
this.subprocess.on('error', error => console.error(`Daemon: ${error}`));
|
||||
}
|
||||
|
||||
quit() {
|
||||
if (process.platform === 'win32') {
|
||||
try {
|
||||
execSync(`taskkill /pid ${this.subprocess.pid} /t /f`);
|
||||
} catch (error) {
|
||||
console.error(error.message);
|
||||
}
|
||||
} else {
|
||||
this.subprocess.kill();
|
||||
}
|
||||
}
|
||||
|
||||
// Follows the publish/subscribe pattern
|
||||
|
||||
// Subscribe method
|
||||
on(event, handler, context = handler) {
|
||||
this.handlers.push({ event, handler: handler.bind(context) });
|
||||
}
|
||||
|
||||
// Publish method
|
||||
fire(event, args) {
|
||||
this.handlers.forEach(topic => {
|
||||
if (topic.event === event) topic.handler(args);
|
||||
});
|
||||
}
|
||||
}
|
63
src/main/Tray.js
Normal file
|
@ -0,0 +1,63 @@
|
|||
import { app, Menu, Tray as ElectronTray } from 'electron';
|
||||
import path from 'path';
|
||||
import createWindow from './createWindow';
|
||||
|
||||
export default class Tray {
|
||||
I went for a exported I went for a exported `createTray()` function initially. However, since it also needs to manage the state of the window, I finally went for using a class.
|
||||
window;
|
||||
updateAttachedWindow;
|
||||
tray;
|
||||
|
||||
constructor(window, updateAttachedWindow) {
|
||||
this.window = window;
|
||||
this.updateAttachedWindow = updateAttachedWindow;
|
||||
}
|
||||
|
||||
create() {
|
||||
let iconPath;
|
||||
switch (process.platform) {
|
||||
case 'darwin': {
|
||||
iconPath = path.join(__static, '/img/tray/mac/trayTemplate.png');
|
||||
break;
|
||||
}
|
||||
case 'win32': {
|
||||
iconPath = path.join(__static, '/img/tray/windows/tray.ico');
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
iconPath = path.join(__static, '/img/tray/default/tray.png');
|
||||
}
|
||||
}
|
||||
|
||||
this.tray = new ElectronTray(iconPath);
|
||||
|
||||
this.tray.on('double-click', () => {
|
||||
if (!this.window || this.window.isDestroyed()) {
|
||||
this.window = createWindow();
|
||||
this.updateAttachedWindow(this.window);
|
||||
} else {
|
||||
this.window.show();
|
||||
this.window.focus();
|
||||
}
|
||||
});
|
||||
|
||||
this.tray.setToolTip('LBRY App');
|
||||
|
||||
const template = [
|
||||
{
|
||||
label: `Open ${app.getName()}`,
|
||||
click: () => {
|
||||
if (!this.window || this.window.isDestroyed()) {
|
||||
this.window = createWindow();
|
||||
this.updateAttachedWindow(this.window);
|
||||
} else {
|
||||
this.window.show();
|
||||
this.window.focus();
|
||||
}
|
||||
},
|
||||
},
|
||||
{ role: 'quit' },
|
||||
];
|
||||
const contextMenu = Menu.buildFromTemplate(template);
|
||||
this.tray.setContextMenu(contextMenu);
|
||||
}
|
||||
}
|
100
src/main/createWindow.js
Normal file
|
@ -0,0 +1,100 @@
|
|||
import { app, BrowserWindow, dialog } from 'electron';
|
||||
import setupBarMenu from './menu/setupBarMenu';
|
||||
import setupContextMenu from './menu/setupContextMenu';
|
||||
|
||||
export default deepLinkingURIArg => {
|
||||
let windowConfiguration = {
|
||||
backgroundColor: '#155B4A',
|
||||
minWidth: 800,
|
||||
minHeight: 600,
|
||||
autoHideMenuBar: true,
|
||||
show: false,
|
||||
};
|
||||
|
||||
// Disable renderer process's webSecurity on development to enable CORS.
|
||||
windowConfiguration =
|
||||
process.env.NODE_ENV === 'development'
|
||||
? {
|
||||
...windowConfiguration,
|
||||
webPreferences: {
|
||||
webSecurity: false,
|
||||
},
|
||||
}
|
||||
: windowConfiguration;
|
||||
|
||||
const rendererURL =
|
||||
process.env.NODE_ENV === 'development'
|
||||
? `http://localhost:${process.env.ELECTRON_WEBPACK_WDS_PORT}`
|
||||
: `file://${__dirname}/index.html`;
|
||||
|
||||
let window = new BrowserWindow(windowConfiguration);
|
||||
|
||||
window.maximize();
|
||||
|
||||
window.loadURL(rendererURL);
|
||||
|
||||
let deepLinkingURI;
|
||||
// Protocol handler for win32
|
||||
if (
|
||||
!deepLinkingURIArg &&
|
||||
process.platform === 'win32' &&
|
||||
String(process.argv[1]).startsWith('lbry')
|
||||
) {
|
||||
// Keep only command line / deep linked arguments
|
||||
// Windows normalizes URIs when they're passed in from other apps. On Windows, this tries to
|
||||
// restore the original URI that was typed.
|
||||
// - If the URI has no path, Windows adds a trailing slash. LBRY URIs can't have a slash with no
|
||||
// path, so we just strip it off.
|
||||
// - In a URI with a claim ID, like lbry://channel#claimid, Windows interprets the hash mark as
|
||||
// an anchor and converts it to lbry://channel/#claimid. We remove the slash here as well.
|
||||
deepLinkingURI = process.argv[1].replace(/\/$/, '').replace('/#', '#');
|
||||
} else {
|
||||
deepLinkingURI = deepLinkingURIArg;
|
||||
}
|
||||
|
||||
setupBarMenu();
|
||||
setupContextMenu(window);
|
||||
|
||||
window.on('closed', () => {
|
||||
window = null;
|
||||
});
|
||||
|
||||
window.on('focus', () => {
|
||||
window.webContents.send('window-is-focused', null);
|
||||
});
|
||||
|
||||
window.on('unresponsive', () => {
|
||||
dialog.showMessageBox(
|
||||
window,
|
||||
{
|
||||
type: 'warning',
|
||||
buttons: ['Wait', 'Quit'],
|
||||
title: 'LBRY Unresponsive',
|
||||
defaultId: 1,
|
||||
message: 'LBRY is not responding. Would you like to quit?',
|
||||
cancelId: 0,
|
||||
},
|
||||
buttonIndex => {
|
||||
if (buttonIndex === 1) app.quit();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
window.once('ready-to-show', () => {
|
||||
window.show();
|
||||
});
|
||||
|
||||
window.webContents.on('did-finish-load', () => {
|
||||
window.webContents.send('open-uri-requested', deepLinkingURI, true);
|
||||
window.webContents.session.setUserAgent(`LBRY/${app.getVersion()}`);
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
window.webContents.openDevTools();
|
||||
}
|
||||
});
|
||||
|
||||
window.webContents.on('crashed', () => {
|
||||
It doesn't seem like this event is actually sent on the renderer side. Did you mean to build that, or is it something we're going to add later? It doesn't seem like this event is actually sent on the renderer side. Did you mean to build that, or is it something we're going to add later?
It's necessary to handle it: https://electronjs.org/docs/api/web-contents#event-crashed It's necessary to handle it: https://electronjs.org/docs/api/web-contents#event-crashed
|
||||
window = null;
|
||||
});
|
||||
|
||||
return window;
|
||||
};
|
|
@ -1,510 +1,127 @@
|
|||
/* eslint-disable no-console */
|
||||
// Module imports
|
||||
import path from 'path';
|
||||
import url from 'url';
|
||||
import Jayson from 'jayson';
|
||||
import SemVer from 'semver';
|
||||
import https from 'https';
|
||||
import keytar from 'keytar-prebuild';
|
||||
import ChildProcess from 'child_process';
|
||||
import assert from 'assert';
|
||||
import { app, BrowserWindow, globalShortcut, ipcMain, Menu, Tray, dialog } from 'electron';
|
||||
import mainMenu from './menu/mainMenu';
|
||||
import contextMenu from './menu/contextMenu';
|
||||
import SemVer from 'semver';
|
||||
import url from 'url';
|
||||
import https from 'https';
|
||||
import { shell, app, ipcMain, dialog } from 'electron';
|
||||
import Daemon from './Daemon';
|
||||
import Tray from './Tray';
|
||||
import createWindow from './createWindow';
|
||||
|
||||
const localVersion = app.getVersion();
|
||||
|
||||
// Debug configs
|
||||
const isDevelopment = process.env.NODE_ENV === 'development';
|
||||
|
||||
// Misc constants
|
||||
const latestReleaseAPIURL = 'https://api.github.com/repos/lbryio/lbry-app/releases/latest';
|
||||
const daemonPath = process.env.LBRY_DAEMON || path.join(__static, 'daemon/lbrynet-daemon');
|
||||
const rendererURL = isDevelopment
|
||||
? `http://localhost:${process.env.ELECTRON_WEBPACK_WDS_PORT}`
|
||||
: `file://${__dirname}/index.html`;
|
||||
|
||||
const client = Jayson.client.http({
|
||||
host: 'localhost',
|
||||
port: 5279,
|
||||
path: '/',
|
||||
timeout: 1000,
|
||||
});
|
||||
|
||||
// Keep a global reference of the window object, if you don't, the window will
|
||||
// be closed automatically when the JavaScript object is garbage collected.
|
||||
// Keep a global reference, if you don't, they will be closed automatically when the JavaScript
|
||||
// object is garbage collected.
|
||||
let rendererWindow;
|
||||
// Also keep the daemon subprocess alive
|
||||
let daemonSubprocess;
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
let tray;
|
||||
let daemon;
|
||||
|
||||
// This is set to true right before we try to shut the daemon subprocess --
|
||||
// if it dies when we didn't ask it to shut down, we want to alert the user.
|
||||
let daemonStopRequested = false;
|
||||
let isQuitting;
|
||||
|
||||
// When a quit is attempted, we cancel the quit, do some preparations, then
|
||||
// this is set to true and app.quit() is called again to quit for real.
|
||||
let readyToQuit = false;
|
||||
|
||||
// If we receive a URI to open from an external app but there's no window to
|
||||
// sendCredits it to, it's cached in this variable.
|
||||
let openURI = null;
|
||||
|
||||
// Set this to true to minimize on clicking close
|
||||
// false for normal action
|
||||
let minimize = true;
|
||||
|
||||
// Keep the tray also, it is getting GC'd if put in createTray()
|
||||
let tray = null;
|
||||
|
||||
function processRequestedURI(URI) {
|
||||
// Windows normalizes URIs when they're passed in from other apps. On Windows,
|
||||
// this function tries to restore the original URI that was typed.
|
||||
// - If the URI has no path, Windows adds a trailing slash. LBRY URIs
|
||||
// can't have a slash with no path, so we just strip it off.
|
||||
// - In a URI with a claim ID, like lbry://channel#claimid, Windows
|
||||
// interprets the hash mark as an anchor and converts it to
|
||||
// lbry://channel/#claimid. We remove the slash here as well.
|
||||
// On Linux and Mac, we just return the URI as given.
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
return URI.replace(/\/$/, '').replace('/#', '#');
|
||||
}
|
||||
return URI;
|
||||
}
|
||||
|
||||
/*
|
||||
* Replacement for Electron's shell.openItem. The Electron version doesn't
|
||||
* reliably work from the main process, and we need to be able to run it
|
||||
* when no windows are open.
|
||||
*/
|
||||
function openItem(fullPath) {
|
||||
const subprocOptions = {
|
||||
detached: true,
|
||||
stdio: 'ignore',
|
||||
const updateRendererWindow = window => {
|
||||
rendererWindow = window;
|
||||
};
|
||||
|
||||
let child;
|
||||
if (process.platform === 'darwin') {
|
||||
child = ChildProcess.spawn('open', [fullPath], subprocOptions);
|
||||
} else if (process.platform === 'linux') {
|
||||
child = ChildProcess.spawn('xdg-open', [fullPath], subprocOptions);
|
||||
} else if (process.platform === 'win32') {
|
||||
child = ChildProcess.spawn(fullPath, Object.assign({}, subprocOptions, { shell: true }));
|
||||
}
|
||||
const installExtensions = async () => {
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies,global-require
|
||||
const installer = require('electron-devtools-installer');
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies,global-require
|
||||
const devtronExtension = require('devtron');
|
||||
const forceDownload = !!process.env.UPGRADE_EXTENSIONS;
|
||||
const extensions = ['REACT_DEVELOPER_TOOLS', 'REDUX_DEVTOOLS'];
|
||||
|
||||
// Causes child process reference to be garbage collected, allowing main process to exit
|
||||
child.unref();
|
||||
}
|
||||
/*
|
||||
* Quits by first killing the daemon, the calling quitting for real.
|
||||
*/
|
||||
export function safeQuit() {
|
||||
minimize = false;
|
||||
app.quit();
|
||||
}
|
||||
|
||||
function getMenuTemplate() {
|
||||
function getToggleItem() {
|
||||
if (rendererWindow.isVisible() && rendererWindow.isFocused()) {
|
||||
return {
|
||||
label: 'Hide LBRY App',
|
||||
click: () => rendererWindow.hide(),
|
||||
return Promise.all(
|
||||
extensions.map(
|
||||
name => installer.default(installer[name], forceDownload),
|
||||
devtronExtension.install()
|
||||
)
|
||||
).catch(console.log);
|
||||
};
|
||||
}
|
||||
return {
|
||||
label: 'Show LBRY App',
|
||||
click: () => rendererWindow.show(),
|
||||
};
|
||||
}
|
||||
|
||||
return [
|
||||
getToggleItem(),
|
||||
{
|
||||
label: 'Quit',
|
||||
click: () => safeQuit(),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// This needs to be done as for linux the context menu doesn't update automatically(docs)
|
||||
function updateTray() {
|
||||
const trayContextMenu = Menu.buildFromTemplate(getMenuTemplate());
|
||||
if (tray) {
|
||||
tray.setContextMenu(trayContextMenu);
|
||||
} else {
|
||||
console.log('How did update tray get called without a tray?');
|
||||
}
|
||||
}
|
||||
|
||||
function createWindow() {
|
||||
// Disable renderer process's webSecurity on development to enable CORS.
|
||||
let windowConfiguration = {
|
||||
backgroundColor: '#155B4A',
|
||||
minWidth: 800,
|
||||
minHeight: 600,
|
||||
autoHideMenuBar: true,
|
||||
};
|
||||
|
||||
windowConfiguration = isDevelopment
|
||||
? {
|
||||
...windowConfiguration,
|
||||
webPreferences: {
|
||||
webSecurity: false,
|
||||
},
|
||||
}
|
||||
: windowConfiguration;
|
||||
|
||||
let window = new BrowserWindow(windowConfiguration);
|
||||
|
||||
window.webContents.session.setUserAgent(`LBRY/${localVersion}`);
|
||||
|
||||
window.maximize();
|
||||
if (isDevelopment) {
|
||||
window.webContents.openDevTools();
|
||||
}
|
||||
window.loadURL(rendererURL);
|
||||
if (openURI) {
|
||||
// We stored and received a URI that an external app requested before we had a window object
|
||||
window.webContents.on('did-finish-load', () => {
|
||||
window.webContents.send('open-uri-requested', openURI, true);
|
||||
});
|
||||
}
|
||||
|
||||
window.webContents.on('crashed', () => {
|
||||
safeQuit();
|
||||
});
|
||||
|
||||
window.removeAllListeners();
|
||||
|
||||
window.on('close', event => {
|
||||
if (minimize) {
|
||||
event.preventDefault();
|
||||
window.hide();
|
||||
}
|
||||
});
|
||||
|
||||
window.on('closed', () => {
|
||||
window = null;
|
||||
});
|
||||
|
||||
window.on('hide', () => {
|
||||
// Checks what to show in the tray icon menu
|
||||
if (minimize) updateTray();
|
||||
});
|
||||
|
||||
window.on('show', () => {
|
||||
// Checks what to show in the tray icon menu
|
||||
if (minimize) updateTray();
|
||||
});
|
||||
|
||||
window.on('blur', () => {
|
||||
// Checks what to show in the tray icon menu
|
||||
if (minimize) updateTray();
|
||||
|
||||
// Unregisters Alt+F4 shortcut
|
||||
globalShortcut.unregister('Alt+F4');
|
||||
});
|
||||
|
||||
window.on('focus', () => {
|
||||
// Checks what to show in the tray icon menu
|
||||
if (minimize) updateTray();
|
||||
|
||||
// Registers shortcut for closing(quitting) the app
|
||||
globalShortcut.register('Alt+F4', () => safeQuit());
|
||||
|
||||
window.webContents.send('window-is-focused', null);
|
||||
});
|
||||
|
||||
window.on('unresponsive', () => {
|
||||
dialog.showMessageBox(
|
||||
window,
|
||||
{
|
||||
type: 'warning',
|
||||
buttons: ['Wait', 'Quit'],
|
||||
title: 'LBRY Unresponsive',
|
||||
defaultId: 1,
|
||||
message: 'LBRY is not responding. Would you like to quit?',
|
||||
cancelId: 0,
|
||||
},
|
||||
buttonIndex => {
|
||||
if (buttonIndex === 1) safeQuit();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
mainMenu();
|
||||
|
||||
return window;
|
||||
}
|
||||
|
||||
function createTray() {
|
||||
// Minimize to tray logic follows:
|
||||
// Set the tray icon
|
||||
let iconPath;
|
||||
if (process.platform === 'darwin') {
|
||||
// Using @2x for mac retina screens so the icon isn't blurry
|
||||
// file name needs to include "Template" at the end for dark menu bar
|
||||
iconPath = path.join(__static, '/img/fav/macTemplate@2x.png');
|
||||
} else {
|
||||
iconPath = path.join(__static, '/img/fav/32x32.png');
|
||||
}
|
||||
|
||||
tray = new Tray(iconPath);
|
||||
tray.setToolTip('LBRY App');
|
||||
tray.setTitle('LBRY');
|
||||
tray.on('double-click', () => {
|
||||
rendererWindow.show();
|
||||
});
|
||||
}
|
||||
|
||||
function handleOpenURIRequested(URI) {
|
||||
if (!rendererWindow) {
|
||||
// Window not created yet, so store up requested URI for when it is
|
||||
openURI = processRequestedURI(URI);
|
||||
} else {
|
||||
if (rendererWindow.isMinimized()) {
|
||||
rendererWindow.restore();
|
||||
} else if (!rendererWindow.isVisible()) {
|
||||
rendererWindow.show();
|
||||
}
|
||||
|
||||
rendererWindow.focus();
|
||||
rendererWindow.webContents.send('open-uri-requested', processRequestedURI(URI));
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Quits without any preparation. When a quit is requested (either through the
|
||||
* interface or through app.quit()), we abort the quit, try to shut down the daemon,
|
||||
* and then call this to quit for real.
|
||||
*/
|
||||
function quitNow() {
|
||||
readyToQuit = true;
|
||||
safeQuit();
|
||||
}
|
||||
|
||||
function handleDaemonSubprocessExited() {
|
||||
console.log('The daemon has exited.');
|
||||
daemonSubprocess = null;
|
||||
if (!daemonStopRequested) {
|
||||
// We didn't request to stop the daemon, so display a
|
||||
// warning and schedule a quit.
|
||||
//
|
||||
// TODO: maybe it would be better to restart the daemon?
|
||||
if (rendererWindow) {
|
||||
console.log('Did not request daemon stop, so quitting in 5 seconds.');
|
||||
rendererWindow.loadURL(`file://${__static}/warning.html`);
|
||||
setTimeout(quitNow, 5000);
|
||||
} else {
|
||||
console.log('Did not request daemon stop, so quitting.');
|
||||
quitNow();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function launchDaemon() {
|
||||
assert(!daemonSubprocess, 'Tried to launch daemon twice');
|
||||
|
||||
console.log('Launching daemon:', daemonPath);
|
||||
daemonSubprocess = ChildProcess.spawn(daemonPath);
|
||||
// Need to handle the data event instead of attaching to
|
||||
// process.stdout because the latter doesn't work. I believe on
|
||||
// windows it buffers stdout and we don't get any meaningful output
|
||||
daemonSubprocess.stdout.on('data', buf => {
|
||||
console.log(String(buf).trim());
|
||||
});
|
||||
daemonSubprocess.stderr.on('data', buf => {
|
||||
console.log(String(buf).trim());
|
||||
});
|
||||
daemonSubprocess.on('exit', handleDaemonSubprocessExited);
|
||||
}
|
||||
|
||||
const isSecondaryInstance = app.makeSingleInstance(argv => {
|
||||
if (argv.length >= 2) {
|
||||
handleOpenURIRequested(argv[1]); // This will handle restoring and focusing the window
|
||||
} else if (rendererWindow) {
|
||||
if (rendererWindow.isMinimized()) {
|
||||
rendererWindow.restore();
|
||||
} else if (!rendererWindow.isVisible()) {
|
||||
rendererWindow.show();
|
||||
}
|
||||
rendererWindow.focus();
|
||||
}
|
||||
});
|
||||
|
||||
if (isSecondaryInstance) {
|
||||
// We're not in the original process, so quit
|
||||
quitNow();
|
||||
}
|
||||
|
||||
function launchDaemonIfNotRunning() {
|
||||
// Check if the daemon is already running. If we get
|
||||
// an error its because its not running
|
||||
console.log('Checking for lbrynet daemon');
|
||||
client.request('status', [], err => {
|
||||
if (err) {
|
||||
console.log('lbrynet daemon needs to be launched');
|
||||
launchDaemon();
|
||||
} else {
|
||||
console.log('lbrynet daemon is already running');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Taken from webtorrent-desktop
|
||||
function checkLinuxTraySupport(cb) {
|
||||
// Check that we're on Ubuntu (or another debian system) and that we have
|
||||
// libappindicator1.
|
||||
ChildProcess.exec('dpkg --get-selections libappindicator1', (err, stdout) => {
|
||||
if (err) return cb(err);
|
||||
// Unfortunately there's no cleaner way, as far as I can tell, to check
|
||||
// whether a debian package is installed:
|
||||
if (stdout.endsWith('\tinstall\n')) {
|
||||
return cb(null);
|
||||
}
|
||||
return cb(new Error('debian package not installed'));
|
||||
});
|
||||
}
|
||||
|
||||
// When a quit is attempted, this is called. It attempts to shutdown the daemon,
|
||||
// then calls quitNow() to quit for real.
|
||||
function shutdownDaemonAndQuit(evenIfNotStartedByApp = false) {
|
||||
function doShutdown() {
|
||||
console.log('Shutting down daemon');
|
||||
daemonStopRequested = true;
|
||||
client.request('daemon_stop', [], err => {
|
||||
if (err) {
|
||||
console.log(`received error when stopping lbrynet-daemon. Error message: ${err.message}\n`);
|
||||
console.log('You will need to manually kill the daemon.');
|
||||
} else {
|
||||
console.log('Successfully stopped daemon via RPC call.');
|
||||
quitNow();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (daemonSubprocess) {
|
||||
doShutdown();
|
||||
} else if (!evenIfNotStartedByApp) {
|
||||
console.log('Not killing lbrynet-daemon because app did not start it');
|
||||
quitNow();
|
||||
} else {
|
||||
doShutdown();
|
||||
}
|
||||
|
||||
// Is it safe to start the installer before the daemon finishes running?
|
||||
// If not, we should wait until the daemon is closed before we start the install.
|
||||
}
|
||||
|
||||
if (isDevelopment) {
|
||||
import('devtron')
|
||||
.then(({ install }) => {
|
||||
install();
|
||||
console.log('Added Extension: Devtron');
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
});
|
||||
import('electron-devtools-installer')
|
||||
.then(({ default: installExtension, REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS }) => {
|
||||
app.on('ready', () => {
|
||||
[REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS].forEach(extension => {
|
||||
installExtension(extension)
|
||||
.then(name => console.log(`Added Extension: ${name}`))
|
||||
.catch(err => console.log('An error occurred: ', err));
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
});
|
||||
}
|
||||
|
||||
app.setAsDefaultProtocolClient('lbry');
|
||||
app.setName('LBRY');
|
||||
|
||||
app.on('ready', () => {
|
||||
launchDaemonIfNotRunning();
|
||||
if (process.platform === 'linux') {
|
||||
checkLinuxTraySupport(err => {
|
||||
if (!err) createTray();
|
||||
else minimize = false;
|
||||
});
|
||||
} else {
|
||||
createTray();
|
||||
}
|
||||
rendererWindow = createWindow();
|
||||
});
|
||||
|
||||
// Quit when all windows are closed.
|
||||
app.on('window-all-closed', () => {
|
||||
// On macOS it is common for applications and their menu bar
|
||||
// to stay active until the user quits explicitly with Cmd + Q
|
||||
if (process.platform !== 'darwin') {
|
||||
app.on('ready', async () => {
|
||||
daemon = new Daemon();
|
||||
daemon.on('exit', () => {
|
||||
daemon = null;
|
||||
if (!isQuitting) {
|
||||
dialog.showErrorBox(
|
||||
'Daemon has Exited',
|
||||
'The daemon may have encountered an unexpected error, or another daemon instance is already running.'
|
||||
);
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
app.on('before-quit', event => {
|
||||
if (!readyToQuit) {
|
||||
// We need to shutdown the daemon before we're ready to actually quit. This
|
||||
// event will be triggered re-entrantly once preparation is done.
|
||||
event.preventDefault();
|
||||
shutdownDaemonAndQuit();
|
||||
} else {
|
||||
console.log('Quitting.');
|
||||
daemon.launch();
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
await installExtensions();
|
||||
}
|
||||
rendererWindow = createWindow();
|
||||
tray = new Tray(rendererWindow, updateRendererWindow);
|
||||
tray.create();
|
||||
});
|
||||
|
||||
app.on('activate', () => {
|
||||
// On macOS it's common to re-create a window in the app when the
|
||||
// dock icon is clicked and there are no other windows open.
|
||||
if (rendererWindow === null) {
|
||||
createWindow();
|
||||
}
|
||||
if (!rendererWindow) rendererWindow = createWindow();
|
||||
});
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
app.on('open-url', (event, URI) => {
|
||||
handleOpenURIRequested(URI);
|
||||
app.on('will-quit', () => {
|
||||
isQuitting = true;
|
||||
if (daemon) daemon.quit();
|
||||
});
|
||||
} else if (process.argv.length >= 2) {
|
||||
handleOpenURIRequested(process.argv[1]);
|
||||
|
||||
// https://electronjs.org/docs/api/app#event-will-finish-launching
|
||||
app.on('will-finish-launching', () => {
|
||||
// Protocol handler for macOS
|
||||
app.on('open-url', (event, URL) => {
|
||||
event.preventDefault();
|
||||
if (rendererWindow && !rendererWindow.isDestroyed()) {
|
||||
rendererWindow.webContents.send('open-uri-requested', URL);
|
||||
rendererWindow.show();
|
||||
rendererWindow.focus();
|
||||
} else {
|
||||
rendererWindow = createWindow(URL);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
// Subscribe to event so the app doesn't quit when closing the window.
|
||||
});
|
||||
|
||||
ipcMain.on('upgrade', (event, installerPath) => {
|
||||
app.on('quit', () => {
|
||||
console.log('Launching upgrade installer at', installerPath);
|
||||
// This gets triggered called after *all* other quit-related events, so
|
||||
// we'll only get here if we're fully prepared and quitting for real.
|
||||
openItem(installerPath);
|
||||
shell.openItem(installerPath);
|
||||
});
|
||||
|
||||
if (rendererWindow) {
|
||||
rendererWindow.loadURL(`file://${__static}/upgrade.html`);
|
||||
}
|
||||
|
||||
shutdownDaemonAndQuit(true);
|
||||
// wait for daemon to shut down before upgrading
|
||||
// what to do if no shutdown in a long time?
|
||||
console.log('Update downloaded to', installerPath);
|
||||
console.log(
|
||||
'The app will close, and you will be prompted to install the latest version of LBRY.'
|
||||
);
|
||||
console.log('After the install is complete, please reopen the app.');
|
||||
app.quit();
|
||||
});
|
||||
|
||||
ipcMain.on('version-info-requested', () => {
|
||||
function formatRc(ver) {
|
||||
// Adds dash if needed to make RC suffix semver friendly
|
||||
// Adds dash if needed to make RC suffix SemVer friendly
|
||||
return ver.replace(/([^-])rc/, '$1-rc');
|
||||
}
|
||||
|
||||
let result = '';
|
||||
const localVersion = app.getVersion();
|
||||
const latestReleaseAPIURL = 'https://api.github.com/repos/lbryio/lbry-app/releases/latest';
|
||||
const opts = {
|
||||
headers: {
|
||||
'User-Agent': `LBRY/${localVersion}`,
|
||||
},
|
||||
};
|
||||
let result = '';
|
||||
|
||||
const req = https.get(Object.assign(opts, url.parse(latestReleaseAPIURL)), res => {
|
||||
res.on('data', data => {
|
||||
|
@ -549,8 +166,33 @@ ipcMain.on('set-auth-token', (event, token) => {
|
|||
});
|
||||
|
||||
process.on('uncaughtException', error => {
|
||||
console.error(error);
|
||||
safeQuit();
|
||||
dialog.showErrorBox('Error Encountered', `Caught error: ${error}`);
|
||||
isQuitting = true;
|
||||
if (daemon) daemon.quit();
|
||||
app.exit(1);
|
||||
});
|
||||
|
||||
export { contextMenu };
|
||||
// Force single instance application
|
||||
const isSecondInstance = app.makeSingleInstance(argv => {
|
||||
// Protocol handler for win32
|
||||
// argv: An array of the second instance’s (command line / deep linked) arguments
|
||||
|
||||
let URI;
|
||||
if (process.platform === 'win32' && String(argv[1]).startsWith('lbry')) {
|
||||
// Keep only command line / deep linked arguments
|
||||
URI = argv[1].replace(/\/$/, '').replace('/#', '#');
|
||||
}
|
||||
|
||||
if (rendererWindow && !rendererWindow.isDestroyed()) {
|
||||
rendererWindow.webContents.send('open-uri-requested', URI);
|
||||
|
||||
rendererWindow.show();
|
||||
rendererWindow.focus();
|
||||
} else {
|
||||
rendererWindow = createWindow(URI);
|
||||
}
|
||||
});
|
||||
|
||||
if (isSecondInstance) {
|
||||
app.exit();
|
||||
}
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
import { Menu } from 'electron';
|
||||
|
||||
const contextMenuTemplate = [{ role: 'cut' }, { role: 'copy' }, { role: 'paste' }];
|
||||
|
||||
export default (win, posX, posY, showDevItems) => {
|
||||
const template = contextMenuTemplate.slice();
|
||||
if (showDevItems) {
|
||||
template.push({
|
||||
type: 'separator',
|
||||
});
|
||||
template.push({
|
||||
label: 'Inspect Element',
|
||||
click() {
|
||||
win.inspectElement(posX, posY);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Menu.buildFromTemplate(template).popup(win);
|
||||
};
|
|
@ -1,138 +0,0 @@
|
|||
import { app, Menu, shell } from 'electron';
|
||||
import { safeQuit } from '../index';
|
||||
|
||||
const baseTemplate = [
|
||||
{
|
||||
label: 'File',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Quit',
|
||||
accelerator: 'CommandOrControl+Q',
|
||||
click: () => safeQuit(),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Edit',
|
||||
submenu: [
|
||||
{
|
||||
role: 'undo',
|
||||
},
|
||||
{
|
||||
role: 'redo',
|
||||
},
|
||||
{
|
||||
type: 'separator',
|
||||
},
|
||||
{
|
||||
role: 'cut',
|
||||
},
|
||||
{
|
||||
role: 'copy',
|
||||
},
|
||||
{
|
||||
role: 'paste',
|
||||
},
|
||||
{
|
||||
role: 'selectall',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'View',
|
||||
submenu: [
|
||||
{
|
||||
role: 'reload',
|
||||
},
|
||||
{
|
||||
label: 'Developer',
|
||||
submenu: [
|
||||
{
|
||||
role: 'forcereload',
|
||||
},
|
||||
{
|
||||
role: 'toggledevtools',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'separator',
|
||||
},
|
||||
{
|
||||
role: 'togglefullscreen',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: 'help',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Learn More',
|
||||
click(item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
focusedWindow.webContents.send('open-menu', '/help');
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Frequently Asked Questions',
|
||||
click() {
|
||||
shell.openExternal('https://lbry.io/faq');
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'separator',
|
||||
},
|
||||
{
|
||||
label: 'Report Issue',
|
||||
click() {
|
||||
shell.openExternal('https://lbry.io/faq/contributing#report-a-bug');
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'separator',
|
||||
},
|
||||
{
|
||||
label: 'Developer API Guide',
|
||||
click() {
|
||||
shell.openExternal('https://lbry.io/quickstart');
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const macOSAppMenuTemplate = {
|
||||
label: app.getName(),
|
||||
submenu: [
|
||||
{
|
||||
role: 'about',
|
||||
},
|
||||
{
|
||||
type: 'separator',
|
||||
},
|
||||
{
|
||||
role: 'hide',
|
||||
},
|
||||
{
|
||||
role: 'hideothers',
|
||||
},
|
||||
{
|
||||
role: 'unhide',
|
||||
},
|
||||
{
|
||||
type: 'separator',
|
||||
},
|
||||
{
|
||||
role: 'quit',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default () => {
|
||||
const template = baseTemplate.slice();
|
||||
if (process.platform === 'darwin') {
|
||||
template.unshift(macOSAppMenuTemplate);
|
||||
}
|
||||
Menu.setApplicationMenu(Menu.buildFromTemplate(template));
|
||||
};
|
89
src/main/menu/setupBarMenu.js
Normal file
|
@ -0,0 +1,89 @@
|
|||
import { app, Menu, shell } from 'electron';
|
||||
|
||||
export default () => {
|
||||
const template = [
|
||||
{
|
||||
label: 'Edit',
|
||||
submenu: [
|
||||
{ role: 'undo' },
|
||||
{ role: 'redo' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'cut' },
|
||||
{ role: 'copy' },
|
||||
{ role: 'paste' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'View',
|
||||
submenu: [
|
||||
{ role: 'reload' },
|
||||
{
|
||||
label: 'Developer',
|
||||
submenu: [{ role: 'forcereload' }, { role: 'toggledevtools' }],
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{ role: 'togglefullscreen' },
|
||||
],
|
||||
},
|
||||
{
|
||||
role: 'window',
|
||||
submenu: [{ role: 'minimize' }, { role: 'close' }],
|
||||
},
|
||||
{
|
||||
role: 'help',
|
||||
submenu: [
|
||||
{
|
||||
label: 'Learn More',
|
||||
click: (menuItem, browserWindow) => {
|
||||
if (browserWindow) {
|
||||
browserWindow.webContents.send('open-menu', '/help');
|
||||
} else {
|
||||
shell.openExternal('https://lbry.io/faq');
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Frequently Asked Questions',
|
||||
click: () => {
|
||||
shell.openExternal('https://lbry.io/faq');
|
||||
},
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Report Issue',
|
||||
click: () => {
|
||||
shell.openExternal('https://github.com/lbryio/lbry-app/issues/new');
|
||||
},
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Developer API Guide',
|
||||
click: () => {
|
||||
shell.openExternal('https://lbry.io/quickstart');
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const darwinTemplateAddition = {
|
||||
label: app.getName(),
|
||||
submenu: [
|
||||
{ role: 'about' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'services', submenu: [] },
|
||||
{ type: 'separator' },
|
||||
{ role: 'hide' },
|
||||
{ role: 'hideothers' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'quit' },
|
||||
],
|
||||
};
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
template.unshift(darwinTemplateAddition);
|
||||
}
|
||||
|
||||
const menu = Menu.buildFromTemplate(template);
|
||||
Menu.setApplicationMenu(menu);
|
||||
};
|
26
src/main/menu/setupContextMenu.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
// @flow
|
||||
import { Menu, BrowserWindow } from 'electron';
|
||||
|
||||
export default (rendererWindow: BrowserWindow) => {
|
||||
rendererWindow.webContents.on('context-menu', (e, params) => {
|
||||
const { x, y } = params;
|
||||
|
||||
const template = [{ role: 'cut' }, { role: 'copy' }, { role: 'paste' }];
|
||||
|
||||
const developmentTemplateAddition = [
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Inspect element',
|
||||
click: () => {
|
||||
rendererWindow.inspectElement(x, y);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
template.push(...developmentTemplateAddition);
|
||||
}
|
||||
|
||||
Menu.buildFromTemplate(template).popup();
|
||||
});
|
||||
};
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable react/jsx-filename-extension */
|
||||
import amplitude from 'amplitude-js';
|
||||
import App from 'component/app';
|
||||
import SnackBar from 'component/snackBar';
|
||||
|
@ -5,7 +6,6 @@ import SplashScreen from 'component/splash';
|
|||
import * as ACTIONS from 'constants/action_types';
|
||||
import { ipcRenderer, remote, shell } from 'electron';
|
||||
import lbry from 'lbry';
|
||||
/* eslint-disable react/jsx-filename-extension */
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
|
@ -17,13 +17,6 @@ import 'scss/all.scss';
|
|||
import store from 'store';
|
||||
import app from './app';
|
||||
|
||||
const { contextMenu } = remote.require('./main.js');
|
||||
|
||||
window.addEventListener('contextmenu', event => {
|
||||
contextMenu(remote.getCurrentWindow(), event.x, event.y, app.env === 'development');
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
ipcRenderer.on('open-uri-requested', (event, uri, newSession) => {
|
||||
if (uri && uri.startsWith('lbry://')) {
|
||||
if (uri.startsWith('lbry://?verify=')) {
|
||||
|
|
BIN
static/img/tray/default/tray.png
Normal file
After Width: | Height: | Size: 2 KiB |
BIN
static/img/tray/mac/trayTemplate.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
static/img/tray/mac/trayTemplate@2x.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
static/img/tray/windows/tray.ico
Normal file
After Width: | Height: | Size: 4.3 KiB |
BIN
static/img/untitled folder/trayTemplate.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
static/img/untitled folder/trayTemplate@2x.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
|
@ -1,7 +1,7 @@
|
|||
const Path = require('path');
|
||||
const path = require('path');
|
||||
const FlowFlowPlugin = require('./flowtype-plugin');
|
||||
|
||||
const ELECTRON_RENDERER_PROCESS_ROOT = Path.resolve(__dirname, 'src/renderer/');
|
||||
const ELECTRON_RENDERER_PROCESS_ROOT = path.resolve(__dirname, 'src/renderer/');
|
||||
|
||||
module.exports = {
|
||||
// This rule is temporarily necessary until https://github.com/electron-userland/electron-webpack/issues/60 is fixed.
|
||||
|
|
Does this need to be a whole object?
create()
is only called once and it doesn't need any state, so maybe just acreateTray
function that takes anupdateAttachedWindow
callback?