Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ux): improve download cid #1419

Merged
merged 6 commits into from
Apr 22, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 23 additions & 7 deletions assets/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -155,5 +149,27 @@
"runGarbageCollectorErrored": {
"title": "Garbage collector",
"message": "The garbage collector run could not be completed successfully."
},
"downloadCidContentDialog": {
"title": "Download to a local directory",
"message": "Enter a CID, IPFS path, or IPNS path:",
"action": "Next"
},
"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."
}
}
144 changes: 144 additions & 0 deletions src/download-cid.js
Original file line number Diff line number Diff line change
@@ -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) || isIPFS.path(text)) ? text : '',
buttons: [
i18n.t('downloadCidContentDialog.action'),
i18n.t('cancel')
],
window: {
width: 500,
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
101 changes: 0 additions & 101 deletions src/download-hash.js

This file was deleted.

4 changes: 2 additions & 2 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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),
Expand Down
71 changes: 71 additions & 0 deletions src/prompt/index.js
Original file line number Diff line number Diff line change
@@ -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) => `<button ${i === 0 ? 'class="default"' : ''} id="${i}">${txt}</button>`)

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))
})
}
Loading