From 343f7cdb2cc4a7e4f1db43e473a6327a220d1e82 Mon Sep 17 00:00:00 2001 From: Henrique Dias Date: Fri, 17 Apr 2020 22:14:02 +0100 Subject: [PATCH 1/6] feat(ux): improve download cid License: MIT Signed-off-by: Henrique Dias --- assets/locales/en.json | 30 +++++++-- src/download-cid.js | 144 +++++++++++++++++++++++++++++++++++++++++ src/download-hash.js | 101 ----------------------------- src/index.js | 4 +- src/prompt/index.js | 71 ++++++++++++++++++++ src/prompt/template.js | 95 +++++++++++++++++++++++++++ src/tray.js | 12 ++-- 7 files changed, 341 insertions(+), 116 deletions(-) create mode 100644 src/download-cid.js delete mode 100644 src/download-hash.js create mode 100644 src/prompt/index.js create mode 100644 src/prompt/template.js diff --git a/assets/locales/en.json b/assets/locales/en.json index d44f50270..e803fd089 100644 --- a/assets/locales/en.json +++ b/assets/locales/en.json @@ -26,12 +26,6 @@ "errorwhileTakingScreenshot": "An error occurred while taking the screenshot.", "clickToOpenLogs": "Click here to open the logs.", "ipfsNotRunning": "IPFS is not running", - "cantDownloadHash": "Could not download hash", - "invalidHashClipboard": "The hash on the clipboard is not valid.", - "errorWhileDownloadingHash": "An error occurred while getting the hash.", - "errorWhileWritingFiles": "An error occurred while writing the files to your file system.", - "hashDownloaded": "Hash downloaded", - "hashDownloadedClickToView": "Hash { hash } content downloaded. Click to view.", "checkForUpdates": "Check for Updates...", "yes": "Yes", "no": "No", @@ -42,7 +36,7 @@ "restartIpfsDesktop": "Restart IPFS Desktop", "openLogs": "Open logs", "takeScreenshot": "Take Screenshot", - "downloadHash": "Download Hash", + "downloadCid": "Download...", "moveRepositoryLocation": "Move Repository Location", "runGarbageCollector": "Run Garbage Collector", "selectDirectory": "Select Directory", @@ -155,5 +149,27 @@ "runGarbageCollectorErrored": { "title": "Garbage collector", "message": "The garbage collector run could not be completed successfully." + }, + "downloadCidContentDialog": { + "title": "Download", + "message": "Enter a CID, IPFS path, or IPNS path to download its contents:", + "action": "Download" + }, + "cantResolveCidDialog": { + "title": "Error", + "message": "Unable to resolve \"{ path }\"." + }, + "couldNotGetCidDialog": { + "title": "Error", + "message": "Unable to fetch \"{ path }\"." + }, + "contentsSavedDialog": { + "title": "Success", + "message": "The contents of \"{ path }\" were successfully downloaded.", + "action": "See Files" + }, + "couldNotSaveDialog": { + "title": "Could not write to disk", + "message": "There was an error writing to the disk. Please try again." } } diff --git a/src/download-cid.js b/src/download-cid.js new file mode 100644 index 000000000..22b77c58b --- /dev/null +++ b/src/download-cid.js @@ -0,0 +1,144 @@ +const { join } = require('path') +const fs = require('fs-extra') +const i18n = require('i18next') +const isIPFS = require('is-ipfs') +const { clipboard, app, shell } = require('electron') +const logger = require('./common/logger') +const { IS_MAC } = require('./common/consts') +const setupGlobalShortcut = require('./setup-global-shortcut') +const { selectDirectory } = require('./dialogs') +const dock = require('./dock') +const showPrompt = require('./prompt') +const { showDialog } = require('./dialogs') + +const CONFIG_KEY = 'downloadHashShortcut' + +const SHORTCUT = IS_MAC + ? 'Command+Control+H' + : 'CommandOrControl+Alt+D' + +async function saveFile (dir, file) { + const location = join(dir, file.path) + await fs.outputFile(location, file.content) +} + +async function getCID () { + const text = clipboard.readText().trim() + + const { button, input } = await showPrompt({ + title: i18n.t('downloadCidContentDialog.title'), + message: i18n.t('downloadCidContentDialog.message'), + defaultValue: isIPFS.cid(text) ? text : '', + buttons: [ + i18n.t('downloadCidContentDialog.action'), + i18n.t('cancel') + ], + window: { + width: 460, + height: 120 + } + }) + + if (button !== 0) { + return + } + + return input +} + +async function downloadCid (ctx) { + const cid = await getCID() + if (!cid) { + logger.info('[cid download] user canceled') + return + } + + const { getIpfsd } = ctx + const ipfsd = await getIpfsd() + + if (!ipfsd) { + return + } + + let path + try { + path = await ipfsd.api.resolve(cid) + } catch (_) { + showDialog({ + title: i18n.t('cantResolveCidDialog.title'), + message: i18n.t('cantResolveCidDialog.message', { path: cid }), + buttons: [i18n.t('close')] + }) + + return + } + + const dir = await dock.run(() => selectDirectory({ + defaultPath: app.getPath('downloads') + })) + + if (!dir) { + logger.info(`[cid download] dropping ${path}: user didn't choose a path.`) + return + } + + let files + + try { + logger.info(`[cid download] downloading ${path}: started`, { withAnalytics: 'DOWNLOAD_HASH' }) + files = await ipfsd.api.get(path) + logger.info(`[cid download] downloading ${path}: completed`) + } catch (err) { + logger.error(`[cid download] ${err.stack}`) + + showDialog({ + title: i18n.t('couldNotGetCidDialog.title'), + message: i18n.t('couldNotGetCidDialog.message', { path }), + buttons: [i18n.t('close')] + }) + + return + } + + try { + await Promise.all( + files + .filter(file => !!file.content) + .map(file => saveFile(dir, file)) + ) + + const opt = showDialog({ + title: i18n.t('contentsSavedDialog.title'), + message: i18n.t('contentsSavedDialog.message', { path }), + buttons: [ + i18n.t('contentsSavedDialog.action'), + i18n.t('close') + ] + }) + + if (opt === 0) { + shell.showItemInFolder(join(dir, files[0].path)) + } + } catch (err) { + logger.error(`[cid download] ${err.toString()}`) + + showDialog({ + title: i18n.t('couldNotSaveDialog.title'), + message: i18n.t('couldNotSaveDialog.message'), + buttons: [i18n.t('close')] + }) + } +} + +module.exports = function (ctx) { + setupGlobalShortcut(ctx, { + settingsOption: CONFIG_KEY, + accelerator: SHORTCUT, + action: () => { + downloadCid(ctx) + } + }) +} + +module.exports.downloadCid = downloadCid +module.exports.SHORTCUT = SHORTCUT diff --git a/src/download-hash.js b/src/download-hash.js deleted file mode 100644 index 40c2aa3c5..000000000 --- a/src/download-hash.js +++ /dev/null @@ -1,101 +0,0 @@ -const path = require('path') -const fs = require('fs-extra') -const i18n = require('i18next') -const { clipboard, app, shell } = require('electron') -const logger = require('./common/logger') -const { IS_MAC } = require('./common/consts') -const { notify, notifyError } = require('./common/notify') -const setupGlobalShortcut = require('./setup-global-shortcut') -const { selectDirectory } = require('./dialogs') -const dock = require('./dock') - -const CONFIG_KEY = 'downloadHashShortcut' - -const SHORTCUT = IS_MAC - ? 'Command+Control+H' - : 'CommandOrControl+Alt+D' - -async function saveFile (dir, file) { - const location = path.join(dir, file.path) - await fs.outputFile(location, file.content) -} - -async function downloadHash (ctx) { - const { getIpfsd } = ctx - let text = clipboard.readText().trim() - const ipfsd = await getIpfsd() - - if (!ipfsd || !text) { - return - } - - try { - text = await ipfsd.api.resolve(text) - } catch (_) { - notify({ - title: i18n.t('cantDownloadHash'), - body: i18n.t('invalidHashClipboard') - }) - - return - } - - const dir = await dock.run(() => selectDirectory({ - defaultPath: app.getPath('downloads') - })) - - if (!dir) { - logger.info(`[hash download] dropping hash ${text}: user didn't choose a path.`) - return - } - - let files - - try { - logger.info(`[hash download] downloading ${text}: started`, { withAnalytics: 'DOWNLOAD_HASH' }) - files = await ipfsd.api.get(text) - logger.info(`[hash download] downloading ${text}: completed`) - } catch (err) { - logger.error(`[hash download] ${err.toString()}`) - - notifyError({ - title: i18n.t('cantDownloadHash'), - body: i18n.t('errorWhileDownloadingHash') - }) - } - - try { - await Promise.all( - files - .filter(file => !!file.content) - .map(file => saveFile(dir, file)) - ) - - notify({ - title: i18n.t('hashDownloaded'), - body: i18n.t('hashDownloadedClickToView', { hash: text }) - }, () => { - shell.showItemInFolder(path.join(dir, files[0].path)) - }) - } catch (err) { - logger.error(`[hash download] ${err.toString()}`) - - notifyError({ - title: i18n.t('cantDownloadHash'), - body: i18n.t('errorWhileWritingFiles') - }) - } -} - -module.exports = function (ctx) { - setupGlobalShortcut(ctx, { - settingsOption: CONFIG_KEY, - accelerator: SHORTCUT, - action: () => { - downloadHash(ctx) - } - }) -} - -module.exports.downloadHash = downloadHash -module.exports.SHORTCUT = SHORTCUT diff --git a/src/index.js b/src/index.js index 4ffc68170..372a0a28d 100644 --- a/src/index.js +++ b/src/index.js @@ -9,7 +9,7 @@ const setupNpmOnIpfs = require('./npm-on-ipfs') const setupDaemon = require('./daemon') const setupWebUI = require('./webui') const setupAutoLaunch = require('./auto-launch') -const setupDownloadHash = require('./download-hash') +const setupDownloadCid = require('./download-cid') const setupTakeScreenshot = require('./take-screenshot') const setupAppMenu = require('./app-menu') const setupArgvFilesHandler = require('./argv-files-handler') @@ -76,7 +76,7 @@ async function run () { setupAutoLaunch(ctx), setupSecondInstance(ctx), // Setup global shortcuts - setupDownloadHash(ctx), + setupDownloadCid(ctx), setupTakeScreenshot(ctx), // Setup PATH-related features setupNpmOnIpfs(ctx), diff --git a/src/prompt/index.js b/src/prompt/index.js new file mode 100644 index 000000000..8e309b5f1 --- /dev/null +++ b/src/prompt/index.js @@ -0,0 +1,71 @@ +const { BrowserWindow, ipcMain, nativeTheme } = require('electron') +const crypto = require('crypto') +const { IS_MAC } = require('../common/consts') +const makePage = require('./template') + +const pallette = { + default: { + background: '#ECECEC', + color: '#262626', + inputBackground: '#ffffff', + defaultBackground: '#007AFF' + }, + dark: { + background: '#323232', + color: '#ffffff', + inputBackground: '#656565', + defaultBackground: '#0A84FF' + } +} + +function generatePage ({ message, defaultValue = '', buttons }, id) { + buttons = buttons.map((txt, i) => ``) + + if (IS_MAC) { + buttons.reverse() + } + + const page = makePage({ pallette, message, defaultValue, buttons, id }) + return `data:text/html;base64,${Buffer.from(page, 'utf8').toString('base64')}` +} + +module.exports = async function showPrompt (options = {}) { + options.window = options.window || {} + + const window = new BrowserWindow({ + title: options.title, + show: false, + width: 350, + height: 330, + resizable: false, + autoHideMenuBar: true, + fullscreenable: false, + backgroundColor: nativeTheme.shouldUseDarkColors + ? pallette.dark.background + : pallette.default.background, + webPreferences: { + nodeIntegration: true + }, + ...options.window + }) + + // Generate random id + const id = crypto.randomBytes(16).toString('hex') + + return new Promise(resolve => { + ipcMain.once(id, (_, data) => { + window.destroy() + resolve(data) + }) + + window.on('close', () => { + resolve({ input: '', button: null }) + }) + + window.once('ready-to-show', () => { + window.show() + }) + + window.loadURL(generatePage(options, id)) + }) +} diff --git a/src/prompt/template.js b/src/prompt/template.js new file mode 100644 index 000000000..e29cdfcab --- /dev/null +++ b/src/prompt/template.js @@ -0,0 +1,95 @@ +module.exports = ({ pallette, message, defaultValue, buttons, id }) => (` + + +

${message}

+ +
${buttons.join('\n')}
+ + + +`) diff --git a/src/tray.js b/src/tray.js index 90a17c33b..0b9aa0e34 100644 --- a/src/tray.js +++ b/src/tray.js @@ -2,7 +2,7 @@ const { Menu, Tray, shell, app, ipcMain } = require('electron') const i18n = require('i18next') const path = require('path') const { SHORTCUT: SCREENSHOT_SHORTCUT, takeScreenshot } = require('./take-screenshot') -const { SHORTCUT: HASH_SHORTCUT, downloadHash } = require('./download-hash') +const { SHORTCUT: DOWNLOAD_SHORTCUT, downloadCid } = require('./download-cid') const addToIpfs = require('./add-to-ipfs') const { STATUS } = require('./daemon') const logger = require('./common/logger') @@ -72,10 +72,10 @@ function buildMenu (ctx) { enabled: false }, { - id: 'downloadHash', - label: i18n.t('downloadHash'), - click: () => { downloadHash(ctx) }, - accelerator: IS_MAC ? HASH_SHORTCUT : null, + id: 'downloadCid', + label: i18n.t('downloadCid'), + click: () => { downloadCid(ctx) }, + accelerator: IS_MAC ? DOWNLOAD_SHORTCUT : null, enabled: false }, { type: 'separator' }, @@ -212,7 +212,7 @@ module.exports = function (ctx) { menu.getMenuItemById('restartIpfs').enabled = !gcRunning menu.getMenuItemById('takeScreenshot').enabled = status === STATUS.STARTING_FINISHED - menu.getMenuItemById('downloadHash').enabled = status === STATUS.STARTING_FINISHED + menu.getMenuItemById('downloadCid').enabled = status === STATUS.STARTING_FINISHED menu.getMenuItemById('moveRepositoryLocation').enabled = !gcRunning && status !== STATUS.STOPPING_STARTED menu.getMenuItemById('runGarbageCollector').enabled = menu.getMenuItemById('ipfsIsRunning').visible && !gcRunning From 53c52d632d71ec4659b9cb9a78895df79883601c Mon Sep 17 00:00:00 2001 From: Henrique Dias Date: Tue, 21 Apr 2020 14:39:30 +0100 Subject: [PATCH 2/6] update copy License: MIT Signed-off-by: Henrique Dias --- assets/locales/en.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/locales/en.json b/assets/locales/en.json index e803fd089..4f161e667 100644 --- a/assets/locales/en.json +++ b/assets/locales/en.json @@ -152,8 +152,8 @@ }, "downloadCidContentDialog": { "title": "Download", - "message": "Enter a CID, IPFS path, or IPNS path to download its contents:", - "action": "Download" + "message": "Enter a CID, IPFS path, or IPNS path:", + "action": "Next" }, "cantResolveCidDialog": { "title": "Error", From 52c14a4631ae7ecaa0af8c03aa613766d78b0c40 Mon Sep 17 00:00:00 2001 From: Henrique Dias Date: Wed, 22 Apr 2020 07:18:37 +0100 Subject: [PATCH 3/6] Update assets/locales/en.json Co-Authored-By: Marcin Rataj --- assets/locales/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/locales/en.json b/assets/locales/en.json index 4f161e667..dfeb2754e 100644 --- a/assets/locales/en.json +++ b/assets/locales/en.json @@ -151,7 +151,7 @@ "message": "The garbage collector run could not be completed successfully." }, "downloadCidContentDialog": { - "title": "Download", + "title": "Download to a local directory", "message": "Enter a CID, IPFS path, or IPNS path:", "action": "Next" }, From c130b8655b406c2fb9a7599385cf484b20ab6777 Mon Sep 17 00:00:00 2001 From: Henrique Dias Date: Wed, 22 Apr 2020 07:19:08 +0100 Subject: [PATCH 4/6] Update src/download-cid.js Co-Authored-By: Marcin Rataj --- src/download-cid.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/download-cid.js b/src/download-cid.js index 22b77c58b..85883f68b 100644 --- a/src/download-cid.js +++ b/src/download-cid.js @@ -28,7 +28,7 @@ async function getCID () { const { button, input } = await showPrompt({ title: i18n.t('downloadCidContentDialog.title'), message: i18n.t('downloadCidContentDialog.message'), - defaultValue: isIPFS.cid(text) ? text : '', + defaultValue: (isIPFS.cid(text) || isIPFS.path(text)) ? text : '', buttons: [ i18n.t('downloadCidContentDialog.action'), i18n.t('cancel') From a9c344ea1e06397d0667837549786f542e3c3c25 Mon Sep 17 00:00:00 2001 From: Henrique Dias Date: Wed, 22 Apr 2020 07:21:32 +0100 Subject: [PATCH 5/6] fix: make window bigger License: MIT Signed-off-by: Henrique Dias --- src/download-cid.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/download-cid.js b/src/download-cid.js index 85883f68b..4bc2f05c3 100644 --- a/src/download-cid.js +++ b/src/download-cid.js @@ -34,7 +34,7 @@ async function getCID () { i18n.t('cancel') ], window: { - width: 460, + width: 500, height: 120 } }) From bd9269f9bf021612830fac01a60a965d3b1e0ca1 Mon Sep 17 00:00:00 2001 From: Henrique Dias Date: Wed, 22 Apr 2020 07:23:36 +0100 Subject: [PATCH 6/6] feat: use event.code License: MIT Signed-off-by: Henrique Dias --- src/prompt/template.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/prompt/template.js b/src/prompt/template.js index e29cdfcab..00a78b920 100644 --- a/src/prompt/template.js +++ b/src/prompt/template.js @@ -86,7 +86,7 @@ module.exports = ({ pallette, message, defaultValue, buttons, id }) => (` { - if (event.keyCode == 13) { + if (event.code === 'Enter') { event.preventDefault() document.querySelector('button.default').click() }