diff --git a/assets/locales/en.json b/assets/locales/en.json index d6d27b3fb..0d892d513 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,6 +151,32 @@ "title": "IPFS on PATH", "message": "Could not add IPFS to the PATH." }, + "launchAtLoginNotSupported": { + "title": "Launch at login", + "message": "Launch at login is not supported on your platform." + }, + "launchAtLoginFailed": { + "title": "Launch at login", + "message": "Launch at login could not be enabled on your machine." + }, + "ipfsOnPathInstall": { + "title": "Install IPFS on PATH", + "message": "By enabling this option, IPFS will be available on your command line as \"ipfs\". This action is reversible.", + "action": "Install" + }, + "ipfsOnPathUninstall": { + "title": "Uninstall IPFS on PATH", + "message": "By disabling this option, IPFS will no longer be available on your command line as \"ipfs\".", + "action": "Uninstall" + }, + "enableGlobalTakeScreenshotShortcut": { + "title": "Take screenshot shortcut", + "message": "By enabling this, the shortcut { accelerator } will be available to take screenshots as long as IPFS Desktop is running." + }, + "enableGlobalDownloadShortcut": { + "title": "Download shortcut", + "message": "By enabling this, the shortcut { accelerator } will be available to download files as long as IPFS Desktop is running." + }, "settings": { "settings": "Settings", "preferences": "Preferences", 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/create-toggler.js b/src/create-toggler.js index 98a974bb2..0ba178b72 100644 --- a/src/create-toggler.js +++ b/src/create-toggler.js @@ -10,7 +10,7 @@ module.exports = function (settingsOption, activate) { // TODO: refactor: tell the user if didn't work or not available. // Receive prompt() to ask user if they're sure they want to enable for some. - if (await activate(newValue, oldValue)) { + if (await activate({ newValue, oldValue, feedback: true })) { store.set(settingsOption, newValue) const action = newValue ? 'enabled' : 'disabled' 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..3fc0ba3bf 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('ipfsOnPathInstall.title'), + message: i18n.t('ipfsOnPathInstall.message'), + buttons: [ + i18n.t('ipfsOnPathInstall.action'), + i18n.t('cancel') + ] + }) !== 0) { + // User canceled + return + } + + return run('install') + } + + if (showDialog({ + title: i18n.t('ipfsOnPathUninstall.title'), + message: i18n.t('ipfsOnPathUninstall.message'), + buttons: [ + i18n.t('ipfsOnPathUninstall.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..81e801be2 100644 --- a/src/npm-on-ipfs/index.js +++ b/src/npm-on-ipfs/index.js @@ -9,14 +9,14 @@ const CONFIG_KEY = 'experiments.npmOnIpfs' module.exports = function (ctx) { let interval = null - createToggler(CONFIG_KEY, async (value, oldValue) => { - if (value === oldValue || oldValue === null) return true + createToggler(CONFIG_KEY, async ({ 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() === value + const manual = isPkgInstalled() === newValue - if (value === true) { + if (newValue === true) { if (!manual && !await pkg.install()) return false interval = setInterval(existsAndUpdate, 43200000) // every 12 hours return true diff --git a/src/setup-global-shortcut.js b/src/setup-global-shortcut.js index bf3c3f7da..5b68eecbf 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 }) => { + if (newValue === oldValue) return + + if (newValue === true) { + if (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 898869be6..80f0ca6c2 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') @@ -232,6 +232,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