diff --git a/assets/locales/en.json b/assets/locales/en.json index d6d27b3fb..617658b89 100644 --- a/assets/locales/en.json +++ b/assets/locales/en.json @@ -37,6 +37,7 @@ "close": "Close", "ok": "OK", "cancel": "Cancel", + "enable": "Enable", "reportTheError": "Report the error", "restartIpfsDesktop": "Restart IPFS Desktop", "openLogs": "Open logs", @@ -150,14 +151,53 @@ "title": "IPFS on PATH", "message": "Could not add IPFS to the PATH." }, + "launchAtLoginNotSupported": { + "title": "Error", + "message": "Launch at login is not supported on your platform." + }, + "launchAtLoginFailed": { + "title": "Error", + "message": "Launch at login could not be enabled on your machine." + }, + "enableIpfsOnPath": { + "title": "Enable IPFS on PATH", + "message": "By enabling this option, IPFS will be available on your command line as \"ipfs\". This action is reversible.", + "action": "Enable" + }, + "disableIpfsOnPath": { + "title": "Disable IPFS on PATH", + "message": "By disabling this option, IPFS will no longer be available on your command line as \"ipfs\".", + "action": "Disable" + }, + "enableGlobalTakeScreenshotShortcut": { + "title": "Enable screenshot shortcut", + "message": "By enabling this, the shortcut \"{ accelerator }\" will be available to take screenshots as long as IPFS Desktop is running." + }, + "enableGlobalDownloadShortcut": { + "title": "Enable download shortcut", + "message": "By enabling this, the shortcut \"{ accelerator }\" will be available to download files as long as IPFS Desktop is running." + }, + "installNpmOnIpfsWarning": { + "title": "Install npm on IPFS", + "message": "This experimental feature installs the \"ipfs-npm\" package on your system. It requires Node.js to be installed.", + "action": "Install" + }, + "unableToInstallNpmOnIpfs": { + "title": "Error", + "message": "It was not possible to install \"ipfs-npm\" package on your system. Please check the logs for more information or try installing it manually by running \"npm install -g ipfs-npm\" on your command line." + }, + "unableToUninstallNpmOnIpfs": { + "title": "Error", + "message": "It was not possible to uninstall \"ipfs-npm\" package on your system. Please check the logs for more information or try uninstalling it manually by running \"npm uninstall -g ipfs-npm\" on your command line." + }, "settings": { "settings": "Settings", "preferences": "Preferences", - "launchOnStartup": "Enable Launch at Login", - "ipfsCommandLineTools": "Enable Command Line Tools", - "takeScreenshotShortcut": "Enable Take Screenshot Shortcut", - "downloadHashShortcut": "Enable Download Hash Shortcut", + "launchOnStartup": "Launch at Login", + "ipfsCommandLineTools": "Command Line Tools", + "takeScreenshotShortcut": "Global Screenshot Shortcut", + "downloadHashShortcut": "Global Download Shortcut", "experiments": "Experiments", - "npmOnIpfs": "Enable npm on IPFS" + "npmOnIpfs": "npm on IPFS" } } diff --git a/src/auto-launch.js b/src/auto-launch.js index cfd0a229e..0ae30f56b 100644 --- a/src/auto-launch.js +++ b/src/auto-launch.js @@ -1,4 +1,5 @@ const { app } = require('electron') +const i18n = require('i18next') const os = require('os') const path = require('path') const fs = require('fs-extra') @@ -7,6 +8,7 @@ const createToggler = require('./create-toggler') const logger = require('./common/logger') const store = require('./common/store') const { IS_MAC, IS_WIN } = require('./common/consts') +const { showDialog, recoverableErrorDialog } = require('./dialogs') const CONFIG_KEY = 'autoLaunch' @@ -48,21 +50,39 @@ async function disable () { } module.exports = async function () { - const activate = async (value, oldValue) => { + const activate = async ({ newValue, oldValue, feedback }) => { if (process.env.NODE_ENV === 'development') { logger.info('[launch on startup] unavailable during development') + + if (feedback) { + showDialog({ + title: 'Launch at Login', + message: 'Not available during development.', + buttons: [i18n.t('close')] + }) + } + return } if (!isSupported()) { logger.info('[launch on startup] not supported on this platform') + + if (feedback) { + showDialog({ + title: i18n.t('launchAtLoginNotSupported.title'), + message: i18n.t('launchAtLoginNotSupported.message'), + buttons: [i18n.t('close')] + }) + } + return false } - if (value === oldValue) return + if (newValue === oldValue) return try { - if (value === true) { + if (newValue === true) { await enable() logger.info('[launch on startup] enabled') } else { @@ -73,12 +93,21 @@ module.exports = async function () { return true } catch (err) { logger.error(`[launch on startup] ${err.toString()}`) + + if (feedback) { + recoverableErrorDialog(err, { + title: i18n.t('launchAtLoginFailed.title'), + message: i18n.t('launchAtLoginFailed.message') + }) + } + return false } } - activate(store.get(CONFIG_KEY, false)) + activate({ newValue: store.get(CONFIG_KEY, false) }) createToggler(CONFIG_KEY, activate) } module.exports.CONFIG_KEY = CONFIG_KEY +module.exports.isSupported = isSupported diff --git a/src/dialogs/errors.js b/src/dialogs/errors.js index bd6851667..6aa10eb60 100644 --- a/src/dialogs/errors.js +++ b/src/dialogs/errors.js @@ -47,7 +47,7 @@ function criticalErrorDialog (e) { // Shows a recoverable error dialog with the default title and message. // Passing an options object alongside the error can be used to override // the title and message. -function recoverableErrorDialog (e, options = {}) { +function recoverableErrorDialog (e, options) { const cfg = { title: i18n.t('recoverableErrorDialog.title'), message: i18n.t('recoverableErrorDialog.message'), @@ -59,12 +59,14 @@ function recoverableErrorDialog (e, options = {}) { ] } - if (options.title) { - cfg.title = options.title - } + if (options) { + if (options.title) { + cfg.title = options.title + } - if (options.message) { - cfg.message = options.message + if (options.message) { + cfg.message = options.message + } } const option = dialog(cfg) diff --git a/src/download-hash.js b/src/download-hash.js index a73bbd00c..804518f31 100644 --- a/src/download-hash.js +++ b/src/download-hash.js @@ -88,7 +88,11 @@ async function downloadHash (ctx) { } module.exports = function (ctx) { - setupGlobalShortcut(ctx, { + setupGlobalShortcut({ + confirmationDialog: { + title: i18n.t('enableGlobalDownloadShortcut.title'), + message: i18n.t('enableGlobalDownloadShortcut.message', { accelerator: SHORTCUT }) + }, settingsOption: CONFIG_KEY, accelerator: SHORTCUT, action: () => { diff --git a/src/exec-or-sudo.js b/src/exec-or-sudo.js index 2a05a4c95..1163e4e2f 100644 --- a/src/exec-or-sudo.js +++ b/src/exec-or-sudo.js @@ -15,7 +15,7 @@ const env = { sudo: 'env ELECTRON_RUN_AS_NODE=1' } -const getResult = (err, stdout, stderr, scope, failSilently) => { +const getResult = (err, stdout, stderr, scope, failSilently, errorOptions) => { if (stdout) { logger.info(`[${scope}] sudo: stdout: ${stdout.toString().trim()}`) } @@ -37,14 +37,14 @@ const getResult = (err, stdout, stderr, scope, failSilently) => { } else if (str.includes('User did not grant permission')) { dialog.showErrorBox(i18n.t('noPermissionDialog.title'), i18n.t('noPermissionDialog.message')) } else { - recoverableErrorDialog(err) + recoverableErrorDialog(err, errorOptions) } } return false } -module.exports = async function ({ script, scope, failSilently, trySudo = true }) { +module.exports = async function ({ script, scope, failSilently, trySudo = true, errorOptions }) { const dataArg = `--data="${app.getPath('userData')}"` let err = null @@ -61,7 +61,7 @@ module.exports = async function ({ script, scope, failSilently, trySudo = true } if (!trySudo) { if (!failSilently) { - recoverableErrorDialog(err) + recoverableErrorDialog(err, errorOptions) } return false @@ -71,7 +71,7 @@ module.exports = async function ({ script, scope, failSilently, trySudo = true } const command = `${env.sudo} "${process.execPath}" "${script}" ${dataArg}` return new Promise(resolve => { sudo.exec(command, { name: 'IPFS Desktop' }, (err, stdout, stderr) => { - resolve(getResult(err, stdout, stderr, scope, failSilently)) + resolve(getResult(err, stdout, stderr, scope, failSilently, errorOptions)) }) }) } diff --git a/src/ipfs-on-path/index.js b/src/ipfs-on-path/index.js index 3d0fbed4d..25440ca9a 100644 --- a/src/ipfs-on-path/index.js +++ b/src/ipfs-on-path/index.js @@ -7,14 +7,49 @@ const execOrSudo = require('../exec-or-sudo') const logger = require('../common/logger') const store = require('../common/store') const { IS_WIN } = require('../common/consts') -const { recoverableErrorDialog } = require('../dialogs') +const { showDialog, recoverableErrorDialog } = require('../dialogs') const CONFIG_KEY = 'ipfsOnPath' -module.exports = async function (ctx) { - createToggler(CONFIG_KEY, async (value, oldValue) => { - if (value === oldValue || (oldValue === null && !value)) return - if (value === true) return run('install') +const errorMessage = { + title: i18n.t('cantAddIpfsToPath.title'), + message: i18n.t('cantAddIpfsToPath.message') +} + +module.exports = async function () { + createToggler(CONFIG_KEY, async ({ newValue, oldValue }) => { + if (newValue === oldValue || (oldValue === null && !newValue)) { + return + } + + if (newValue === true) { + if (showDialog({ + title: i18n.t('enableIpfsOnPath.title'), + message: i18n.t('enableIpfsOnPath.message'), + buttons: [ + i18n.t('enableIpfsOnPath.action'), + i18n.t('cancel') + ] + }) !== 0) { + // User canceled + return + } + + return run('install') + } + + if (showDialog({ + title: i18n.t('disableIpfsOnPath.title'), + message: i18n.t('disableIpfsOnPath.message'), + buttons: [ + i18n.t('disableIpfsOnPath.action'), + i18n.t('cancel') + ] + }) !== 0) { + // User canceled + return + } + return run('uninstall') }) @@ -56,10 +91,7 @@ async function runWindows (script, { failSilently }) { logger.error(`[ipfs on path] ${err.toString()}`) if (!failSilently) { - recoverableErrorDialog(err, { - title: i18n.t('cantAddIpfsToPath.title'), - message: i18n.t('cantAddIpfsToPath.message') - }) + recoverableErrorDialog(err, errorMessage) } return resolve(false) @@ -80,6 +112,7 @@ async function run (script, { trySudo = true, failSilently = false } = {}) { script: join(__dirname, `./scripts/${script}.js`), scope: 'ipfs on path', trySudo, - failSilently + failSilently, + errorOptions: errorMessage }) } diff --git a/src/npm-on-ipfs/index.js b/src/npm-on-ipfs/index.js index bad403fe9..2ee0a2537 100644 --- a/src/npm-on-ipfs/index.js +++ b/src/npm-on-ipfs/index.js @@ -1,45 +1,27 @@ const which = require('which') +const i18n = require('i18next') const pkg = require('./package') const logger = require('../common/logger') const store = require('../common/store') +const { showDialog } = require('../dialogs') const createToggler = require('../create-toggler') const CONFIG_KEY = 'experiments.npmOnIpfs' module.exports = function (ctx) { - let interval = null + // Every 12 hours, check if `ipfs-npm` is installed and, if it is, + // tries to update it to the latest version. + setInterval(existsAndUpdate, 43200000) - createToggler(CONFIG_KEY, async (value, oldValue) => { - if (value === oldValue || oldValue === null) return true + // Configure toggler + createToggler(CONFIG_KEY, toggle) - // If the user is telling to (un)install even though they have (un)installed - // ipfs-npm package manually. - const manual = isPkgInstalled() === value - - if (value === true) { - if (!manual && !await pkg.install()) return false - interval = setInterval(existsAndUpdate, 43200000) // every 12 hours - return true - } - - clearInterval(interval) - return manual || pkg.uninstall() - }) - - let opt = store.get(CONFIG_KEY, null) - const exists = isPkgInstalled() - - if (opt === null) { + // When running for the first time, update the config to know if `ipfs-npm` + // is installed or not. + if (store.get(CONFIG_KEY, null) === null) { + const exists = isPkgInstalled() logger.info(`[npm on ipfs] 1st time running and package is ${exists ? 'installed' : 'not installed'}`) store.set(CONFIG_KEY, exists) - opt = exists - } - - if (opt === true) { - logger.info('[npm on ipfs] set to update every 12 hours') - interval = setInterval(existsAndUpdate, 43200000) // every 12 hours - } else { - logger.info('[npm on ipfs] no action taken') } } @@ -56,3 +38,34 @@ function existsAndUpdate () { store.set(CONFIG_KEY, false) } } + +async function toggle ({ newValue, oldValue }) { + if (newValue === oldValue || oldValue === null) { + return true + } + + // If the user is telling to (un)install even though they have (un)installed + // ipfs-npm package manually. + const manual = isPkgInstalled() === newValue + + if (!newValue) { + return manual || pkg.uninstall() + } + + const opt = showDialog({ + type: 'warning', + title: i18n.t('installNpmOnIpfsWarning.title'), + message: i18n.t('installNpmOnIpfsWarning.message'), + buttons: [ + i18n.t('installNpmOnIpfsWarning.action'), + i18n.t('cancel') + ] + }) + + if (opt !== 0) { + // User canceled + return + } + + return manual || pkg.install() +} diff --git a/src/npm-on-ipfs/package.js b/src/npm-on-ipfs/package.js index 980ca6860..11ff37eb5 100644 --- a/src/npm-on-ipfs/package.js +++ b/src/npm-on-ipfs/package.js @@ -1,6 +1,8 @@ const util = require('util') +const i18n = require('i18next') const logger = require('../common/logger') const { IS_WIN } = require('../common/consts') +const { recoverableErrorDialog } = require('../dialogs') const childProcess = require('child_process') const execFile = util.promisify(childProcess.execFile) @@ -31,8 +33,12 @@ async function install () { await execFile(npmBin, ['install', '-g', 'ipfs-npm']) logger.info('[npm on ipfs] ipfs-npm: installed globally') return true - } catch (e) { - logger.error(`[npm on ipfs] ${e.toString()}`) + } catch (err) { + logger.error(`[npm on ipfs] ${err.toString()}`) + recoverableErrorDialog(err, { + title: i18n.t('unableToInstallNpmOnIpfs.title'), + message: i18n.t('unableToInstallNpmOnIpfs.message') + }) return false } } @@ -42,8 +48,12 @@ async function uninstall () { await execFile(npmBin, ['uninstall', '-g', 'ipfs-npm']) logger.info('[npm on ipfs] ipfs-npm: uninstalled globally') return true - } catch (e) { - logger.error(`[npm on ipfs] ${e.toString()}`) + } catch (err) { + logger.error(`[npm on ipfs] ${err.toString()}`) + recoverableErrorDialog(err, { + title: i18n.t('unableToUninstallNpmOnIpfs.title'), + message: i18n.t('unableToUninstallNpmOnIpfs.message') + }) return false } } diff --git a/src/setup-global-shortcut.js b/src/setup-global-shortcut.js index bf3c3f7da..dc1607148 100644 --- a/src/setup-global-shortcut.js +++ b/src/setup-global-shortcut.js @@ -1,15 +1,30 @@ const { globalShortcut, ipcMain } = require('electron') +const i18n = require('i18next') const createToggler = require('./create-toggler') const store = require('./common/store') const { IS_MAC } = require('./common/consts') +const { showDialog } = require('./dialogs') // This function registers a global shortcut/accelerator with a certain action // and (de)activates it according to its 'settingsOption' value on settings. -module.exports = function (ctx, { settingsOption, accelerator, action }) { - const activate = (value, oldValue) => { - if (value === oldValue) return +module.exports = function ({ settingsOption, accelerator, action, confirmationDialog }) { + const activate = ({ newValue, oldValue, feedback }) => { + if (newValue === oldValue) return + + if (newValue === true) { + if (feedback && confirmationDialog) { + if (showDialog({ + ...confirmationDialog, + buttons: [ + i18n.t('enable'), + i18n.t('cancel') + ] + }) !== 0) { + // User canceled + return + } + } - if (value === true) { globalShortcut.register(accelerator, action) } else { globalShortcut.unregister(accelerator) @@ -18,7 +33,7 @@ module.exports = function (ctx, { settingsOption, accelerator, action }) { return true } - activate(store.get(settingsOption, false)) + activate({ newValue: store.get(settingsOption, false) }) createToggler(settingsOption, activate) if (!IS_MAC) { diff --git a/src/take-screenshot.js b/src/take-screenshot.js index 079c2ed73..c87ec385a 100644 --- a/src/take-screenshot.js +++ b/src/take-screenshot.js @@ -102,7 +102,11 @@ function takeScreenshot (ctx) { } module.exports = function (ctx) { - setupGlobalShortcut(ctx, { + setupGlobalShortcut({ + confirmationDialog: { + title: i18n.t('enableGlobalTakeScreenshotShortcut.title'), + message: i18n.t('enableGlobalTakeScreenshotShortcut.message', { accelerator: SHORTCUT }) + }, settingsOption: CONFIG_KEY, accelerator: SHORTCUT, action: () => { diff --git a/src/tray.js b/src/tray.js index 438ec919a..93e834155 100644 --- a/src/tray.js +++ b/src/tray.js @@ -11,7 +11,7 @@ const runGarbageCollector = require('./run-gc') const { SHORTCUT: SCREENSHOT_SHORTCUT, CONFIG_KEY: SCREENSHOT_KEY, takeScreenshot } = require('./take-screenshot') const { SHORTCUT: HASH_SHORTCUT, CONFIG_KEY: HASH_KEY, downloadHash } = require('./download-hash') -const { CONFIG_KEY: AUTO_LAUNCH_KEY } = require('./auto-launch') +const { CONFIG_KEY: AUTO_LAUNCH_KEY, isSupported: supportsLaunchAtLogin } = require('./auto-launch') const { CONFIG_KEY: IPFS_PATH_KEY } = require('./ipfs-on-path') const { CONFIG_KEY: NPM_IPFS_KEY } = require('./npm-on-ipfs') @@ -234,6 +234,8 @@ module.exports = function (ctx) { const updateStatus = data => { status = data + menu.getMenuItemById(AUTO_LAUNCH_KEY).enabled = supportsLaunchAtLogin() + menu.getMenuItemById('ipfsIsStarting').visible = status === STATUS.STARTING_STARTED menu.getMenuItemById('ipfsIsRunning').visible = status === STATUS.STARTING_FINISHED menu.getMenuItemById('ipfsIsStopping').visible = status === STATUS.STOPPING_STARTED