diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 000000000..2fbd88746 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,34 @@ +{ + "plugins": [ + "flowtype" + ], + "extends": [ + "airbnb", + "plugin:import/electron", + "plugin:flowtype/recommended", + "plugin:prettier/recommended" + ], + "settings": { + "import/resolver": { + "webpack": { + "config": "webpack.renderer.additions.js" + } + } + }, + "parser": "babel-eslint", + "env": { + "browser": true, + "node": true + }, + "globals": { + "__static": true, + "staticResourcesPath": true, + "__": true, + "__n": true, + "app": true + }, + "rules": { + "import/no-commonjs": 1, + "import/no-amd": 1 + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index e20de7b15..023767f02 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ /lbry-app /lbry-venv /static/daemon/lbrynet* +/static/locales /daemon/build /daemon/venv /daemon/requirements.txt diff --git a/.lintstagedrc b/.lintstagedrc new file mode 100644 index 000000000..bd9f228ce --- /dev/null +++ b/.lintstagedrc @@ -0,0 +1,12 @@ +{ + "linters": { + "src/**/*.{js,jsx,scss,json}": [ + "prettier --write", + "git add" + ], + "src/**/*.{js,jsx}": [ + "eslint --fix", + "git add" + ] + } +} diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 000000000..1d3fce725 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,5 @@ +{ + "trailingComma": "es5", + "printWidth": 100, + "singleQuote": true +} \ No newline at end of file diff --git a/README.md b/README.md index aed0caca0..9cb673480 100644 --- a/README.md +++ b/README.md @@ -32,9 +32,9 @@ Run `yarn dev` This will set up a server that will automatically compile any changes made inside `src\` folder and automatically reload the app without losing the state. -### Packaging +### Build -Run `yarn dist` +Run `yarn build` We use [electron-builder](https://github.com/electron-userland/electron-builder) to create distributable packages. @@ -60,16 +60,16 @@ exit python -m pip install -r build\requirements.txt npm install -g yarn yarn install -yarn dist +yarn build ``` -3. Download the lbry daemon and cli [binaries](https://github.com/lbryio/lbry/releases) and place them in `dist\` - -### Building lbry-app -Run `yarn dist` +3. Download the lbry daemon and cli [binaries](https://github.com/lbryio/lbry/releases) and place them in `static\daemon` ### Ongoing Development Run `yarn dev` +### Build +Run `yarn build` + This will set up a server that will automatically compile any changes made inside `src\` folder and automatically reload the app without losing the state. ## Internationalization diff --git a/build/build.ps1 b/build/build.ps1 index 3fb9d90b8..730fd80af 100644 --- a/build/build.ps1 +++ b/build/build.ps1 @@ -26,7 +26,7 @@ dir static\daemon\ # verify that daemon binary is there rm daemon.zip # build electron app -yarn dist +yarn build dir dist # verify that binary was built/named correctly python build\upload_assets.py diff --git a/build/build.sh b/build/build.sh index 37a26b0a1..2273eff00 100755 --- a/build/build.sh +++ b/build/build.sh @@ -77,7 +77,7 @@ if [ "$FULL_BUILD" == "true" ]; then security unlock-keychain -p ${KEYCHAIN_PASSWORD} osx-build.keychain fi - yarn dist + yarn build # electron-build has a publish feature, but I had a hard time getting # it to reliably work and it also seemed difficult to configure. Not proud of diff --git a/src/renderer/extractLocals.js b/build/extractLocals.js similarity index 51% rename from src/renderer/extractLocals.js rename to build/extractLocals.js index b4437dbf5..8f48b1492 100644 --- a/src/renderer/extractLocals.js +++ b/build/extractLocals.js @@ -1,20 +1,21 @@ -var extract = require("i18n-extract"); +const extract = require("i18n-extract"); const fs = require("fs"); +const path = require("path"); -var dir = __dirname + "/../../dist/locales"; -var path = dir + "/en.json"; +const outputDir = `${__dirname}/../static/locales`; +const outputPath = `${outputDir}/en.json`; -if (!fs.existsSync(dir)) { - fs.mkdirSync(dir); +if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir); } -fs.writeFile(path, "{}", "utf8", function(err) { +fs.writeFile(outputPath, "{}", "utf8", err => { if (err) { return console.log(err); } - var enLocale = require(path); + const enLocale = require(outputPath); - const keys = extract.extractFromFiles(["js/**/*.{js,jsx}"], { + const keys = extract.extractFromFiles("src/**/*.{js,jsx}", { marker: "__", }); @@ -22,21 +23,21 @@ fs.writeFile(path, "{}", "utf8", function(err) { reports = reports.concat(extract.findMissing(enLocale, keys)); if (reports.length > 0) { - fs.readFile(path, "utf8", function readFileCallback(err, data) { + fs.readFile(outputPath, "utf8", (err, data) => { if (err) { console.log(err); } else { localeObj = JSON.parse(data); - for (var i = 0; i < reports.length; i++) { + for (let i = 0; i < reports.length; i++) { // no need to care for other types than MISSING because starting file will always be empty if (reports[i].type === "MISSING") { localeObj[reports[i].key] = reports[i].key; } } - var json = JSON.stringify(localeObj, null, "\t"); //convert it back to json-string - fs.writeFile(path, json, "utf8", function callback(err) { + const json = JSON.stringify(localeObj, null, "\t"); // convert it back to json-string + fs.writeFile(outputPath, json, "utf8", err => { if (err) { throw err; } diff --git a/package.json b/package.json index f15fb3714..55b1a0905 100644 --- a/package.json +++ b/package.json @@ -14,21 +14,23 @@ "name": "LBRY Inc.", "email": "hello@lbry.io" }, + "main": "src/main/index.js", "scripts": { - "extract-langs": "node src/renderer/extractLocals.js", + "extract-langs": "node build/extractLocals.js", "dev": "electron-webpack dev", "compile": "electron-webpack && yarn extract-langs", - "dist": "yarn compile && electron-builder", - "dist:dir": "yarn dist -- --dir -c.compression=store -c.mac.identity=null", + "build": "yarn compile && electron-builder build", "postinstall": "electron-builder install-app-deps", - "precommit": "lint-staged" + "precommit": "lint-staged", + "lint": "eslint 'src/**/*.{js,jsx}' --fix", + "pretty-print": "prettier 'src/**/*.{js,jsx,scss,json}' --write" }, - "main": "src/main/index.js", "keywords": [ "lbry" ], "dependencies": { "amplitude-js": "^4.0.0", + "bluebird": "^3.5.1", "classnames": "^2.2.5", "electron-dl": "^1.6.0", "formik": "^0.10.4", @@ -64,18 +66,27 @@ "y18n": "^4.0.0" }, "devDependencies": { + "babel-eslint": "^8.0.3", "babel-plugin-module-resolver": "^3.0.0", "babel-plugin-react-require": "^3.0.0", "babel-polyfill": "^6.20.0", "babel-preset-env": "^1.6.1", "babel-preset-react": "^6.24.1", "babel-preset-stage-2": "^6.18.0", - "bluebird": "^3.5.1", "devtron": "^1.4.0", "electron": "^1.7.9", - "electron-builder": "^19.48.2", + "electron-builder": "^19.49.0", "electron-devtools-installer": "^2.2.1", "electron-webpack": "^1.11.0", + "eslint": "^4.13.1", + "eslint-config-airbnb": "^16.1.0", + "eslint-config-prettier": "^2.9.0", + "eslint-import-resolver-webpack": "^0.8.3", + "eslint-plugin-flowtype": "^2.40.1", + "eslint-plugin-import": "^2.8.0", + "eslint-plugin-jsx-a11y": "^6.0.3", + "eslint-plugin-prettier": "^2.4.0", + "eslint-plugin-react": "^7.5.1", "flow-babel-webpack-plugin": "^1.1.0", "flow-bin": "^0.61.0", "flow-typed": "^2.2.3", @@ -98,12 +109,6 @@ "yarn": "^1.3" }, "license": "MIT", - "lint-staged": { - "src/**/*.{js,jsx}": [ - "prettier --trailing-comma es5 --write", - "git add" - ] - }, "lbrySettings": { "lbrynetDaemonVersion": "0.18.0", "lbrynetDaemonUrlTemplate": "https://github.com/lbryio/lbry/releases/download/vDAEMONVER/lbrynet-daemon-vDAEMONVER-OSNAME.zip" diff --git a/src/main/index.js b/src/main/index.js index 0c8e114d6..383be184d 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -1,67 +1,39 @@ +/* eslint-disable no-console */ // Module imports -const { - app, - BrowserWindow, - ipcMain, - Menu, - Tray, - globalShortcut, -} = require("electron"); -const path = require("path"); -const url = require("url"); -const jayson = require("jayson"); -const semver = require("semver"); -const https = require("https"); -const keytar = require("keytar"); -// tree-kill has better cross-platform handling of -// killing a process. child-process.kill was unreliable -const kill = require("tree-kill"); -const child_process = require("child_process"); -const assert = require("assert"); +import Path from 'path'; +import Url from 'url'; +import Jayson from 'jayson'; +import Semver from 'semver'; +import Https from 'https'; +import Keytar from 'keytar'; +import ChildProcess from 'child_process'; +import Assert from 'assert'; +import { app, BrowserWindow, globalShortcut, ipcMain, Menu, Tray } from 'electron'; +import mainMenu from './menu/mainMenu'; + const localVersion = app.getVersion(); -const setMenu = require("./menu/main-menu.js"); -export const contextMenu = require("./menu/context-menu"); +export { contextMenu as Default } from './menu/contextMenu'; // Debug configs -const isDevelopment = process.env.NODE_ENV === "development"; -if (isDevelopment) { - try { - const { - default: installExtension, - REACT_DEVELOPER_TOOLS, - REDUX_DEVTOOLS, - } = require("electron-devtools-installer"); - 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 (err) { - console.error(err); - } -} +const isDevelopment = process.env.NODE_ENV === 'development'; // Misc constants -const LATEST_RELEASE_API_URL = - "https://api.github.com/repos/lbryio/lbry-app/releases/latest"; -const DAEMON_PATH = - process.env.LBRY_DAEMON || path.join(__static, "daemon/lbrynet-daemon"); +const LATEST_RELEASE_API_URL = 'https://api.github.com/repos/lbryio/lbry-app/releases/latest'; +const DAEMON_PATH = 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`; -let client = jayson.client.http({ - host: "localhost", +const client = Jayson.client.http({ + host: 'localhost', port: 5279, - path: "/", + 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. -let win; +let rendererWindow; // Also keep the daemon subprocess alive let daemonSubprocess; @@ -94,11 +66,10 @@ function processRequestedUri(uri) { // 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("/#", "#"); - } else { - return uri; + if (process.platform === 'win32') { + return uri.replace(/\/$/, '').replace('/#', '#'); } + return uri; } /* @@ -109,226 +80,21 @@ function processRequestedUri(uri) { function openItem(fullPath) { const subprocOptions = { detached: true, - stdio: "ignore", + stdio: 'ignore', }; let child; - if (process.platform === "darwin") { - child = child_process.spawn("open", [fullPath], subprocOptions); - } else if (process.platform === "linux") { - child = child_process.spawn("xdg-open", [fullPath], subprocOptions); - } else if (process.platform === "win32") { - child = child_process.spawn( - fullPath, - Object.assign({}, subprocOptions, { shell: true }) - ); + 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 })); } // Causes child process reference to be garbage collected, allowing main process to exit child.unref(); } - -function getPidsForProcessName(name) { - if (process.platform === "win32") { - const tasklistOut = child_process.execSync( - `tasklist /fi "Imagename eq ${name}.exe" /nh`, - { encoding: "utf8" } - ); - if (tasklistOut.startsWith("INFO")) { - return []; - } else { - return tasklistOut.match(/[^\r\n]+/g).map(line => line.split(/\s+/)[1]); // Second column of every non-empty line - } - } else { - const pgrepOut = child_process.spawnSync("pgrep", ["-x", name], { - encoding: "utf8", - }).stdout; - return pgrepOut.match(/\d+/g); - } -} - -function createWindow() { - // Disable renderer process's webSecurity on development to enable CORS. - win = isDevelopment - ? new BrowserWindow({ - backgroundColor: "#155B4A", - minWidth: 800, - minHeight: 600, - webPreferences: { webSecurity: false }, - }) - : new BrowserWindow({ - backgroundColor: "#155B4A", - minWidth: 800, - minHeight: 600, - }); - - win.webContents.session.setUserAgent(`LBRY/${localVersion}`); - - win.maximize(); - if (isDevelopment) { - win.webContents.openDevTools(); - } - win.loadURL(rendererUrl); - if (openUri) { - // We stored and received a URI that an external app requested before we had a window object - win.webContents.on("did-finish-load", () => { - win.webContents.send("open-uri-requested", openUri, true); - }); - } - - win.removeAllListeners(); - - win.on("close", function(event) { - if (minimize) { - event.preventDefault(); - win.hide(); - } - }); - - win.on("closed", () => { - win = null; - }); - - win.on("hide", () => { - // Checks what to show in the tray icon menu - if (minimize) updateTray(); - }); - - win.on("show", () => { - // Checks what to show in the tray icon menu - if (minimize) updateTray(); - }); - - win.on("blur", () => { - // Checks what to show in the tray icon menu - if (minimize) updateTray(); - - // Unregisters Alt+F4 shortcut - globalShortcut.unregister("Alt+F4"); - }); - - win.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()); - - win.webContents.send("window-is-focused", null); - }); - - // Menu bar - win.setAutoHideMenuBar(true); - win.setMenuBarVisibility(isDevelopment); - setMenu(); -} - -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", () => { - win.show(); - }); -} - -// This needs to be done as for linux the context menu doesn't update automatically(docs) -function updateTray() { - let contextMenu = Menu.buildFromTemplate(getMenuTemplate()); - if (tray) { - tray.setContextMenu(contextMenu); - } else { - console.log("How did update tray get called without a tray?"); - } -} - -function getMenuTemplate() { - return [ - getToggleItem(), - { - label: "Quit", - click: () => safeQuit(), - }, - ]; - - function getToggleItem() { - if (win.isVisible() && win.isFocused()) { - return { - label: "Hide LBRY App", - click: () => win.hide(), - }; - } else { - return { - label: "Show LBRY App", - click: () => win.show(), - }; - } - } -} - -function handleOpenUriRequested(uri) { - if (!win) { - // Window not created yet, so store up requested URI for when it is - openUri = processRequestedUri(uri); - } else { - if (win.isMinimized()) { - win.restore(); - } else if (!win.isVisible()) { - win.show(); - } - - win.focus(); - win.webContents.send("open-uri-requested", processRequestedUri(uri)); - } -} - -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 (win) { - console.log("Did not request daemon stop, so quitting in 5 seconds."); - win.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:", DAEMON_PATH); - daemonSubprocess = child_process.spawn(DAEMON_PATH); - // 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); -} - /* * Quits by first killing the daemon, the calling quitting for real. */ @@ -337,6 +103,155 @@ export function safeQuit() { app.quit(); } +function getMenuTemplate() { + function getToggleItem() { + if (rendererWindow.isVisible() && rendererWindow.isFocused()) { + return { + label: 'Hide LBRY App', + click: () => rendererWindow.hide(), + }; + } + 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.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); + }); + + 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, @@ -347,16 +262,52 @@ function quitNow() { 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:', DAEMON_PATH); + daemonSubprocess = ChildProcess.spawn(DAEMON_PATH); + // 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 (win) { - if (win.isMinimized()) { - win.restore(); - } else if (!win.isVisible()) { - win.show(); + } else if (rendererWindow) { + if (rendererWindow.isMinimized()) { + rendererWindow.restore(); + } else if (!rendererWindow.isVisible()) { + rendererWindow.show(); } - win.focus(); + rendererWindow.focus(); } }); @@ -368,126 +319,44 @@ if (isSecondaryInstance) { 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", [], function(err, res) { + console.log('Checking for lbrynet daemon'); + client.request('status', [], err => { if (err) { - console.log("lbrynet daemon needs to be launched"); + console.log('lbrynet daemon needs to be launched'); launchDaemon(); } else { - console.log("lbrynet daemon is already running"); + console.log('lbrynet daemon is already running'); } }); } -/* - * Last resort for killing unresponsive daemon instances. - * Looks for any processes called "lbrynet-daemon" and - * tries to force kill them. - */ -function forceKillAllDaemonsAndQuit() { - console.log( - "Attempting to force kill any running lbrynet-daemon instances..." - ); - - const daemonPids = getPidsForProcessName("lbrynet-daemon"); - if (!daemonPids) { - console.log("No lbrynet-daemon found running."); - quitNow(); - } else { - console.log( - `Found ${ - daemonPids.length - } running daemon instances. Attempting to force kill...` - ); - - for (const pid of daemonPids) { - let daemonKillAttemptsComplete = 0; - kill(pid, "SIGKILL", err => { - daemonKillAttemptsComplete++; - if (err) { - console.log( - `Failed to force kill daemon task with pid ${pid}. Error message: ${ - err.message - }` - ); - } else { - console.log(`Force killed daemon task with pid ${pid}.`); - } - if (daemonKillAttemptsComplete >= daemonPids.length - 1) { - quitNow(); - } - }); +// 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); } - } -} - -app.setAsDefaultProtocolClient("lbry"); - -app.on("ready", function() { - launchDaemonIfNotRunning(); - if (process.platform === "linux") { - checkLinuxTraySupport(err => { - if (!err) createTray(); - else minimize = false; - }); - } else { - createTray(); - } - 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.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."); - } -}); - -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 (win === null) { - createWindow(); - } -}); - -if (process.platform === "darwin") { - app.on("open-url", (event, uri) => { - handleOpenUriRequested(uri); + return cb(new Error('debian package not installed')); }); -} else if (process.argv.length >= 2) { - handleOpenUriRequested(process.argv[1]); } // 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"); + console.log('Shutting down daemon'); daemonStopRequested = true; - client.request("daemon_stop", [], (err, res) => { + 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."); + 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."); + console.log('Successfully stopped daemon via RPC call.'); quitNow(); } }); @@ -496,7 +365,7 @@ function shutdownDaemonAndQuit(evenIfNotStartedByApp = false) { if (daemonSubprocess) { doShutdown(); } else if (!evenIfNotStartedByApp) { - console.log("Not killing lbrynet-daemon because app did not start it"); + console.log('Not killing lbrynet-daemon because app did not start it'); quitNow(); } else { doShutdown(); @@ -506,111 +375,154 @@ function shutdownDaemonAndQuit(evenIfNotStartedByApp = false) { // If not, we should wait until the daemon is closed before we start the install. } -// Taken from webtorrent-desktop -function checkLinuxTraySupport(cb) { - // Check that we're on Ubuntu (or another debian system) and that we have - // libappindicator1. - child_process.exec("dpkg --get-selections libappindicator1", function( - 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")) { - cb(null); - } else { - cb(new Error("debian package not installed")); - } - }); +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); + }); } -ipcMain.on("upgrade", (event, installerPath) => { - app.on("quit", () => { - console.log("Launching upgrade installer at", installerPath); +app.setAsDefaultProtocolClient('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.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.'); + } +}); + +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 (process.platform === 'darwin') { + app.on('open-url', (event, uri) => { + handleOpenUriRequested(uri); + }); +} else if (process.argv.length >= 2) { + handleOpenUriRequested(process.argv[1]); +} + +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); }); - if (win) { - win.loadURL(`file://${__static}/upgrade.html`); + 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('Update downloaded to', installerPath); console.log( - "The app will close, and you will be prompted to install the latest version of LBRY." + '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."); + console.log('After the install is complete, please reopen the app.'); }); -ipcMain.on("version-info-requested", () => { +ipcMain.on('version-info-requested', () => { function formatRc(ver) { // Adds dash if needed to make RC suffix semver friendly - return ver.replace(/([^-])rc/, "$1-rc"); + return ver.replace(/([^-])rc/, '$1-rc'); } - let result = ""; + let result = ''; const opts = { headers: { - "User-Agent": `LBRY/${localVersion}`, + 'User-Agent': `LBRY/${localVersion}`, }, }; - const req = https.get( - Object.assign(opts, url.parse(LATEST_RELEASE_API_URL)), - res => { - res.on("data", data => { - result += data; - }); - res.on("end", () => { - const tagName = JSON.parse(result).tag_name; - const [_, remoteVersion] = tagName.match(/^v([\d.]+(?:-?rc\d+)?)$/); - if (!remoteVersion) { - if (win) { - win.webContents.send("version-info-received", null); - } - } else { - const upgradeAvailable = semver.gt( - formatRc(remoteVersion), - formatRc(localVersion) - ); - if (win) { - win.webContents.send("version-info-received", { - remoteVersion, - localVersion, - upgradeAvailable, - }); - } + const req = Https.get(Object.assign(opts, Url.parse(LATEST_RELEASE_API_URL)), res => { + res.on('data', data => { + result += data; + }); + res.on('end', () => { + const tagName = JSON.parse(result).tag_name; + const [, remoteVersion] = tagName.match(/^v([\d.]+(?:-?rc\d+)?)$/); + if (!remoteVersion) { + if (rendererWindow) { + rendererWindow.webContents.send('version-info-received', null); } - }); - } - ); + } else { + const upgradeAvailable = Semver.gt(formatRc(remoteVersion), formatRc(localVersion)); + if (rendererWindow) { + rendererWindow.webContents.send('version-info-received', { + remoteVersion, + localVersion, + upgradeAvailable, + }); + } + } + }); + }); - req.on("error", err => { - console.log("Failed to get current version from GitHub. Error:", err); - if (win) { - win.webContents.send("version-info-received", null); + req.on('error', err => { + console.log('Failed to get current version from GitHub. Error:', err); + if (rendererWindow) { + rendererWindow.webContents.send('version-info-received', null); } }); }); -ipcMain.on("get-auth-token", event => { - keytar.getPassword("LBRY", "auth_token").then(token => { - event.sender.send( - "auth-token-response", - token ? token.toString().trim() : null - ); +ipcMain.on('get-auth-token', event => { + Keytar.getPassword('LBRY', 'auth_token').then(token => { + event.sender.send('auth-token-response', token ? token.toString().trim() : null); }); }); -ipcMain.on("set-auth-token", (event, token) => { - keytar.setPassword( - "LBRY", - "auth_token", - token ? token.toString().trim() : null - ); +ipcMain.on('set-auth-token', (event, token) => { + Keytar.setPassword('LBRY', 'auth_token', token ? token.toString().trim() : null); }); diff --git a/src/main/menu/context-menu.js b/src/main/menu/context-menu.js deleted file mode 100644 index f38de5d1c..000000000 --- a/src/main/menu/context-menu.js +++ /dev/null @@ -1,36 +0,0 @@ -const {Menu} = require('electron'); -const electron = require('electron'); -const app = electron.app; - -const contextMenuTemplate = [ - { - role: 'cut', - }, - { - role: 'copy', - }, - { - role: 'paste', - }, -]; - -module.exports = { - showContextMenu(win, posX, posY, showDevItems) { - let template = contextMenuTemplate.slice(); - if (showDevItems) { - template.push({ - type: 'separator', - }); - template.push( - { - label: 'Inspect Element', - click() { - win.inspectElement(posX, posY); - } - } - ); - } - - Menu.buildFromTemplate(template).popup(win); - }, -}; diff --git a/src/main/menu/contextMenu.js b/src/main/menu/contextMenu.js new file mode 100644 index 000000000..ad33c6bc2 --- /dev/null +++ b/src/main/menu/contextMenu.js @@ -0,0 +1,30 @@ +import { Menu } from 'electron'; + +const contextMenuTemplate = [ + { + role: 'cut', + }, + { + role: 'copy', + }, + { + role: 'paste', + }, +]; + +export default function contextMenu(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); +} diff --git a/src/main/menu/main-menu.js b/src/main/menu/mainMenu.js similarity index 63% rename from src/main/menu/main-menu.js rename to src/main/menu/mainMenu.js index 7b5854d75..879ce8206 100644 --- a/src/main/menu/main-menu.js +++ b/src/main/menu/mainMenu.js @@ -1,5 +1,5 @@ -const { app, shell, Menu } = require('electron'); -const { safeQuit } = require('../index.js'); +import { app, Menu, shell } from 'electron'; +import { safeQuit } from '../index'; const baseTemplate = [ { @@ -7,10 +7,10 @@ const baseTemplate = [ submenu: [ { label: 'Quit', - accelerator: "CommandOrControl+Q", + accelerator: 'CommandOrControl+Q', click: () => safeQuit(), }, - ] + ], }, { label: 'Edit', @@ -36,32 +36,32 @@ const baseTemplate = [ { role: 'selectall', }, - ] + ], }, { label: 'View', submenu: [ { - role: 'reload' + role: 'reload', }, { label: 'Developer', submenu: [ { - role: 'forcereload' + role: 'forcereload', }, { - role: 'toggledevtools' + role: 'toggledevtools', }, - ] + ], }, { - type: 'separator' + type: 'separator', }, { - role: 'togglefullscreen' - } - ] + role: 'togglefullscreen', + }, + ], }, { role: 'help', @@ -72,34 +72,34 @@ const baseTemplate = [ if (focusedWindow) { focusedWindow.webContents.send('open-menu', '/help'); } - } + }, }, { label: 'Frequently Asked Questions', - click(item, focusedWindow){ - shell.openExternal('https://lbry.io/faq') - } + click() { + shell.openExternal('https://lbry.io/faq'); + }, }, { - type: 'separator' + type: 'separator', }, { label: 'Report Issue', - click(item, focusedWindow){ + click() { shell.openExternal('https://lbry.io/faq/contributing#report-a-bug'); - } + }, }, { - type: 'separator' + type: 'separator', }, { label: 'Developer API Guide', - click(item, focusedWindow){ - shell.openExternal('https://lbry.io/quickstart') - } + click() { + shell.openExternal('https://lbry.io/quickstart'); + }, }, - ] - } + ], + }, ]; const macOSAppMenuTemplate = { @@ -126,11 +126,13 @@ const macOSAppMenuTemplate = { { role: 'quit', }, - ] + ], }; -module.exports = () => { - let template = baseTemplate.slice(); - (process.platform === 'darwin') && template.unshift(macOSAppMenuTemplate); - Menu.setApplicationMenu(Menu.buildFromTemplate(template)); +export default () => { + const template = baseTemplate.slice(); + if (process.platform === 'darwin') { + template.unshift(macOSAppMenuTemplate); + } + Menu.setApplicationMenu(Menu.buildFromTemplate(template)); }; diff --git a/src/renderer/app.js b/src/renderer/app.js index e249810f9..75752fac7 100644 --- a/src/renderer/app.js +++ b/src/renderer/app.js @@ -1,29 +1,40 @@ -import store from "store.js"; -import { remote } from "electron"; +import store from 'store'; +import { remote } from 'electron'; +import Path from 'path'; +import y18n from 'y18n'; -const env = process.env.NODE_ENV || "production"; -const config = { - ...require(`./config/${env}`), -}; -const i18n = require("y18n")({ - directory: remote.app.getAppPath() + "/locales", +const env = process.env.NODE_ENV || 'production'; +const i18n = y18n({ + directory: Path.join(remote.app.getAppPath(), '/../static/locales').replace(/\\/g, '\\\\'), updateFiles: false, - locale: "en", + locale: 'en', }); + const logs = []; const app = { - env: env, - config: config, - store: store, - i18n: i18n, - logs: logs, - log: function(message) { + env, + store, + i18n, + logs, + log(message) { logs.push(message); }, }; -window.__ = i18n.__; -window.__n = i18n.__n; +// Workaround for https://github.com/electron-userland/electron-webpack/issues/52 +if (env !== 'development') { + window.staticResourcesPath = Path.join(remote.app.getAppPath(), '../static').replace( + /\\/g, + '\\\\' + ); +} else { + window.staticResourcesPath = ''; +} +// eslint-disable-next-line no-underscore-dangle +global.__ = i18n.__; +// eslint-disable-next-line no-underscore-dangle +global.__n = i18n.__n; global.app = app; -module.exports = app; + +export default app; diff --git a/src/renderer/component/address/index.js b/src/renderer/component/address/index.js index 00bec962e..b861b75c5 100644 --- a/src/renderer/component/address/index.js +++ b/src/renderer/component/address/index.js @@ -1,6 +1,6 @@ -import { connect } from "react-redux"; -import { doShowSnackBar } from "redux/actions/app"; -import Address from "./view"; +import { connect } from 'react-redux'; +import { doShowSnackBar } from 'redux/actions/app'; +import Address from './view'; export default connect(null, { doShowSnackBar, diff --git a/src/renderer/component/address/view.jsx b/src/renderer/component/address/view.jsx index 96049eb35..65ea563c0 100644 --- a/src/renderer/component/address/view.jsx +++ b/src/renderer/component/address/view.jsx @@ -1,8 +1,8 @@ -import React from "react"; -import PropTypes from "prop-types"; -import { clipboard } from "electron"; -import Link from "component/link"; -import classnames from "classnames"; +import React from 'react'; +import PropTypes from 'prop-types'; +import { clipboard } from 'electron'; +import Link from 'component/link'; +import classnames from 'classnames'; export default class Address extends React.PureComponent { static propTypes = { @@ -21,8 +21,8 @@ export default class Address extends React.PureComponent { return (
{__("Content-Type")} | +{__('Content-Type')} | {mediaType} |
{__("Language")} | +{__('Language')} | {language} |
{__("License")} | +{__('License')} | {license} |
{__("Downloaded to")} | +{__('Downloaded to')} | - openFolder(downloadPath)}> - {downloadPath} - + openFolder(downloadPath)}>{downloadPath} |