diff --git a/package.json b/package.json index 979456bc8..5200f09f3 100644 --- a/package.json +++ b/package.json @@ -11,13 +11,13 @@ "electron-compile": "^6.4.2", "electron-menubar": "^1.0.1", "electron-squirrel-startup": "^1.0.0", + "file-extension": "^4.0.1", "go-ipfs-dep": "^0.4.13", + "ipfs-stats": "^1.1.4", "ipfsd-ctl": "^0.27.0", - "file-extension": "^4.0.1", - "ipfs-stats": "^1.0.4", + "is-ipfs": "^0.3.2", "moment": "^2.20.1", "multiaddr": "^3.0.2", - "is-ipfs": "^0.3.2", "normalize.css": "^7.0.0", "pretty-bytes": "^4.0.2", "prop-types": "^15.6.0", diff --git a/src/components/Block.js b/src/components/Block.js new file mode 100644 index 000000000..3f7495957 --- /dev/null +++ b/src/components/Block.js @@ -0,0 +1,45 @@ +import React from 'react' +import PropTypes from 'prop-types' + +/** + * It's a Block. + * + * @param {Object} props + * + * @prop {Any} wrapped + * @prop {Any} unwrapped + * @prop {Function} [onClick] + * + * @return {ReactElement} + */ +export default function Block (props) { + let className = 'block' + if (props.className !== '') { + className += ' ' + props.className + } + + if (props.onClick !== null) { + className += ' clickable' + } + + return ( +
+
+ {props.wrapped} +
+ {props.unwrapped && props.unwrapped} +
+ ) +} + +Block.propTypes = { + wrapped: PropTypes.any.isRequired, + unwrapped: PropTypes.any, + className: PropTypes.string, + onClick: PropTypes.func +} + +Block.defaultProps = { + className: '', + onClick: null +} diff --git a/src/components/Breadcrumbs.js b/src/components/Breadcrumbs.js new file mode 100644 index 000000000..8bec18212 --- /dev/null +++ b/src/components/Breadcrumbs.js @@ -0,0 +1,50 @@ +import React from 'react' +import PropTypes from 'prop-types' + +function makeBread (root) { + root = root || '/' + if (root.endsWith('/')) { + root = root.substring(0, root.length - 2) + } + + let parts = root.split('/').map(part => { + return { + name: part, + path: part + } + }) + + for (let i = 1; i < parts.length; i++) { + parts[i] = { + name: parts[i].name, + path: parts[i - 1].path + '/' + parts[i].path + } + } + + parts[0] = { + name: 'ipfs', + path: '/' + } + + return parts +} + +export default function Breadcrumbs ({path, navigate}) { + const bread = makeBread(path) + const res = [] + + bread.forEach((link, index) => { + res.push( { navigate(link.path) }}>{link.name}) + res.push(/) + }) + + res.pop() + return ( + {res} + ) +} + +Breadcrumbs.propTypes = { + path: PropTypes.string.isRequired, + navigate: PropTypes.func.isRequired +} diff --git a/src/js/components/view/button.js b/src/components/Button.js similarity index 74% rename from src/js/components/view/button.js rename to src/components/Button.js index 522fdfa22..9492fd198 100644 --- a/src/js/components/view/button.js +++ b/src/components/Button.js @@ -1,5 +1,6 @@ import React from 'react' import PropTypes from 'prop-types' +import Input from './Input' /** * Is a Button. @@ -13,9 +14,11 @@ import PropTypes from 'prop-types' */ export default function Button (props) { return ( - + + + ) } diff --git a/src/js/components/view/checkbox-block.js b/src/components/CheckboxBlock.js similarity index 61% rename from src/js/components/view/checkbox-block.js rename to src/components/CheckboxBlock.js index 2eba011bf..86ef3c2d3 100644 --- a/src/js/components/view/checkbox-block.js +++ b/src/components/CheckboxBlock.js @@ -1,6 +1,9 @@ + import React from 'react' import PropTypes from 'prop-types' +import Block from './Block' + /** * Is a Checkbox Block. * @@ -19,18 +22,21 @@ export default function CheckboxBlock (props) { } return ( -
-
+ -

{props.title}

-

{props.info}

-
-
- - +
+

{props.title}

+

{props.info}

+
+
+ + +
-
- + )} /> ) } diff --git a/src/components/FileBlock.js b/src/components/FileBlock.js new file mode 100644 index 000000000..699463a6b --- /dev/null +++ b/src/components/FileBlock.js @@ -0,0 +1,124 @@ +import React from 'react' +import PropTypes from 'prop-types' +import fileExtension from 'file-extension' +import prettyBytes from 'pretty-bytes' + +import Block from './Block' +import Icon from './Icon' +import IconButton from './IconButton' + +const wrapper = (fn) => { + return (event) => { + event.preventDefault() + event.stopPropagation() + fn() + } +} + +/** + * Is a File Block. + * + * @param {Object} props + * + * @prop {String} name - file name + * @prop {String} date - date when the file was modified/uploaded + * @prop {String} hash - file's hash in IPFS system + * + * @return {ReactElement} + */ +export default function FileBlock (props) { + const extension = fileExtension(props.name) + let icon = 'file' + + if (props.type === 'directory') { + icon = 'folder' + } else if (fileTypes[extension]) { + icon = fileTypes[extension] + } + + const open = () => { + if (props.type === 'directory') { + props.navigate(props.name, props.hash) + } else { + props.open(props.name, props.hash) + } + } + + const copy = wrapper(() => { props.copy(props.hash) }) + const remove = wrapper(() => { props.remove(props.name) }) + + const wrapped = ( +
+
+ +
+
+

{props.name}

+

{prettyBytes(props.size)} | {props.hash}

+
+
+ ) + + const unwrapped = ( +
+ { typeof props.copy === 'function' && + + } + { typeof props.remove === 'function' && + + } +
+ ) + + return ( + + ) +} + +FileBlock.propTypes = { + name: PropTypes.string.isRequired, + hash: PropTypes.string.isRequired, + size: PropTypes.number.isRequired, + navigate: PropTypes.func, + copy: PropTypes.func, + remove: PropTypes.func, + type: PropTypes.string, + open: PropTypes.func +} + +FileBlock.defaultProps = { + type: 'file' +} + +const fileTypes = { + png: 'image', + jpg: 'image', + tif: 'image', + tiff: 'image', + bmp: 'image', + gif: 'image', + eps: 'image', + raw: 'image', + cr2: 'image', + nef: 'image', + orf: 'image', + sr2: 'image', + jpeg: 'image', + mp3: 'music-alt', + flac: 'music-alt', + ogg: 'music-alt', + oga: 'music-alt', + aa: 'music-alt', + aac: 'music-alt', + m4p: 'music-alt', + webm: 'music-alt', + mp4: 'video-clapper', + mkv: 'video-clapper', + avi: 'video-clapper', + asf: 'video-clapper', + flv: 'video-clapper' +} diff --git a/src/js/components/view/footer.js b/src/components/Footer.js similarity index 100% rename from src/js/components/view/footer.js rename to src/components/Footer.js diff --git a/src/js/components/view/header.js b/src/components/Header.js similarity index 66% rename from src/js/components/view/header.js rename to src/components/Header.js index f4d7e39f2..7a5348bc8 100644 --- a/src/js/components/view/header.js +++ b/src/components/Header.js @@ -6,8 +6,8 @@ import PropTypes from 'prop-types' * * @param {Object} props * - * @prop {String} title - The title of the pane - * @prop {String} [subtitle] - Subtitle of the pane + * @prop {String|Node} title - The title of the pane + * @prop {String|Node} [subtitle] - Subtitle of the pane * @prop {Node} [children] - Header children (e.g.: buttons) * @prop {Bool} [loading] - Show a loading animation * @@ -35,10 +35,18 @@ export default function Header (props) { } Header.propTypes = { - title: PropTypes.string.isRequired, + title: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.node, + PropTypes.arrayOf(PropTypes.node) + ]).isRequired, + subtitle: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.node, + PropTypes.arrayOf(PropTypes.node) + ]).isRequired, children: PropTypes.node, - loading: PropTypes.bool, - subtitle: PropTypes.string + loading: PropTypes.bool } Header.defaultProps = { diff --git a/src/js/components/view/heartbeat.js b/src/components/Heartbeat.js similarity index 87% rename from src/js/components/view/heartbeat.js rename to src/components/Heartbeat.js index b0e9f8ab5..d7eb65623 100644 --- a/src/js/components/view/heartbeat.js +++ b/src/components/Heartbeat.js @@ -2,8 +2,8 @@ import React from 'react' import {resolve, join} from 'path' import PropTypes from 'prop-types' -const icyLogo = resolve(join(__dirname, '../../../img/ipfs-logo-ice.png')) -const blackLogo = resolve(join(__dirname, '../../../img/ipfs-logo-black.png')) +const icyLogo = resolve(join(__dirname, '../img/ipfs-logo-ice.png')) +const blackLogo = resolve(join(__dirname, '../img/ipfs-logo-black.png')) /** * Is an Hearbeat. diff --git a/src/js/components/view/icon.js b/src/components/Icon.js similarity index 71% rename from src/js/components/view/icon.js rename to src/components/Icon.js index 5caac9967..2b8bdcb15 100644 --- a/src/js/components/view/icon.js +++ b/src/components/Icon.js @@ -1,5 +1,8 @@ import React from 'react' import PropTypes from 'prop-types' +import {resolve, join} from 'path' + +const logoBlack = resolve(join(__dirname, '../img/ipfs-logo-black.png')) /** * Is an Icon. @@ -14,7 +17,7 @@ export default function Icon (props) { if (props.name === 'ipfs') { return ( - + IPFS Logo ) } diff --git a/src/js/components/view/icon-button.js b/src/components/IconButton.js similarity index 96% rename from src/js/components/view/icon-button.js rename to src/components/IconButton.js index 1fd3ee654..06393c8f3 100644 --- a/src/js/components/view/icon-button.js +++ b/src/components/IconButton.js @@ -1,7 +1,6 @@ import React from 'react' import PropTypes from 'prop-types' - -import Icon from './icon' +import Icon from './Icon' /** * Is a Button with an Icon. diff --git a/src/js/components/view/icon-dropdown-list.js b/src/components/IconDropdownList.js similarity index 97% rename from src/js/components/view/icon-dropdown-list.js rename to src/components/IconDropdownList.js index ee529a923..106f57024 100644 --- a/src/js/components/view/icon-dropdown-list.js +++ b/src/components/IconDropdownList.js @@ -1,7 +1,7 @@ import React from 'react' import PropTypes from 'prop-types' -import Icon from './icon' +import Icon from './Icon' function onChangeWrapper (fn) { return event => { diff --git a/src/js/components/view/info-block.js b/src/components/InfoBlock.js similarity index 88% rename from src/js/components/view/info-block.js rename to src/components/InfoBlock.js index 14cfaad62..a5ed03366 100644 --- a/src/js/components/view/info-block.js +++ b/src/components/InfoBlock.js @@ -1,7 +1,7 @@ import React from 'react' import PropTypes from 'prop-types' - -import Button from '../view/button' +import Button from './Button' +import Block from './Block' /** * Is an information block. @@ -42,15 +42,15 @@ export default function InfoBlock (props) { let clickable = props.onClick && !props.button return ( -
-
+

{props.title}

{info}
-
- {button} - + )} /> ) } diff --git a/src/components/Input.js b/src/components/Input.js new file mode 100644 index 000000000..5c1384d97 --- /dev/null +++ b/src/components/Input.js @@ -0,0 +1,27 @@ +import React from 'react' +import PropTypes from 'prop-types' + +/** + * Is an Input. + * + * @param {Object} props + * + * @prop {Any} children + * + * @return {ReactElement} + */ +export default function Input (props) { + let className = 'input' + if (props.class) { + className += ' ' + props.class + } + + return ( +
{props.children}
+ ) +} + +Input.propTypes = { + class: PropTypes.string, + children: PropTypes.any.isRequired +} diff --git a/src/components/InputText.js b/src/components/InputText.js new file mode 100644 index 000000000..b58fc03d1 --- /dev/null +++ b/src/components/InputText.js @@ -0,0 +1,27 @@ +import React from 'react' +import PropTypes from 'prop-types' + +import Input from './Input' + +export default function InputText (props) { + const onChange = (event) => { + event.preventDefault() + props.onChange(event.target.value) + } + + return ( + + + + ) +} + +InputText.propTypes = { + value: PropTypes.string.isRequired, + onChange: PropTypes.func.isRequired, + placeholder: PropTypes.string +} diff --git a/src/js/components/view/key.js b/src/components/Key.js similarity index 100% rename from src/js/components/view/key.js rename to src/components/Key.js diff --git a/src/js/components/view/key-combo.js b/src/components/KeyCombo.js similarity index 95% rename from src/js/components/view/key-combo.js rename to src/components/KeyCombo.js index 202310059..a3422e33e 100644 --- a/src/js/components/view/key-combo.js +++ b/src/components/KeyCombo.js @@ -1,7 +1,7 @@ import React from 'react' import PropTypes from 'prop-types' -import Key from './key' +import Key from './Key' /** * Is a Key Combination diff --git a/src/components/Menu.js b/src/components/Menu.js new file mode 100644 index 000000000..37026e77b --- /dev/null +++ b/src/components/Menu.js @@ -0,0 +1,17 @@ +import React from 'react' +import PropTypes from 'prop-types' + +export default function Menu (props) { + return ( +
+ {props.children} +
+ ) +} + +Menu.propTypes = { + children: PropTypes.oneOfType([ + PropTypes.node, + PropTypes.arrayOf(PropTypes.node) + ]).isRequired +} diff --git a/src/js/components/view/menu-option.js b/src/components/MenuOption.js similarity index 96% rename from src/js/components/view/menu-option.js rename to src/components/MenuOption.js index 91a0a8739..b441339cb 100644 --- a/src/js/components/view/menu-option.js +++ b/src/components/MenuOption.js @@ -1,7 +1,7 @@ import React from 'react' import PropTypes from 'prop-types' -import Icon from './icon' +import Icon from './Icon' /** * Is a menu option. diff --git a/src/js/components/logic/new-pinned-hash.js b/src/components/NewPinnedHash.js similarity index 96% rename from src/js/components/logic/new-pinned-hash.js rename to src/components/NewPinnedHash.js index 6828ad47f..d9dee7391 100644 --- a/src/js/components/logic/new-pinned-hash.js +++ b/src/components/NewPinnedHash.js @@ -1,7 +1,7 @@ import React, {Component} from 'react' import PropTypes from 'prop-types' -import IconButton from '../view/icon-button' +import IconButton from './IconButton' /** * Is a New Pinned Hash form. @@ -91,7 +91,7 @@ export default class NewPinnedHash extends Component { * @returns {ReactElement} */ render () { - let className = 'info-block new-pinned' + let className = 'block new-pinned' if (this.props.hidden) { className += ' hide' } diff --git a/src/js/components/view/pane.js b/src/components/Pane.js similarity index 77% rename from src/js/components/view/pane.js rename to src/components/Pane.js index be89940bc..a2337cf17 100644 --- a/src/js/components/view/pane.js +++ b/src/components/Pane.js @@ -1,5 +1,6 @@ import React from 'react' import PropTypes from 'prop-types' +import Footer from './Footer' /** * Is a Pane. @@ -17,6 +18,12 @@ export default function Pane (props) { className += ' ' + props.class } + React.Children.forEach(props.children, (child) => { + if (child.type === Footer) { + className += ' has-footer' + } + }) + return (
{props.children} diff --git a/src/js/components/view/pane-container.js b/src/components/PaneContainer.js similarity index 95% rename from src/js/components/view/pane-container.js rename to src/components/PaneContainer.js index 3b673a516..66d0027d2 100644 --- a/src/js/components/view/pane-container.js +++ b/src/components/PaneContainer.js @@ -2,7 +2,7 @@ import React from 'react' import PropTypes from 'prop-types' /** - * Is a Pane. + * Is a Pane Container. * * @param {Object} props * diff --git a/src/components/PeerBlock.js b/src/components/PeerBlock.js new file mode 100644 index 000000000..7bf7e3098 --- /dev/null +++ b/src/components/PeerBlock.js @@ -0,0 +1,17 @@ +import React from 'react' +import PropTypes from 'prop-types' +import InfoBlock from './InfoBlock' + +export default function PeerBlock (props) { + return ( + + ) +} + +PeerBlock.propTypes = { + id: PropTypes.string.isRequired, + location: PropTypes.string.isRequired +} diff --git a/src/js/components/view/pinned-hash.js b/src/components/PinnedHash.js similarity index 94% rename from src/js/components/view/pinned-hash.js rename to src/components/PinnedHash.js index 9fc8a6cd0..61fb26d0f 100644 --- a/src/js/components/view/pinned-hash.js +++ b/src/components/PinnedHash.js @@ -2,7 +2,7 @@ import React from 'react' import PropTypes from 'prop-types' import {ipcRenderer} from 'electron' -import Button from './button' +import Button from './Button' /** * Is a Pinned Hash. @@ -17,7 +17,7 @@ import Button from './button' */ export default function PinnedHash (props) { return ( -
+
{ - send('files', fileHistory.toArray()) - } - - ipcMain.on('request-files', handler) - fileHistory.on('change', handler) -} diff --git a/src/controls/main/files.js b/src/controls/main/files.js new file mode 100644 index 000000000..120e2a2a5 --- /dev/null +++ b/src/controls/main/files.js @@ -0,0 +1,83 @@ +import {ipcMain} from 'electron' +import uploadFiles from '../utils/upload-files' + +function basename (path) { + const parts = path.split('/') + parts.pop() + return parts.join('/') || '/' +} + +function sort (a, b) { + if (a.Type === 'directory' && b.Type !== 'directory') { + return -1 + } else if (b.Type === 'directory' && a.Type !== 'directory') { + return 1 + } + + return a.Name > b.Name +} + +function listAndSend (opts, root) { + const {debug, ipfs, send} = opts + + ipfs().files.ls(root) + .then(res => { + const files = res.Entries || [] + + Promise.all(files.map(file => { + return ipfs().files.stat([root, file.Name].join('/')) + .then(stats => Object.assign({}, file, stats)) + })) + .then(res => res.sort(sort)) + .then(res => send('files', root, res)) + .catch(e => { debug(e.stack) }) + }) +} + +function list (opts) { + return (event, root) => { + listAndSend(opts, root) + } +} + +function createDirectory (opts) { + const {ipfs, debug} = opts + + return (event, path) => { + ipfs().files.mkdir(path, {parents: true}) + .then(() => { listAndSend(opts, basename(path)) }) + .catch(e => { debug(e.stack) }) + } +} + +function remove (opts) { + const {ipfs, debug} = opts + + return (event, path) => { + ipfs().files.rm(path, {recursive: true}) + .then(() => { listAndSend(opts, basename(path)) }) + .catch(e => { debug(e.stack) }) + } +} + +function move (opts) { + const {ipfs, debug} = opts + + return (event, from, to) => { + ipfs().files.mv([from, to]) + .then(() => { listAndSend(opts, basename(to)) }) + .catch(e => { debug(e.stack) }) + } +} + +export default function (opts) { + const {menubar} = opts + + ipcMain.on('request-files', list(opts)) + ipcMain.on('create-directory', createDirectory(opts)) + ipcMain.on('remove-file', remove(opts)) + ipcMain.on('move-file', move(opts)) + + ipcMain.on('drop-files', uploadFiles(opts)) + menubar.tray.on('drop-files', uploadFiles(opts)) +} diff --git a/src/controls/main/index.js b/src/controls/main/index.js index 5da512672..a3870a42b 100644 --- a/src/controls/main/index.js +++ b/src/controls/main/index.js @@ -1,6 +1,6 @@ import autoLaunch from './auto-launch' import downloadHash from './download-hash' -import fileHistory from './file-history' +import files from './files' import menuShortcuts from './menu-shortcuts' import openFileDialog from './open-file-dialog' import openUrl from './open-url' @@ -9,12 +9,11 @@ import pinnedFiles from './pinned-files' import settings from './settings' import takeScreenshot from './take-screenshot' import toggleSticky from './toggle-sticky' -import uploadFiles from './upload-files' export default function (opts) { autoLaunch(opts) downloadHash(opts) - fileHistory(opts) + files(opts) menuShortcuts(opts) openFileDialog(opts) openUrl(opts) @@ -23,5 +22,4 @@ export default function (opts) { settings(opts) takeScreenshot(opts) toggleSticky(opts) - uploadFiles(opts) } diff --git a/src/controls/main/open-file-dialog.js b/src/controls/main/open-file-dialog.js index a96f2c37f..b561b129c 100644 --- a/src/controls/main/open-file-dialog.js +++ b/src/controls/main/open-file-dialog.js @@ -1,15 +1,15 @@ import {dialog, ipcMain} from 'electron' -import {uploadFiles} from '../utils' +import uploadFiles from '../utils/upload-files' function openFileDialog (opts, dir = false) { let window = opts.window - return (event) => { + return (event, root) => { dialog.showOpenDialog(window, { properties: [dir ? 'openDirectory' : 'openFile', 'multiSelections'] }, (files) => { if (!files || files.length === 0) return - uploadFiles(opts)(event, files) + uploadFiles(opts)(event, files, root) }) } } diff --git a/src/controls/main/take-screenshot.js b/src/controls/main/take-screenshot.js index f50ec0498..5a7d95db9 100644 --- a/src/controls/main/take-screenshot.js +++ b/src/controls/main/take-screenshot.js @@ -3,8 +3,22 @@ import {clipboard, ipcMain, globalShortcut} from 'electron' const settingsOption = 'screenshotShortcut' const shortcut = 'CommandOrControl+Alt+S' +function makeScreenshotDir (opts) { + const {ipfs} = opts + + return new Promise((resolve, reject) => { + ipfs().files.stat('/screenshots') + .then(resolve) + .catch(() => { + ipfs().files.mkdir('/screenshots') + .then(resolve) + .catch(reject) + }) + }) +} + function handleScreenshot (opts) { - let {debug, fileHistory, ipfs} = opts + let {debug, ipfs, send} = opts return (event, image) => { let base64Data = image.replace(/^data:image\/png;base64,/, '') @@ -16,18 +30,17 @@ function handleScreenshot (opts) { return } - ipfs() - .add([{ - path: `Screenshot ${new Date().toLocaleString()}.png`, - content: Buffer.from(base64Data, 'base64') - }]) + const path = `/screenshots/${new Date().toISOString()}.png` + const content = Buffer.from(base64Data, 'base64') + + makeScreenshotDir(opts) + .then(() => ipfs().files.write(path, content, {create: true})) + .then(() => ipfs().files.stat(path)) .then((res) => { - res.forEach((file) => { - const url = `https://ipfs.io/ipfs/${file.hash}` - clipboard.writeText(url) - debug('Screenshot uploaded', {path: file.path}) - fileHistory.add(file.path, file.hash) - }) + const url = `https://ipfs.io/ipfs/${res.Hash}` + clipboard.writeText(url) + send('files-updated') + debug('Screenshot uploaded', {path: path}) }) .catch(e => { debug(e.stack) }) } diff --git a/src/controls/main/upload-files.js b/src/controls/main/upload-files.js deleted file mode 100644 index 94b1596d5..000000000 --- a/src/controls/main/upload-files.js +++ /dev/null @@ -1,7 +0,0 @@ -import {ipcMain} from 'electron' -import {uploadFiles} from '../utils' - -export default function (opts) { - ipcMain.on('drop-files', uploadFiles(opts)) - opts.menubar.tray.on('drop-files', uploadFiles(opts)) -} diff --git a/src/controls/utils.js b/src/controls/utils.js deleted file mode 100644 index 5b4ff538b..000000000 --- a/src/controls/utils.js +++ /dev/null @@ -1,48 +0,0 @@ -import multiaddr from 'multiaddr' -import {clipboard} from 'electron' -import isIPFS from 'is-ipfs' - -export function apiAddrToUrl (apiAddr) { - const parts = multiaddr(apiAddr).nodeAddress() - const address = parts.address === '127.0.0.1' ? 'localhost' : parts.address - - return `http://${address}:${parts.port}/webui` -} - -export function uploadFiles (opts) { - let {ipfs, debug, fileHistory, send} = opts - let adding = 0 - - const sendAdding = () => { send('adding', adding > 0) } - const inc = () => { adding++; sendAdding() } - const dec = () => { adding--; sendAdding() } - - return (event, files) => { - debug('Uploading files', {files}) - inc() - - ipfs() - .add(files, {recursive: true, wrap: true}) - .then((res) => { - dec() - - res.forEach((file) => { - const url = `https://ipfs.io/ipfs/${file.hash}` - clipboard.writeText(url) - debug('Uploaded file', {path: file.path}) - fileHistory.add(file.path, file.hash) - }) - }) - .catch(e => { - dec() - debug(e.stack) - }) - } -} - -export function validateIPFS (text) { - return isIPFS.multihash(text) || - isIPFS.cid(text) || - isIPFS.ipfsPath(text) || - isIPFS.ipfsPath(`/ipfs/${text}`) -} diff --git a/src/controls/utils/index.js b/src/controls/utils/index.js new file mode 100644 index 000000000..f4d69fd0f --- /dev/null +++ b/src/controls/utils/index.js @@ -0,0 +1,16 @@ +import multiaddr from 'multiaddr' +import isIPFS from 'is-ipfs' + +export function apiAddrToUrl (apiAddr) { + const parts = multiaddr(apiAddr).nodeAddress() + const address = parts.address === '127.0.0.1' ? 'localhost' : parts.address + + return `http://${address}:${parts.port}/webui` +} + +export function validateIPFS (text) { + return isIPFS.multihash(text) || + isIPFS.cid(text) || + isIPFS.ipfsPath(text) || + isIPFS.ipfsPath(`/ipfs/${text}`) +} diff --git a/src/controls/utils/upload-files.js b/src/controls/utils/upload-files.js new file mode 100644 index 000000000..04ce814fe --- /dev/null +++ b/src/controls/utils/upload-files.js @@ -0,0 +1,56 @@ +import path from 'path' +import fs from 'fs' + +function join (...parts) { + const replace = new RegExp('/{1,}', 'g') + return parts.join('/').replace(replace, '/') +} + +function clean (files, root) { + const res = [] + + files.forEach((file) => { + const stat = fs.lstatSync(file) + const dst = join(root, path.basename(file)) + + if (stat.isDirectory()) { + const files = clean(fs.readdirSync(file).map(f => path.join(file, f)), dst) + res.push({dir: true, dst: dst}, ...files) + } else { + res.push({ + dst: dst, + src: file + }) + } + }) + + return res +} + +export default function uploadFiles (opts) { + let {ipfs, debug, send} = opts + let adding = 0 + + const sendAdding = () => { send('adding', adding > 0) } + const inc = () => { adding++; sendAdding() } + const dec = () => { adding--; sendAdding() } + + return (event, files, root = '/') => { + debug('Uploading files', {files}) + files = clean(files, root) + + inc() + Promise.all(files.map(file => { + if (file.dir) { + return ipfs().files.mkdir(file.dst) + } + + return ipfs().files.write(file.dst, file.src, {create: true}) + })).then(() => { + dec() + send('files-updated') + }).catch((e) => { + debug(e.stack) + }) + } +} diff --git a/src/index.js b/src/index.js index 54afbcb30..f8c652357 100644 --- a/src/index.js +++ b/src/index.js @@ -38,9 +38,7 @@ function send (type, ...args) { } config.send = send -config.ipfs = () => { - return IPFS -} +config.ipfs = () => IPFS function stopPolling () { if (poller) poller.stop() @@ -55,17 +53,19 @@ function onPollerChange (stats) { } function onRequestState (node, event) { - if (!node.initialized) { + if (!node.started) { return } let status = 'stopped' - if (node.pid()) { - status = IPFS ? 'running' : 'starting' - } + node.pid((pid) => { + if (pid) { + status = IPFS ? 'running' : 'starting' + } - send('node-status', status) + send('node-status', status) + }) } function onStartDaemon (node) { @@ -79,7 +79,7 @@ function onStartDaemon (node) { } debug('Daemon started') - poller = new StatsPoller(api, 1000, debug) + poller = new StatsPoller(api, 1000) if (menubar.window && menubar.window.isVisible()) { poller.start() @@ -224,9 +224,23 @@ function initialize (path, node) { }) } +// Tries to remove the repo.lock file if it already exists. +// This fixes a bug on Windows, where the daemon seems +// not to be exiting correctly, hence the file is not +// removed. +const lockPath = join(config.settingsStore.get('ipfsPath'), 'repo.lock') + +if (fs.existsSync(lockPath)) { + try { + fs.unlinkSync(lockPath) + } catch (e) { + debug('Could not remove lock. Daemon might be running.') + } +} + // main entry point DaemonFactory.create().spawn({ - repoPath: config.ipfsPath, + repoPath: config.settingsStore.get('ipfsPath'), disposable: false, init: false, start: false diff --git a/src/js/components/logic/README.md b/src/js/components/logic/README.md deleted file mode 100644 index c373a01ca..000000000 --- a/src/js/components/logic/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Logic Components - -All components that are statefull, i.e. they have `state` to track are stored here. diff --git a/src/js/components/view/README.md b/src/js/components/view/README.md deleted file mode 100644 index 1b0c04945..000000000 --- a/src/js/components/view/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# View Components - -In this directory only components that have no own state are stored. -Having no state on themselves means they are only responsible for -displaying the data they are given. - -They should be written as *stateless functional components*: - -```js -export default function MyComponent ({name}) { - return ( -
Hello my name is, {name}.
- ) -} -``` \ No newline at end of file diff --git a/src/js/components/view/file-block.js b/src/js/components/view/file-block.js deleted file mode 100644 index 4bf8593d9..000000000 --- a/src/js/components/view/file-block.js +++ /dev/null @@ -1,99 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' -import moment from 'moment' -import fileExtension from 'file-extension' -import {ipcRenderer, clipboard} from 'electron' - -import Button from './button' -import Icon from './icon' - -/** - * Is a File Block. - * - * @param {Object} props - * - * @prop {String} name - file name - * @prop {String} date - date when the file was modified/uploaded - * @prop {String} hash - file's hash in IPFS system - * - * @return {ReactElement} - */ -export default function FileBlock (props) { - const extension = fileExtension(props.name) - let icon = 'file' - if (fileTypes[extension]) { - icon = fileTypes[extension] - } - - const url = `https://ipfs.io/ipfs/${props.hash}` - - const open = () => { - ipcRenderer.send('open-url', url) - } - - const copy = (event) => { - event.stopPropagation() - event.preventDefault() - clipboard.writeText(url) - } - - return ( -
-
-
- -
-
-

{props.name}

-

{moment(props.date).fromNow()}

-
- { props.uploading && -
- -
- } -
- -
-
-
- ) -} - -FileBlock.propTypes = { - name: PropTypes.string.isRequired, - date: PropTypes.string.isRequired, - hash: PropTypes.string.isRequired, - uploading: PropTypes.bool -} - -const fileTypes = { - png: 'image', - jpg: 'image', - tif: 'image', - tiff: 'image', - bmp: 'image', - gif: 'image', - eps: 'image', - raw: 'image', - cr2: 'image', - nef: 'image', - orf: 'image', - sr2: 'image', - jpeg: 'image', - mp3: 'music-alt', - flac: 'music-alt', - ogg: 'music-alt', - oga: 'music-alt', - aa: 'music-alt', - aac: 'music-alt', - m4p: 'music-alt', - webm: 'music-alt', - mp4: 'video-clapper', - mkv: 'video-clapper', - avi: 'video-clapper', - asf: 'video-clapper', - flv: 'video-clapper' -} diff --git a/src/js/panes/files.js b/src/js/panes/files.js deleted file mode 100644 index b6e7a5110..000000000 --- a/src/js/panes/files.js +++ /dev/null @@ -1,122 +0,0 @@ -import React, {Component} from 'react' -import PropTypes from 'prop-types' -import {ipcRenderer} from 'electron' -import {NativeTypes} from 'react-dnd-html5-backend' -import {DropTarget} from 'react-dnd' - -import Pane from '../components/view/pane' -import Header from '../components/view/header' -import Footer from '../components/view/footer' -import File from '../components/view/file-block' -import IconButton from '../components/view/icon-button' - -const fileTarget = { - drop (props, monitor) { - const files = monitor.getItem().files - const filesArray = [] - for (let i = 0; i < files.length; i++) { - filesArray.push(files[i].path) - } - - ipcRenderer.send('drop-files', filesArray) - } -} - -class Files extends Component { - constructor (props) { - super(props) - this.state = { - sticky: false - } - } - - static propTypes = { - connectDropTarget: PropTypes.func.isRequired, - isOver: PropTypes.bool.isRequired, - canDrop: PropTypes.bool.isRequired, - adding: PropTypes.bool, - files: PropTypes.array - } - - static defaultProps = { - adding: false, - files: [] - } - - _selectFileDialog (event) { - ipcRenderer.send('open-file-dialog') - } - - _selectDirectoryDialog (event) { - ipcRenderer.send('open-dir-dialog') - } - - _toggleStickWindow = (event) => { - ipcRenderer.send('toggle-sticky') - } - - _onSticky = (event, sticky) => { - this.setState({ sticky: sticky }) - } - - componentDidMount () { - ipcRenderer.on('sticky-window', this._onSticky) - } - - componentWillUnmount () { - ipcRenderer.removeListener('sticky-window', this._onSticky) - if (this.state.sticky) this._toggleStickWindow() - } - - render () { - const {connectDropTarget, isOver, canDrop} = this.props - - const dropper = { - visibility: (isOver && canDrop) ? 'visible' : 'hidden' - } - - let files = this.props.files.map(file => { - return () - }) - - if (files.length === 0) { - files = ( -

- You do not have any files yet. Add your first one by dropping - it here or clicking on one of the buttons on the bottom right side. -

- ) - } - - return connectDropTarget( -
- -
- -
- {files} -
- -
- Drop to upload to IPFS -
- -
- - -
- - -
-
- -
- ) - } -} - -export default DropTarget(NativeTypes.FILE, fileTarget, (connect, monitor) => ({ - connectDropTarget: connect.dropTarget(), - isOver: monitor.isOver(), - canDrop: monitor.canDrop() -}))(Files) diff --git a/src/js/panes/info.js b/src/js/panes/info.js deleted file mode 100644 index 7c2dd14fa..000000000 --- a/src/js/panes/info.js +++ /dev/null @@ -1,126 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' -import {clipboard, ipcRenderer} from 'electron' -import prettyBytes from 'pretty-bytes' - -import Pane from '../components/view/pane' -import Header from '../components/view/header' -import Footer from '../components/view/footer' -import IconButton from '../components/view/icon-button' -import InfoBlock from '../components/view/info-block' - -function onClickCopy (text) { - return () => clipboard.writeText(text) -} - -function openNodeSettings () { - ipcRenderer.send('open-node-settings') -} - -function openWebUI () { - ipcRenderer.send('open-webui') -} - -export default function Info (props) { - const onClick = () => { - if (props.running) { - ipcRenderer.send('stop-daemon') - } else { - ipcRenderer.send('start-daemon') - } - } - - return ( - -
- -
-
-

{prettyBytes(props.repo.RepoSize)}

-

Sharing {props.repo.NumObjects} objects

-
- - - - - - - - - - - - - - - - - - - - -
- -
-
- -
-
- - ) -} - -Info.propTypes = { - id: PropTypes.string, - running: PropTypes.bool.isRequired, - location: PropTypes.string, - protocolVersion: PropTypes.string, - publicKey: PropTypes.string, - addresses: PropTypes.array, - repo: PropTypes.object, - bandwidth: PropTypes.object -} - -Info.defaultProps = { - id: 'Undefined', - location: 'Unknown', - protocolVersion: 'Undefined', - publicKey: 'Undefined', - addresses: [], - repo: { - RepoSize: 0, - NumObjects: 'NA' - }, - bandwidth: { - TotalIn: 0, - TotalOut: 0, - RateIn: 0, - RateOut: 0 - } -} diff --git a/src/panes/Files.js b/src/panes/Files.js new file mode 100644 index 000000000..ba2fb67bb --- /dev/null +++ b/src/panes/Files.js @@ -0,0 +1,178 @@ +import React, {Component} from 'react' +import PropTypes from 'prop-types' +import {ipcRenderer, clipboard} from 'electron' +import {NativeTypes} from 'react-dnd-html5-backend' +import {DropTarget} from 'react-dnd' + +import Pane from '../components/Pane' +import Header from '../components/Header' +import Footer from '../components/Footer' +import IconButton from '../components/IconButton' +import FileBlock from '../components/FileBlock' +import Breadcrumbs from '../components/Breadcrumbs' + +function join (...parts) { + const replace = new RegExp('/{1,}', 'g') + return parts.join('/').replace(replace, '/') +} + +const fileTarget = { + drop (props, monitor, component) { + const files = monitor.getItem().files + const filesArray = [] + for (let i = 0; i < files.length; i++) { + filesArray.push(files[i].path) + } + + ipcRenderer.send('drop-files', filesArray, component.state.root) + } +} + +class Files extends Component { + constructor (props) { + super(props) + this.state = { + sticky: false, + root: '/', + files: [] + } + } + + static propTypes = { + connectDropTarget: PropTypes.func.isRequired, + isOver: PropTypes.bool.isRequired, + canDrop: PropTypes.bool.isRequired, + adding: PropTypes.bool + } + + onFiles = (event, root, files) => { + this.setState({ + root: root, + files: files + }) + } + + selectFileDialog = (event) => { + ipcRenderer.send('open-file-dialog', this.state.root) + } + + selectDirectoryDialog = (event) => { + ipcRenderer.send('open-dir-dialog', this.state.root) + } + + toggleStickWindow = (event) => { + ipcRenderer.send('toggle-sticky') + } + + open = (name, hash) => { + ipcRenderer.send('open-url', `https://ipfs.io/ipfs/${hash}`) + } + + navigate = (name) => { + const root = join(this.state.root, name) + ipcRenderer.send('request-files', root) + } + + trash = (name) => { + name = join(this.state.root, name) + ipcRenderer.send('remove-file', name) + } + + filesUpdated = () => { + ipcRenderer.send('request-files', this.state.root) + } + + onSticky = (event, sticky) => { + this.setState({ sticky: sticky }) + } + + copy = (hash) => { + clipboard.writeText(`https://ipfs.io/ipfs/${hash}`) + } + + componentDidMount () { + ipcRenderer.on('files', this.onFiles) + ipcRenderer.on('sticky-window', this.onSticky) + ipcRenderer.on('files-updated', this.filesUpdated) + + ipcRenderer.send('request-files', this.state.root) + } + + componentWillUnmount () { + ipcRenderer.removeListener('files', this.onFiles) + ipcRenderer.removeListener('files-updated', this.filesUpdated) + ipcRenderer.removeListener('sticky-window', this.onSticky) + + if (this.state.sticky) this.toggleStickWindow() + } + + makeBreadcrumbs = () => { + const navigate = (root) => { ipcRenderer.send('request-files', root) } + + return + } + + render () { + const {connectDropTarget, isOver, canDrop} = this.props + + const dropper = { + visibility: (isOver && canDrop) ? 'visible' : 'hidden' + } + + let files = this.state.files.map((file, index) => { + return ( + + ) + }) + + if (files.length === 0) { + files = ( +

+ You do not have any files yet. Add your first one by dropping + it here or clicking on one of the buttons on the bottom right side. +

+ ) + } + + return connectDropTarget( +
+ +
+ +
+ {files} +
+ +
+ Drop to upload to IPFS +
+ +
+ + +
+ + +
+
+ +
+ ) + } +} + +export default DropTarget(NativeTypes.FILE, fileTarget, (connect, monitor) => ({ + connectDropTarget: connect.dropTarget(), + isOver: monitor.isOver(), + canDrop: monitor.canDrop() +}))(Files) diff --git a/src/panes/Info.js b/src/panes/Info.js new file mode 100644 index 000000000..877ca6c68 --- /dev/null +++ b/src/panes/Info.js @@ -0,0 +1,120 @@ +import React from 'react' +import PropTypes from 'prop-types' +import {clipboard, ipcRenderer} from 'electron' +import prettyBytes from 'pretty-bytes' + +import Pane from '../components/Pane' +import Header from '../components/Header' +import InfoBlock from '../components/InfoBlock' + +function copy (text) { + return () => { clipboard.writeText(text) } +} + +function openNodeSettings () { + ipcRenderer.send('open-node-settings') +} + +function openWebUI () { + ipcRenderer.send('open-webui') +} + +function stopDaemon () { + ipcRenderer.send('stop-daemon') +} + +export default function Info (props) { + return ( + +
+ +
+
+

{prettyBytes(props.repo.RepoSize)}

+

Sharing {props.repo.NumObjects} objects

+
+ + + + + + + + + + + + + + + + + + + + + + +
+ + ) +} + +Info.propTypes = { + node: PropTypes.object, + repo: PropTypes.object, + bw: PropTypes.object +} + +Info.defaultProps = { + node: { + id: 'Undefined', + location: 'Unknown', + protocolVersion: 'Undefined', + publicKey: 'Undefined', + addresses: [] + }, + repo: { + RepoSize: 0, + NumObjects: 0 + }, + bw: { + TotalIn: 0, + TotalOut: 0, + RateIn: 0, + RateOut: 0 + } +} diff --git a/src/js/panes/intro.js b/src/panes/Intro.js similarity index 90% rename from src/js/panes/intro.js rename to src/panes/Intro.js index 075d1563d..082fadfcf 100644 --- a/src/js/panes/intro.js +++ b/src/panes/Intro.js @@ -2,10 +2,10 @@ import React, {Component} from 'react' import PropTypes from 'prop-types' import {ipcRenderer} from 'electron' -import Pane from '../components/view/pane' -import Button from '../components/view/button' -import Icon from '../components/view/icon' -import IconDropdownList from '../components/view/icon-dropdown-list' +import Pane from '../components/Pane' +import Button from '../components/Button' +import Icon from '../components/Icon' +import IconDropdownList from '../components/IconDropdownList' export default class Intro extends Component { constructor (props) { diff --git a/src/js/panes/loader.js b/src/panes/Loader.js similarity index 86% rename from src/js/panes/loader.js rename to src/panes/Loader.js index d80269b32..2e3b4e082 100644 --- a/src/js/panes/loader.js +++ b/src/panes/Loader.js @@ -1,6 +1,5 @@ import React from 'react' - -import Pane from '../components/view/pane' +import Pane from '../components/Pane' /** * Is a Loader. diff --git a/src/js/panes/peers.js b/src/panes/Peers.js similarity index 59% rename from src/js/panes/peers.js rename to src/panes/Peers.js index 6ea37b219..debfb91d0 100644 --- a/src/js/panes/peers.js +++ b/src/panes/Peers.js @@ -1,19 +1,26 @@ import React, {Component} from 'react' import PropTypes from 'prop-types' -import Pane from '../components/view/pane' -import InfoBlock from '../components/view/info-block' -import Header from '../components/view/header' -import Footer from '../components/view/footer' +import Pane from '../components/Pane' +import Header from '../components/Header' +import Footer from '../components/Footer' +import PeerBlock from '../components/PeerBlock' +import InputText from '../components/InputText' export default class Peers extends Component { constructor (props) { super(props) - this.state = { search: null } - } - onChangeSearch = event => { - this.setState({ search: event.target.value.toLowerCase() }) + this.state = { + search: '', + location: 'Unknown' + } + + this.onChangeSearch = value => { + this.setState({ + search: value.toLowerCase() + }) + } } render () { @@ -27,7 +34,13 @@ export default class Peers extends Component { } peers = peers.map((peer, i) => { - return () + return ( + + ) }) return ( @@ -35,13 +48,18 @@ export default class Peers extends Component {
+
{peers}
- +
diff --git a/src/js/panes/pinned.js b/src/panes/Pinned.js similarity index 76% rename from src/js/panes/pinned.js rename to src/panes/Pinned.js index 3d222ac18..88374bf20 100644 --- a/src/js/panes/pinned.js +++ b/src/panes/Pinned.js @@ -2,12 +2,13 @@ import React, {Component} from 'react' import PropTypes from 'prop-types' import {ipcRenderer} from 'electron' -import Pane from '../components/view/pane' -import Header from '../components/view/header' -import Footer from '../components/view/footer' -import IconButton from '../components/view/icon-button' -import PinnedHash from '../components/view/pinned-hash' -import NewPinnedHash from '../components/logic/new-pinned-hash' +import Pane from '../components/Pane' +import Header from '../components/Header' +import Footer from '../components/Footer' +import IconButton from '../components/IconButton' +import PinnedHash from '../components/PinnedHash' +import NewPinnedHash from '../components/NewPinnedHash' +import InputText from '../components/InputText' export default class Pinned extends Component { static propTypes = { @@ -34,8 +35,8 @@ export default class Pinned extends Component { this.setState({ showNew: !this.state.showNew }) } - onSearch = (event) => { - this.setState({ search: event.target.value.toLowerCase() }) + onSearch = (text) => { + this.setState({ search: text }) } static tagUpdater = (hash) => (event) => { @@ -54,6 +55,7 @@ export default class Pinned extends Component { hashes.push(( )) @@ -83,7 +85,11 @@ export default class Pinned extends Component {
- +
diff --git a/src/js/panes/README.md b/src/panes/README.md similarity index 100% rename from src/js/panes/README.md rename to src/panes/README.md diff --git a/src/js/panes/settings.js b/src/panes/Settings.js similarity index 85% rename from src/js/panes/settings.js rename to src/panes/Settings.js index b3bf9fcf1..86cf21bfd 100644 --- a/src/js/panes/settings.js +++ b/src/panes/Settings.js @@ -2,11 +2,11 @@ import React from 'react' import PropTypes from 'prop-types' import {ipcRenderer} from 'electron' -import Pane from '../components/view/pane' -import Header from '../components/view/header' -import InfoBlock from '../components/view/info-block' -import CheckboxBlock from '../components/view/checkbox-block' -import KeyCombo from '../components/view/key-combo' +import Pane from '../components/Pane' +import Header from '../components/Header' +import InfoBlock from '../components/InfoBlock' +import CheckboxBlock from '../components/CheckboxBlock' +import KeyCombo from '../components/KeyCombo' function generateOnChange (key) { return (value) => { @@ -62,7 +62,7 @@ export default function Settings (props) { }) return ( - +
diff --git a/src/js/screens/README.md b/src/screens/README.md similarity index 100% rename from src/js/screens/README.md rename to src/screens/README.md diff --git a/src/js/screens/menu.js b/src/screens/menubar.js similarity index 80% rename from src/js/screens/menu.js rename to src/screens/menubar.js index 1b1681419..cb1806630 100644 --- a/src/js/screens/menu.js +++ b/src/screens/menubar.js @@ -3,19 +3,21 @@ import {ipcRenderer} from 'electron' import {DragDropContext} from 'react-dnd' import HTML5Backend from 'react-dnd-html5-backend' -import PaneContainer from '../components/view/pane-container' -import Pane from '../components/view/pane' -import MenuOption from '../components/view/menu-option' +import Pane from '../components/Pane' +import PaneContainer from '../components/PaneContainer' +import MenuOption from '../components/MenuOption' +import Menu from '../components/Menu' -import Loader from '../panes/loader' -import Files from '../panes/files' -import Pinned from '../panes/pinned' -import Peers from '../panes/peers' -import Info from '../panes/info' -import Settings from '../panes/settings' +import Peers from '../panes/Peers' +import Loader from '../panes/Loader' + +import Files from '../panes/Files' +import Pinned from '../panes/Pinned' +import Info from '../panes/Info' +import Settings from '../panes/Settings' const UNINITIALIZED = 'uninitialized' -const RUNNING = 'running' +const STOPPED = 'stopped' const STARTING = 'starting' const STOPPING = 'stopping' @@ -47,7 +49,7 @@ const panes = [ } ] -class Menu extends Component { +class Menubar extends Component { state = { status: UNINITIALIZED, route: panes[0].id, @@ -81,14 +83,12 @@ class Menu extends Component { // -- Listen to control events ipcRenderer.on('node-status', this._onSomething('status')) ipcRenderer.on('stats', this._onSomething('stats')) - ipcRenderer.on('files', this._onSomething('files')) ipcRenderer.on('pinned', this._onSomething('pinned')) ipcRenderer.on('settings', this._onSomething('settings')) ipcRenderer.on('adding', this._onSomething('adding')) ipcRenderer.on('pinning', this._onSomething('pinning')) ipcRenderer.send('request-state') - ipcRenderer.send('request-files') ipcRenderer.send('request-settings') ipcRenderer.send('request-pinned') } @@ -97,7 +97,6 @@ class Menu extends Component { // -- Remove control events ipcRenderer.removeListener('node-status', this._onSomething('status')) ipcRenderer.removeListener('stats', this._onSomething('stats')) - ipcRenderer.removeListener('files', this._onSomething('files')) ipcRenderer.removeListener('pinned', this._onSomething('pinned')) ipcRenderer.removeListener('settings', this._onSomething('settings')) ipcRenderer.removeListener('adding', this._onSomething('adding')) @@ -109,9 +108,15 @@ class Menu extends Component { return } + if (this.state.status === STOPPED || this.state.status === UNINITIALIZED) { + // TODO: add start running screen here :) + // It should overlap all of the interface so it is the only thing available. + return + } + switch (this.state.route) { case 'files': - return + return case 'settings': return case 'peers': @@ -124,9 +129,8 @@ class Menu extends Component { case 'info': return ( ) case 'pinned': @@ -162,7 +166,7 @@ class Menu extends Component { }) return ( -
{menu}
+ {menu} ) } @@ -181,4 +185,4 @@ class Menu extends Component { } } -export default DragDropContext(HTML5Backend)(Menu) +export default DragDropContext(HTML5Backend)(Menubar) diff --git a/src/js/screens/welcome.js b/src/screens/welcome.js similarity index 91% rename from src/js/screens/welcome.js rename to src/screens/welcome.js index fee02110a..9898b33e0 100644 --- a/src/js/screens/welcome.js +++ b/src/screens/welcome.js @@ -2,11 +2,11 @@ import React, {Component} from 'react' import {ipcRenderer} from 'electron' import Intro from '../panes/intro' -import Loader from '../panes/loader' -import PaneContainer from '../components/view/pane-container' -import Pane from '../components/view/pane' -import Heartbeat from '../components/view/heartbeat' +import Pane from '../components/Pane' +import PaneContainer from '../components/PaneContainer' +import Loader from '../components/Loader' +import Heartbeat from '../components/Heartbeat' const INTRO = 'intro' const INTITIALZING = 'initializing' diff --git a/src/styles/app.less b/src/styles/app.less index d3f650943..9ec6fef5c 100644 --- a/src/styles/app.less +++ b/src/styles/app.less @@ -1,18 +1,34 @@ @import '../../node_modules/normalize.css/normalize.css'; -@import './fonts.less'; -@import './themify-icons.less'; @import './common.less'; -@import './components/heartbeat.less'; -@import './components/pane.less'; -@import './components/info-block.less'; -@import './components/file-block.less'; -@import './components/checkbox-block.less'; -@import './components/new-pinned-hash.less'; +@import "./components/Block.less"; +@import "./components/Breadcrumbs.less"; +@import "./components/Button.less"; +@import "./components/CheckboxBlock.less"; +@import "./components/FileBlock.less"; +@import "./components/Footer.less"; +@import "./components/Header.less"; +@import "./components/Heartbeat.less"; +@import "./components/Icon.less"; +@import "./components/IconButton.less"; +@import "./components/Input.less"; +@import "./components/Key.less"; +@import "./components/Menu.less"; +@import "./components/MenuOption.less"; +@import "./components/NewPinnedHash.less"; +@import "./components/Pane.less"; +@import "./components/PaneContainer.less"; -@import './panes/node.less'; -@import './panes/files.less'; -@import './panes/loader.less'; +@import './panes/Files.less'; +@import './panes/Info.less'; +@import './panes/Loader.less'; @import './screens/welcome.less'; -@import './screens/menubar.less'; \ No newline at end of file + +#menubar .pane { + width: calc(~"100vw - 4em"); + + .footer { + width: calc(~"100vw - 4.5em"); + } +} diff --git a/src/styles/common.less b/src/styles/common.less index 46b4e19e0..c8ba15d3c 100644 --- a/src/styles/common.less +++ b/src/styles/common.less @@ -6,20 +6,6 @@ box-sizing: border-box; } -html, -body { - font-family: 'Inter UI', sans-serif; - overflow: hidden; - color: #FFF; - background: #000000; - user-select: none; -} - -.light { - color: #212121; - background: #fff; -} - ::-webkit-scrollbar { background-color: transparent; width: .5em; @@ -36,44 +22,6 @@ body { background: rgba(0, 0, 0, 0.3); } -.dropdown-list, -.directory-input, -.button, -input[type="text"] { - border-radius: 0.2em; - padding: 0.5em 1em; - border: 0; - color: #fff; - background: rgba(255, 255, 255, 0.25); - outline: 0; - transition: .2s ease all; -} - -.light .dropdown-list, -.light .directory-input, -.light .button, -.light input[type="text"] { - background: rgba(0, 0, 0, 0.2); -} - -.button { - cursor: pointer; - - &:hover { - background: rgba(255, 255, 255, 0.1); - } - - span { - font-size: @normal-font; - } -} - -.light .button { - &:hover { - background: rgba(0, 0, 0, 0.3) - } -} - .directory-input { cursor: pointer; @@ -87,11 +35,6 @@ input[type="text"] { } } -.icon img { - width: 20px; - height: 20px; -} - .directory-input, .dropdown-list { display: flex; @@ -130,32 +73,6 @@ input[type="text"] { } } -.button-icon { - line-height: 1; - border: 0; - outline: 0; - padding: 0; - background: transparent; - color: rgba(255, 255, 255, 0.4); - cursor: pointer; - margin-left: 0.5em; - transition: .2s ease all; - - &.active, - &:hover { - color: rgba(255, 255, 255, 0.6); - } -} - -.light .button-icon { - color: rgba(0, 0, 0, 0.5); - - &.active, - &:hover { - color: rgba(0, 0, 0, 0.8); - } -} - .notice { color: rgba(255, 255, 255, 0.5); position: absolute; @@ -169,14 +86,3 @@ input[type="text"] { .light .notice { color: #212121 } - -.key { - font-size: 0.8em; - background: rgba(255, 255, 255, 0.1); - border-radius: 0.2em; - padding: 0 0.2em; -} - -.light .key { - background: #eee; -} \ No newline at end of file diff --git a/src/styles/components/Block.less b/src/styles/components/Block.less new file mode 100644 index 000000000..6ddca3688 --- /dev/null +++ b/src/styles/components/Block.less @@ -0,0 +1,101 @@ +.block { + padding: 0 1em; + transition: .2s ease background-color; + position: relative; +} + +.block.clickable { + cursor: pointer; +} + +.block .wrapper { + padding: 1em 0; + border-top: 1px solid rgba(255,255,255,0.2); + display: flex; +} + +.block .wrapper > div { + max-width: 100%; +} + +.block:hover { + background: #202020; +} + +.block:first-of-type { + border-top: 0; +} + +.block p { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + margin: 0; +} + +.block .label { + margin-bottom: 0.2em; + font-size: 18px; +} + +.block .info { + color: rgba(255,255,255,0.5); + font-size: 14px; +} + +.block .button-overlay { + opacity: 0; + transition: .2s ease opacity; + position: absolute; + top: 1em; + right: 1em; + height: calc(~"100% - 2em"); + width: 50%; + background: linear-gradient(to left,#202020 0%,#202020 50%,transparent 100%); + display: flex; + align-items: center; + justify-content: flex-end; +} + +.block .button-overlay > * { + opacity: 0; + margin-left: .5em; +} + +.block:hover .button-overlay > * { + opacity: 1; +} + +.block:hover .button-overlay { + opacity: 1; +} + +.block input.info, +.block input.label { + background: transparent; + display: block; + padding: 0; + color: inherit; + width: 100%; +} + +.block .right { + margin-left: auto; + padding-left: 1em; +} + +.light .block>div { + border-color: rgba(0,0,0,0.1); +} + +.light .block:hover { + background: #fbfbfb; +} + +.light .block .info { + color: rgba(0,0,0,0.5); +} + +.light .block .button-overlay { + background: linear-gradient(to left,#fbfbfb 0%,#fbfbfb 50%,transparent 100%); +} \ No newline at end of file diff --git a/src/styles/components/Breadcrumbs.less b/src/styles/components/Breadcrumbs.less new file mode 100644 index 000000000..cd1a096ae --- /dev/null +++ b/src/styles/components/Breadcrumbs.less @@ -0,0 +1,7 @@ +.breadcrumbs a { + cursor: pointer; +} + +.breadcrumbs span { + margin: 0 0.2em; +} diff --git a/src/styles/components/Button.less b/src/styles/components/Button.less new file mode 100644 index 000000000..e882c2327 --- /dev/null +++ b/src/styles/components/Button.less @@ -0,0 +1,16 @@ +.input button, +.input.button { + cursor: pointer; +} + +.input.button:hover { + background: rgba(255,255,255,0.1); +} + +.input.button span { + font-size: 14px; +} + +.light .input.button:hover { + background: rgba(0,0,0,0.3); +} diff --git a/src/styles/components/CheckboxBlock.less b/src/styles/components/CheckboxBlock.less new file mode 100644 index 000000000..4989ed5ca --- /dev/null +++ b/src/styles/components/CheckboxBlock.less @@ -0,0 +1,42 @@ +.block.checkbox { + cursor: pointer; +} + +.block.checkbox .wrapper > div { + max-width: 100%; + display: flex; + width: 100%; + align-items: center; +} + +.block.checkbox input[type='checkbox'] { + display: none; +} + +.block.checkbox p { + white-space: pre-wrap; +} + +.block.checkbox .checkbox { + width: 1em; + height: 1em; + background: #fff; + border-radius: 0.2em; + display: block; + transition: .2s ease-in-out all; + position: relative; +} + +.block.checkbox input[type='checkbox']:checked~.checkbox::before { + content: '\2714'; + color: #5cb6bf; + font-size: 0.9em; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} + +.light .block.checkbox .checkbox { + background: #eee; +} \ No newline at end of file diff --git a/src/styles/components/FileBlock.less b/src/styles/components/FileBlock.less new file mode 100644 index 000000000..9fca4f40f --- /dev/null +++ b/src/styles/components/FileBlock.less @@ -0,0 +1,18 @@ +.block.file > div > div{ + display: flex; + align-items: center; +} + +.block.file > div > div div:nth-child(2) { + overflow: hidden; +} + +.block.file .icon { + font-size: 1.6em; + margin-right: 0.35em; + color: rgba(255, 255, 255, 0.5) +} + +.light .block.file .icon { + color: rgba(0, 0, 0, 0.5) +} diff --git a/src/styles/components/Footer.less b/src/styles/components/Footer.less new file mode 100644 index 000000000..8abecf277 --- /dev/null +++ b/src/styles/components/Footer.less @@ -0,0 +1,30 @@ +.footer { + padding: 1em; + height: 5.25em; + display: flex; + background: linear-gradient(to top, #000000 0%, #000000 45%, transparent 100%); + position: fixed; + bottom: 0; + right: 0.5em; + padding-top: 3em; + box-sizing: border-box; + width: 100%; +} + +.footer .right { + margin-left: auto; +} + +.footer .button { + margin-left: 0.5em; +} + +.footer .button, +.footer .input { + font-size: 14px; + margin-top: -0.5em; +} + +.light .footer { + background: linear-gradient(to top, #fff 0%, #fff 45%, transparent 100%); +} diff --git a/src/styles/components/Header.less b/src/styles/components/Header.less new file mode 100644 index 000000000..206d74315 --- /dev/null +++ b/src/styles/components/Header.less @@ -0,0 +1,36 @@ +.header { + padding: 1em; + display: flex; + position: relative; +} + +.header .title { + font-size: 18px; + color: rgba(255, 255, 255, 0.5); + margin: 0 +} + +.header .subtitle { + margin: 0; + font-size: 35px; +} + +.header>div:first-child { + margin-right: auto; +} + +.header.loading:after{ + display: block; + position: absolute; + content: ""; + left: -20em; + width: 20em; + height: 4px; + bottom: 0; + background-color: #FFEB3B; + animation: loading 2s linear infinite; +} + +.light .header .title { + color: rgba(0, 0, 0, 0.5); +} diff --git a/src/styles/components/heartbeat.less b/src/styles/components/Heartbeat.less similarity index 100% rename from src/styles/components/heartbeat.less rename to src/styles/components/Heartbeat.less diff --git a/src/styles/themify-icons.less b/src/styles/components/Icon.less similarity index 98% rename from src/styles/themify-icons.less rename to src/styles/components/Icon.less index 847e86abe..626849338 100644 --- a/src/styles/themify-icons.less +++ b/src/styles/components/Icon.less @@ -1,10 +1,15 @@ +.icon img { + width: 20px; + height: 20px; +} + @font-face { font-family: 'themify'; - src:url('../fonts/themify.eot?-fvbane'); - src:url('../fonts/themify.eot?#iefix-fvbane') format('embedded-opentype'), - url('../fonts/themify.woff?-fvbane') format('woff'), - url('../fonts/themify.ttf?-fvbane') format('truetype'), - url('../fonts/themify.svg?-fvbane#themify') format('svg'); + src:url('../fonts/themify.eot'); + src:url('../fonts/themify.eot') format('embedded-opentype'), + url('../fonts/themify.woff') format('woff'), + url('../fonts/themify.ttf') format('truetype'), + url('../fonts/themify.svg') format('svg'); font-weight: normal; font-style: normal; } diff --git a/src/styles/components/IconButton.less b/src/styles/components/IconButton.less new file mode 100644 index 000000000..edabc7748 --- /dev/null +++ b/src/styles/components/IconButton.less @@ -0,0 +1,25 @@ +.button-icon { + line-height: 1; + border: 0; + outline: 0; + padding: 0; + background: transparent; + color: rgba(255, 255, 255, 0.4); + cursor: pointer; + margin-left: 0.5em; + transition: .2s ease all; +} + +.button-icon.active, +.button-icon:hover { + color: rgba(255, 255, 255, 0.6); +} + +.light .button-icon { + color: rgba(0, 0, 0, 0.5); +} + +.light .button-icon.active, +.light .button-icon:hover { + color: rgba(0, 0, 0, 0.8); +} diff --git a/src/styles/components/Input.less b/src/styles/components/Input.less new file mode 100644 index 000000000..3269715c0 --- /dev/null +++ b/src/styles/components/Input.less @@ -0,0 +1,24 @@ +.input { + border-radius: 0.2em; + padding: 0; + border: 0; + color: #fff; + background: rgba(255, 255, 255, 0.25); + outline: 0; + transition: .2s ease all; +} + +.input > * { + background: transparent; + border: none; + padding: 0; + margin: 0; + font-size: inherit; + outline: 0; + padding: 0.5em 1em; + color: inherit; +} + +.light .input { + background: rgba(0, 0, 0, 0.2); +} diff --git a/src/styles/components/Key.less b/src/styles/components/Key.less new file mode 100644 index 000000000..5836befff --- /dev/null +++ b/src/styles/components/Key.less @@ -0,0 +1,10 @@ +.key { + font-size: 0.8em; + background: rgba(255, 255, 255, 0.1); + border-radius: 0.2em; + padding: 0 0.2em; +} + +.light .key { + background: #eee; +} diff --git a/src/styles/components/Menu.less b/src/styles/components/Menu.less new file mode 100644 index 000000000..f4b47e395 --- /dev/null +++ b/src/styles/components/Menu.less @@ -0,0 +1,9 @@ +.menu { + display: flex; + background: #292929; + flex-direction: column; +} + +.light .menu { + background: #eee; +} diff --git a/src/styles/components/MenuOption.less b/src/styles/components/MenuOption.less new file mode 100644 index 000000000..cedf57247 --- /dev/null +++ b/src/styles/components/MenuOption.less @@ -0,0 +1,44 @@ +.menu-option { + line-height: 1; + border: 0; + outline: 0; + margin: 0; + padding: 0.5em; + background: transparent; + color: rgba(255, 255, 255, 0.4); + cursor: pointer; + transition: .2s ease all; + width: 4em; + height: 4em; +} + +.menu-option:hover { + color: rgba(255, 255, 255, 0.6); +} + +.menu-option.active { + background: #000; + color: #fff; +} + +.menu-option p { + margin: 0.8em 0 0; + font-size: 0.8em; +} + +.menu-option:last-child { + margin-top: auto; +} + +.light .menu-option { + color: #212121; +} + +.light .menu-option:hover { + background: rgba(0, 0, 0, 0.1); +} + +.light .menu-option.active { + background: #000; + color: #fff; +} diff --git a/src/styles/components/new-pinned-hash.less b/src/styles/components/NewPinnedHash.less similarity index 95% rename from src/styles/components/new-pinned-hash.less rename to src/styles/components/NewPinnedHash.less index d0e208901..4b5f64345 100644 --- a/src/styles/components/new-pinned-hash.less +++ b/src/styles/components/NewPinnedHash.less @@ -1,4 +1,4 @@ -.info-block.new-pinned { +.block.new-pinned { transition: .2s ease max-height, .2s ease transform; overflow: hidden; max-height: 5em; diff --git a/src/styles/fonts.less b/src/styles/components/PaneContainer.less similarity index 63% rename from src/styles/fonts.less rename to src/styles/components/PaneContainer.less index 34b409dfc..90861629f 100644 --- a/src/styles/fonts.less +++ b/src/styles/components/PaneContainer.less @@ -1,5 +1,3 @@ -@charset 'UTF-8'; - @font-face { font-family: 'Inter UI'; font-style: normal; @@ -12,4 +10,22 @@ font-style: normal; font-weight: 500; src: url("../fonts/Inter-UI-Medium.woff2?v=2.3") format("woff2"), url("../fonts/Inter-UI-Medium.woff?v=2.3") format("woff"); -} \ No newline at end of file +} + +.panes * { + box-sizing: border-box; +} + +.panes { + font-family: 'Inter UI', sans-serif; + display: flex; + overflow: hidden; + user-select: none; + color: #FFF; + background: #000000; +} + +.panes.light { + color: #212121; + background: #fff; +} diff --git a/src/styles/components/checkbox-block.less b/src/styles/components/checkbox-block.less deleted file mode 100644 index 6fa6316c5..000000000 --- a/src/styles/components/checkbox-block.less +++ /dev/null @@ -1,42 +0,0 @@ -.info-block.checkbox { - cursor: pointer; - - &>div { - display: flex; - align-items: center; - } - - input[type='checkbox'] { - display: none; - } - - p { - white-space: pre-wrap; - } - - .checkbox { - width: 1em; - height: 1em; - background: #fff; - border-radius: 0.2em; - display: block; - transition: .2s ease-in-out all; - position: relative; - } - - input[type='checkbox']:checked ~ .checkbox::before { - content: '\2714'; - color: #5cb6bf; - font-size: 0.9em; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%) - } -} - -.light .info-block.checkbox { - .checkbox { - background: #eee; - } -} \ No newline at end of file diff --git a/src/styles/components/file-block.less b/src/styles/components/file-block.less deleted file mode 100644 index 306fce8cc..000000000 --- a/src/styles/components/file-block.less +++ /dev/null @@ -1,22 +0,0 @@ -.info-block.file { - &>div { - display: flex; - align-items: center; - } - - &>div>div:nth-child(2) { - overflow: hidden; - } - - & .icon { - font-size: 1.6em; - margin-right: 0.35em; - color: rgba(255, 255, 255, 0.5) - } -} - -.light .info-block.file { - & .icon { - color: rgba(0, 0, 0, 0.5) - } -} \ No newline at end of file diff --git a/src/styles/components/info-block.less b/src/styles/components/info-block.less deleted file mode 100644 index 6edbfec97..000000000 --- a/src/styles/components/info-block.less +++ /dev/null @@ -1,100 +0,0 @@ -@import '../variables.less'; - -.info-block { - padding: 0 1em; - transition: .2s ease background-color; - position: relative; - - &.clickable { - cursor: pointer; - } - - .wrapper { - padding: 1em 0; - border-top: 1px solid rgba(255, 255, 255, 0.2); - display: flex; - } - - .wrapper > div { - max-width: 100%; - } - - &:hover { - background: #202020; - } - - &:first-of-type { - border-top: 0; - } - - p { - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; - margin: 0; - } - - .label { - margin-bottom: 0.2em; - font-size: @medium-font; - } - - .info { - color: rgba(255, 255, 255, 0.5); - font-size: @normal-font; - } - - .button-overlay { - opacity: 0; - transition: .2s ease opacity; - position: absolute; - top: 1em; - right: 1em; - height: calc(~"100% - 2em"); - width: 50%; - background: linear-gradient(to left, #202020 0%, #202020 50%, transparent 100%); - align-items: center; - justify-content: flex-end; - display: flex; - } - - .button-overlay .button { - margin-left: 0.5em; - } - - &:hover .button-overlay { - opacity: 1; - } - - input.info, - input.label { - background: transparent; - display: block; - padding: 0; - color: inherit; - width: 100%; - } - - .right { - margin-left: auto; - padding-left: 1em; - } -} - -.light .info-block { - &>div { - border-color: rgba(0, 0, 0, 0.1); - } - - &:hover { - background: #fbfbfb; - } - - .info { - color: rgba(0, 0, 0, 0.5); - } - - .button-overlay { - background: linear-gradient(to left, #fbfbfb 0%, #fbfbfb 50%, transparent 100%); - } -} diff --git a/src/styles/components/pane.less b/src/styles/components/pane.less index a63e8c331..3745d815f 100644 --- a/src/styles/components/pane.less +++ b/src/styles/components/pane.less @@ -1,9 +1,3 @@ -@import '../variables.less'; - -.panes { - display: flex; -} - .pane { height: 100%; overflow-y: auto; @@ -11,99 +5,6 @@ position: relative; background: #000; flex-grow: 1; - - .header, - .footer { - padding: 1em; - } - - .header { - display: flex; - position: relative; - - .title { - font-size: @medium-font; - color: rgba(255, 255, 255, 0.5); - margin: 0 - } - - .subtitle { - margin: 0; - font-size: @big-font; - } - - &>div:first-child { - margin-right: auto; - } - - &.loading:after{ - display: block; - position: absolute; - content: ""; - left: -20em; - width: 20em; - height: 4px; - bottom: 0; - background-color: #FFEB3B; - animation: loading 2s linear infinite; - } - } - - .main { - padding-bottom: 5.25em; - } - - &.footerless .main { - padding-bottom: 0; - } - - .footer { - height: 5.25em; - display: flex; - background: linear-gradient(to top, #000000 0%, #000000 45%, transparent 100%); - position: fixed; - bottom: 0; - right: 0.5em; - padding-top: 3em; - box-sizing: border-box; - width: 100%; - - .right { - margin-left: auto; - } - - .button { - margin-left: 0.5em; - } - - .button, - input[type="text"] { - font-size: @normal-font; - margin-top: -0.5em; - } - } -} - -.pane.translucent { - .footer>*:not(.always-on), - .main>*:not(.always-on) { - opacity: 0.2; - pointer-events: none; - } -} - -.light .pane { - background: #fff; - - .header { - .title { - color: rgba(0, 0, 0, 0.5); - } - } - - .footer { - background: linear-gradient(to top, #fff 0%, #fff 45%, transparent 100%); - } } @keyframes loading { @@ -126,4 +27,18 @@ to { left: 100%; } -} \ No newline at end of file +} + +.pane.has-footer > .main { + padding-bottom: 5.25em; +} + +.pane.translucent .footer>*:not(.always-on), +.pane.translucent .main>*:not(.always-on) { + opacity: 0.2; + pointer-events: none; +} + +.light .pane { + background: #fff; +} diff --git a/src/styles/panes/node.less b/src/styles/panes/Info.less similarity index 94% rename from src/styles/panes/node.less rename to src/styles/panes/Info.less index 983bcaf2a..77ab0749b 100644 --- a/src/styles/panes/node.less +++ b/src/styles/panes/Info.less @@ -1,6 +1,6 @@ @import '../variables.less'; -.node { +.info { .sharing { padding: 0 1em 1em; p { @@ -21,7 +21,7 @@ } } -.light .node { +.light .info { .sharing { p:last-of-type { font-size: @medium-font; diff --git a/src/styles/screens/menubar.less b/src/styles/screens/menubar.less deleted file mode 100644 index 8f3d0c3c9..000000000 --- a/src/styles/screens/menubar.less +++ /dev/null @@ -1,63 +0,0 @@ -#menubar .pane { - width: calc(~"100vw - 4em"); - - .footer { - width: calc(~"100vw - 4.5em"); - } -} - -.menu { - display: flex; - background: #292929; - flex-direction: column; -} - -.light .menu { - background: #eee; -} - -.menu-option { - line-height: 1; - border: 0; - outline: 0; - margin: 0; - padding: 0.5em; - background: transparent; - color: rgba(255, 255, 255, 0.4); - cursor: pointer; - transition: .2s ease all; - width: 4em; - height: 4em; - - p { - margin: 0.8em 0 0; - font-size: 0.8em; - } - - &:hover { - color: rgba(255, 255, 255, 0.6); - } - - &.active { - background: #000; - color: #fff; - } -} - -.light .menu-option { - color: #212121; - - &:hover { - background: rgba(0, 0, 0, 0.1); - } - - &.active { - background: #000; - color: #fff; - } -} - -.menu-option:last-child { - margin-top: auto; -} - diff --git a/src/utils/file-history.js b/src/utils/file-history.js deleted file mode 100644 index c2a36e8f0..000000000 --- a/src/utils/file-history.js +++ /dev/null @@ -1,27 +0,0 @@ -import FileStore from './file-store' - -/** - * Is a File History. - * @extends FileStore - */ -export default class FileHistory extends FileStore { - constructor (location) { - super(location, []) - } - - /** - * Adds a file to the history. - * @param {String} name - file name - * @param {String} hash - file hash - * @returns {Void} - */ - add (name, hash) { - this.data.unshift({ - name: name, - hash: hash, - date: new Date() - }) - - this.write() - } -} diff --git a/src/views/menubar.html b/src/views/menubar.html index b79d081ae..ed7f65cfd 100644 --- a/src/views/menubar.html +++ b/src/views/menubar.html @@ -5,6 +5,6 @@