Refactor Electron's main process #951

Merged
IGassmann merged 3 commits from issue/938 into master 2018-01-19 17:35:59 +01:00
16 changed files with 450 additions and 631 deletions

64
src/main/Daemon.js Normal file
View 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
View 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 {
alexliebowitz commented 2018-01-19 16:59:53 +01:00 (Migrated from github.com)
Review

Does this need to be a whole object? create() is only called once and it doesn't need any state, so maybe just a createTray function that takes an updateAttachedWindow callback?

Does this need to be a whole object? `create()` is only called once and it doesn't need any state, so maybe just a `createTray` function that takes an `updateAttachedWindow` callback?
IGassmann commented 2018-01-19 17:35:27 +01:00 (Migrated from github.com)
Review

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.

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
View 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', () => {
alexliebowitz commented 2018-01-19 16:50:20 +01:00 (Migrated from github.com)
Review

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?
IGassmann commented 2018-01-19 16:58:40 +01:00 (Migrated from github.com)
Review
It's necessary to handle it: https://electronjs.org/docs/api/web-contents#event-crashed
window = null;
});
return window;
};

View file

@ -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 instances (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();
}

View file

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

View file

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

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

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

View file

@ -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=')) {

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -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.