├── dist └── .gitignore ├── tests ├── fixtures │ ├── hello.txt │ └── dat.json ├── utils │ ├── wait.js │ ├── waitForVisible.js │ ├── waitForAndClick.js │ └── waitForMatch.js └── index.js ├── app ├── consts │ ├── env.js │ ├── screen.js │ └── state.js ├── containers │ ├── table.js │ ├── status-bar.js │ ├── dat-import.js │ ├── intro.js │ ├── header.js │ ├── table-row.js │ ├── dialog.js │ ├── inspect.js │ └── drag-drop.js ├── components │ ├── icon.js │ ├── status-bar.js │ ├── finder-button.js │ ├── file-list.js │ ├── app.js │ ├── hex-content.js │ ├── empty.js │ ├── table.js │ ├── dat-import.js │ ├── header.js │ ├── button.js │ ├── status.js │ ├── title-field.js │ ├── intro.js │ ├── inspect.js │ ├── table-row.js │ └── dialog.js ├── index.js ├── actions │ ├── index.js │ └── dat-middleware.js └── reducers │ └── index.js ├── unit-tests ├── _helpers │ ├── mockWindow.js │ └── enzymeSetup.js ├── dat-import.js ├── status-bar.js ├── table.js ├── file-list.js ├── hex-content.js ├── title-field.js ├── intro.js ├── status.js ├── inspect.js └── table-row.js ├── assets ├── screenshot.png ├── intro-2.svg ├── intro-3.svg ├── dotted-lines.svg ├── table-skeleton.svg ├── intro-1.svg ├── logo-dat-desktop.svg ├── intro-4.svg ├── intro-6.svg └── intro-5.svg ├── static ├── fonts │ ├── SourceCodePro-Regular.ttf │ └── SourceSansPro-Regular.ttf └── base.css ├── dev └── react-dev-tools │ ├── icons │ ├── 128-deadcode.png │ ├── 128-disabled.png │ ├── 128-outdated.png │ ├── 16-deadcode.png │ ├── 16-disabled.png │ ├── 16-outdated.png │ ├── 32-deadcode.png │ ├── 32-disabled.png │ ├── 32-outdated.png │ ├── 48-deadcode.png │ ├── 48-disabled.png │ ├── 48-outdated.png │ ├── 128-production.png │ ├── 128-unminified.png │ ├── 16-development.png │ ├── 16-production.png │ ├── 16-unminified.png │ ├── 32-development.png │ ├── 32-production.png │ ├── 32-unminified.png │ ├── 48-development.png │ ├── 48-production.png │ ├── 48-unminified.png │ ├── 128-development.png │ ├── outdated.svg │ ├── disabled.svg │ ├── production.svg │ ├── deadcode.svg │ └── development.svg │ ├── main.html │ ├── popups │ ├── production.html │ ├── disabled.html │ ├── shared.js │ ├── outdated.html │ ├── development.html │ ├── unminified.html │ └── deadcode.html │ ├── panel.html │ ├── manifest.json │ ├── build │ ├── contentScript.js │ ├── main.js │ ├── background.js │ └── inject.js │ └── _metadata │ └── verified_contents.json ├── .gitignore ├── .babelrc ├── .travis.yml ├── appveyor.yml ├── index.html ├── webpack.config.js ├── LICENSE ├── preload.js ├── .github ├── workflows │ └── nodejs.yml └── PULL_REQUEST_TEMPLATE.md ├── CONTRIBUTING.md ├── lib └── auto-updater.js ├── README.md ├── CHANGELOG.md ├── index.js └── package.json /dist/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /tests/fixtures/hello.txt: -------------------------------------------------------------------------------- 1 | mundo 2 | -------------------------------------------------------------------------------- /tests/fixtures/dat.json: -------------------------------------------------------------------------------- 1 | {"title": "hello world", "author": "karissa"} 2 | -------------------------------------------------------------------------------- /app/consts/env.js: -------------------------------------------------------------------------------- 1 | export const DAT_ENV = window.DAT_ENV || {} // in test, attach the blank object. 2 | -------------------------------------------------------------------------------- /unit-tests/_helpers/mockWindow.js: -------------------------------------------------------------------------------- 1 | global.window = { 2 | addEventListener: function () {} 3 | } 4 | -------------------------------------------------------------------------------- /assets/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dat-ecosystem-archive/dat-desktop/HEAD/assets/screenshot.png -------------------------------------------------------------------------------- /static/fonts/SourceCodePro-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dat-ecosystem-archive/dat-desktop/HEAD/static/fonts/SourceCodePro-Regular.ttf -------------------------------------------------------------------------------- /static/fonts/SourceSansPro-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dat-ecosystem-archive/dat-desktop/HEAD/static/fonts/SourceSansPro-Regular.ttf -------------------------------------------------------------------------------- /unit-tests/_helpers/enzymeSetup.js: -------------------------------------------------------------------------------- 1 | import configure from 'enzyme-adapter-react-helper' 2 | 3 | configure({ disableLifecycleMethods: true }) 4 | -------------------------------------------------------------------------------- /dev/react-dev-tools/icons/128-deadcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dat-ecosystem-archive/dat-desktop/HEAD/dev/react-dev-tools/icons/128-deadcode.png -------------------------------------------------------------------------------- /dev/react-dev-tools/icons/128-disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dat-ecosystem-archive/dat-desktop/HEAD/dev/react-dev-tools/icons/128-disabled.png -------------------------------------------------------------------------------- /dev/react-dev-tools/icons/128-outdated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dat-ecosystem-archive/dat-desktop/HEAD/dev/react-dev-tools/icons/128-outdated.png -------------------------------------------------------------------------------- /dev/react-dev-tools/icons/16-deadcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dat-ecosystem-archive/dat-desktop/HEAD/dev/react-dev-tools/icons/16-deadcode.png -------------------------------------------------------------------------------- /dev/react-dev-tools/icons/16-disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dat-ecosystem-archive/dat-desktop/HEAD/dev/react-dev-tools/icons/16-disabled.png -------------------------------------------------------------------------------- /dev/react-dev-tools/icons/16-outdated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dat-ecosystem-archive/dat-desktop/HEAD/dev/react-dev-tools/icons/16-outdated.png -------------------------------------------------------------------------------- /dev/react-dev-tools/icons/32-deadcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dat-ecosystem-archive/dat-desktop/HEAD/dev/react-dev-tools/icons/32-deadcode.png -------------------------------------------------------------------------------- /dev/react-dev-tools/icons/32-disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dat-ecosystem-archive/dat-desktop/HEAD/dev/react-dev-tools/icons/32-disabled.png -------------------------------------------------------------------------------- /dev/react-dev-tools/icons/32-outdated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dat-ecosystem-archive/dat-desktop/HEAD/dev/react-dev-tools/icons/32-outdated.png -------------------------------------------------------------------------------- /dev/react-dev-tools/icons/48-deadcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dat-ecosystem-archive/dat-desktop/HEAD/dev/react-dev-tools/icons/48-deadcode.png -------------------------------------------------------------------------------- /dev/react-dev-tools/icons/48-disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dat-ecosystem-archive/dat-desktop/HEAD/dev/react-dev-tools/icons/48-disabled.png -------------------------------------------------------------------------------- /dev/react-dev-tools/icons/48-outdated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dat-ecosystem-archive/dat-desktop/HEAD/dev/react-dev-tools/icons/48-outdated.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dmgs 4 | npm-debug.log 5 | build/background.tiff 6 | .dat 7 | tags 8 | yarn.lock 9 | static/bundle.js 10 | -------------------------------------------------------------------------------- /dev/react-dev-tools/icons/128-production.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dat-ecosystem-archive/dat-desktop/HEAD/dev/react-dev-tools/icons/128-production.png -------------------------------------------------------------------------------- /dev/react-dev-tools/icons/128-unminified.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dat-ecosystem-archive/dat-desktop/HEAD/dev/react-dev-tools/icons/128-unminified.png -------------------------------------------------------------------------------- /dev/react-dev-tools/icons/16-development.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dat-ecosystem-archive/dat-desktop/HEAD/dev/react-dev-tools/icons/16-development.png -------------------------------------------------------------------------------- /dev/react-dev-tools/icons/16-production.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dat-ecosystem-archive/dat-desktop/HEAD/dev/react-dev-tools/icons/16-production.png -------------------------------------------------------------------------------- /dev/react-dev-tools/icons/16-unminified.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dat-ecosystem-archive/dat-desktop/HEAD/dev/react-dev-tools/icons/16-unminified.png -------------------------------------------------------------------------------- /dev/react-dev-tools/icons/32-development.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dat-ecosystem-archive/dat-desktop/HEAD/dev/react-dev-tools/icons/32-development.png -------------------------------------------------------------------------------- /dev/react-dev-tools/icons/32-production.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dat-ecosystem-archive/dat-desktop/HEAD/dev/react-dev-tools/icons/32-production.png -------------------------------------------------------------------------------- /dev/react-dev-tools/icons/32-unminified.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dat-ecosystem-archive/dat-desktop/HEAD/dev/react-dev-tools/icons/32-unminified.png -------------------------------------------------------------------------------- /dev/react-dev-tools/icons/48-development.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dat-ecosystem-archive/dat-desktop/HEAD/dev/react-dev-tools/icons/48-development.png -------------------------------------------------------------------------------- /dev/react-dev-tools/icons/48-production.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dat-ecosystem-archive/dat-desktop/HEAD/dev/react-dev-tools/icons/48-production.png -------------------------------------------------------------------------------- /dev/react-dev-tools/icons/48-unminified.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dat-ecosystem-archive/dat-desktop/HEAD/dev/react-dev-tools/icons/48-unminified.png -------------------------------------------------------------------------------- /dev/react-dev-tools/icons/128-development.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dat-ecosystem-archive/dat-desktop/HEAD/dev/react-dev-tools/icons/128-development.png -------------------------------------------------------------------------------- /dev/react-dev-tools/main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/consts/screen.js: -------------------------------------------------------------------------------- 1 | export const INTRO = 'intro' 2 | export const DATS = 'dats' 3 | export const DOWNLOAD = 'download' 4 | export const INSPECT = 'inspect' 5 | 6 | export default { 7 | INTRO, 8 | DATS, 9 | DOWNLOAD, 10 | INSPECT 11 | } 12 | -------------------------------------------------------------------------------- /tests/utils/wait.js: -------------------------------------------------------------------------------- 1 | // Returns a promise that resolves after 'ms' milliseconds. Default: 1 second 2 | module.exports = function wait (ms) { 3 | ms = ms || 3000 4 | return new Promise(function (resolve, reject) { 5 | setTimeout(resolve, ms) 6 | }) 7 | } 8 | -------------------------------------------------------------------------------- /tests/utils/waitForVisible.js: -------------------------------------------------------------------------------- 1 | module.exports = function waitForVisible (t, app, selector, ms, reverse) { 2 | return app.client.waitForVisible(selector, ms, reverse).then(() => { 3 | t.ok(true, selector + ' exists.') 4 | return selector 5 | }) 6 | } 7 | -------------------------------------------------------------------------------- /assets/intro-2.svg: -------------------------------------------------------------------------------- 1 | intro-2 -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-react" 4 | ], 5 | "env": { 6 | "test": { 7 | "plugins": [ 8 | "@babel/plugin-transform-modules-commonjs", 9 | "@babel/plugin-transform-classes", 10 | "@babel/plugin-proposal-object-rest-spread" 11 | ] 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/utils/waitForAndClick.js: -------------------------------------------------------------------------------- 1 | const waitForVisible = require('./waitForVisible') 2 | 3 | module.exports = function waitForAndClick (t, app, selector, ms, reverse) { 4 | return waitForVisible(t, app, selector, ms, reverse) 5 | .then(selector => { 6 | return app.client.click(selector) 7 | }) 8 | .then(() => t.ok(true, selector + ' clicked.')) 9 | } 10 | -------------------------------------------------------------------------------- /app/containers/table.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import SCREEN from '../consts/screen' 4 | import Table from '../components/table' 5 | import { connect } from 'react-redux' 6 | 7 | const mapStateToProps = state => ({ 8 | dats: state.dats, 9 | show: state.screen === SCREEN.DATS 10 | }) 11 | 12 | const TableContainer = connect(mapStateToProps, null)(Table) 13 | 14 | export default TableContainer 15 | -------------------------------------------------------------------------------- /app/containers/status-bar.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import StatusBar from '../components/status-bar' 4 | import { connect } from 'react-redux' 5 | 6 | const mapStateToProps = state => ({ 7 | up: state.speed.up, 8 | down: state.speed.down, 9 | show: true 10 | }) 11 | 12 | const mapDispatchToProps = dispatch => ({}) 13 | 14 | const StatusBarContainer = connect(mapStateToProps, mapDispatchToProps)( 15 | StatusBar 16 | ) 17 | 18 | export default StatusBarContainer 19 | -------------------------------------------------------------------------------- /app/components/icon.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import React from 'react' 4 | import styled from 'styled-components' 5 | 6 | const Svg = styled.svg` 7 | display: block; 8 | fill: currentColor; 9 | ` 10 | 11 | const Icon = ({ name, ...props }) => ( 12 | 17 | 18 | 19 | ) 20 | 21 | export default Icon 22 | -------------------------------------------------------------------------------- /unit-tests/dat-import.js: -------------------------------------------------------------------------------- 1 | import test from 'tape' 2 | import React from 'react' 3 | import { shallow } from 'enzyme' 4 | import DatImport from '../app/components/dat-import' 5 | import Icon from '../app/components/icon' 6 | 7 | test('dat import should render input element and icon', t => { 8 | const wrapper = shallow( {}} />) 9 | 10 | t.equal(wrapper.find('input').length, 1) 11 | t.equal(wrapper.find(Icon).length, 1) 12 | 13 | t.end() 14 | }) 15 | -------------------------------------------------------------------------------- /dev/react-dev-tools/popups/production.html: -------------------------------------------------------------------------------- 1 | 2 | 17 |

18 | This page is using the production build of React. ✅ 19 |
20 | Open the developer tools, and the React tab will appear to the right. 21 |

22 | -------------------------------------------------------------------------------- /assets/intro-3.svg: -------------------------------------------------------------------------------- 1 | intro-3 -------------------------------------------------------------------------------- /dev/react-dev-tools/popups/disabled.html: -------------------------------------------------------------------------------- 1 | 2 | 17 |

18 | This page doesn’t appear to be using React. 19 |
20 | If this seems wrong, follow the troubleshooting instructions. 21 |

22 | -------------------------------------------------------------------------------- /app/containers/dat-import.js: -------------------------------------------------------------------------------- 1 | import DatImport from '../components/dat-import' 2 | import { requestDownload, downloadSparseDat } from '../actions' 3 | import { connect } from 'react-redux' 4 | 5 | const mapStateToProps = state => state 6 | 7 | const mapDispatchToProps = dispatch => { 8 | return { 9 | requestDownload: key => dispatch(requestDownload(key)), 10 | downloadSparseDat: key => dispatch(downloadSparseDat(key)) 11 | } 12 | } 13 | 14 | const DatImportContainer = connect(mapStateToProps, mapDispatchToProps)( 15 | DatImport 16 | ) 17 | 18 | export default DatImportContainer 19 | -------------------------------------------------------------------------------- /app/consts/state.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import SCREEN from '../consts/screen' 4 | 5 | export const generateDefaultState = () => ({ 6 | dats: {}, 7 | screen: SCREEN.INTRO, 8 | dialogs: { 9 | link: { 10 | link: null, 11 | copied: false 12 | }, 13 | delete: { 14 | dat: null 15 | } 16 | }, 17 | speed: { 18 | up: 0, 19 | down: 0 20 | }, 21 | inspect: { 22 | key: null 23 | }, 24 | intro: { 25 | screen: 1 26 | }, 27 | version: require('../../package.json').version, 28 | menu: { 29 | visible: false 30 | }, 31 | downloadDatKey: null 32 | }) 33 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | cache: bundler 3 | sudo: false 4 | 5 | language: node_js 6 | node_js: 7 | - 12.4 8 | 9 | os: 10 | - osx 11 | - linux 12 | 13 | notifications: 14 | email: false 15 | 16 | addons: 17 | apt: 18 | packages: 19 | - icnsutils 20 | - graphicsmagick 21 | - xz-utils 22 | - xvfb 23 | 24 | env: 25 | - DISPLAY=":99.0" 26 | 27 | install: 28 | - Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & 29 | - npm install 30 | - npm run build:prod 31 | 32 | deploy: 33 | provider: script 34 | script: npm run dist:publish 35 | on: 36 | branch: master 37 | skip_cleanup: true 38 | -------------------------------------------------------------------------------- /app/components/status-bar.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import bytes from 'prettier-bytes' 4 | 5 | const StatusBar = styled.footer` 6 | width: 100%; 7 | min-width: 800px; 8 | height: 2.5rem; 9 | padding: 0.25rem 0.75rem; 10 | background-color: var(--color-neutral-04); 11 | color: var(--color-neutral-60); 12 | ` 13 | 14 | export default function ({ up, down, show }) { 15 | if (!show) return null 16 | 17 | return ( 18 | 19 | Download: {bytes(down)}/s 20 | Upload: {bytes(up)}/s 21 | 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /unit-tests/status-bar.js: -------------------------------------------------------------------------------- 1 | import test from 'tape' 2 | import React from 'react' 3 | import { shallow } from 'enzyme' 4 | import StatusBar from '../app/components/status-bar' 5 | 6 | test('status bar should render empty when show is false', t => { 7 | const wrapper = shallow() 8 | 9 | t.equal(wrapper.isEmptyRender(), true) 10 | 11 | t.end() 12 | }) 13 | 14 | test('status bar should render a Bar with two childrens when show is true', t => { 15 | const show = true 16 | const wrapper = shallow() 17 | 18 | t.equal(wrapper.find('span').length, 2) 19 | 20 | t.end() 21 | }) 22 | -------------------------------------------------------------------------------- /app/containers/intro.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import SCREEN from '../consts/screen' 4 | import { connect } from 'react-redux' 5 | import IntroScreen from '../components/intro' 6 | import { openHomepage, nextIntro, hideIntro } from '../actions' 7 | 8 | const mapStateToProps = state => ({ 9 | show: state.screen === SCREEN.INTRO, 10 | screen: state.intro.screen 11 | }) 12 | 13 | const mapDispatchToProps = dispatch => ({ 14 | openHomepage: () => openHomepage(), 15 | next: screen => dispatch(nextIntro(screen)), 16 | hide: () => dispatch(hideIntro()) 17 | }) 18 | 19 | const IntroContainer = connect(mapStateToProps, mapDispatchToProps)(IntroScreen) 20 | 21 | export default IntroContainer 22 | -------------------------------------------------------------------------------- /app/containers/header.js: -------------------------------------------------------------------------------- 1 | import Header from '../components/header' 2 | import { createDat, toggleMenu } from '../actions' 3 | import { connect } from 'react-redux' 4 | import { shell } from 'electron' 5 | 6 | const mapStateToProps = state => ({ 7 | menuVisible: state.menu.visible, 8 | version: state.version 9 | }) 10 | 11 | const mapDispatchToProps = dispatch => ({ 12 | onShare: () => dispatch(createDat()), 13 | onMenu: visible => dispatch(toggleMenu(visible)), 14 | onReport: () => 15 | shell.openExternal('https://github.com/dat-land/dat-desktop/issues/') 16 | }) 17 | 18 | const HeaderContainer = connect(mapStateToProps, mapDispatchToProps)(Header) 19 | 20 | export default HeaderContainer 21 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | ELECTRON_ENABLE_STACK_DUMPING: 'true' 3 | DEBUG: '*,-nugget*,-eslint*,-extract-zip*,-sumchecker*,-electron-download*,-dependency-check*' 4 | matrix: 5 | - nodejs_version: "8" 6 | 7 | platform: 8 | - x86 9 | - x64 10 | 11 | install: 12 | - ps: Install-Product node $env:nodejs_version $env:platform 13 | - npm install 14 | - node --version 15 | - npm --version 16 | 17 | test_script: 18 | - npm run build:prod 19 | - npm test 20 | 21 | # Don't actually build. 22 | build: off 23 | 24 | # RDP for debugging purposes 25 | init: 26 | - ps: iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1')) -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Dat Desktop 6 | 7 | 8 | 9 | 10 | 11 |
12 | 19 | 20 | -------------------------------------------------------------------------------- /assets/dotted-lines.svg: -------------------------------------------------------------------------------- 1 | dotted-lines 2 | -------------------------------------------------------------------------------- /dev/react-dev-tools/popups/shared.js: -------------------------------------------------------------------------------- 1 | /* globals chrome */ 2 | 3 | document.addEventListener('DOMContentLoaded', function() { 4 | // Make links work 5 | var links = document.getElementsByTagName('a'); 6 | for (var i = 0; i < links.length; i++) { 7 | (function() { 8 | var ln = links[i]; 9 | var location = ln.href; 10 | ln.onclick = function() { 11 | chrome.tabs.create({active: true, url: location}); 12 | }; 13 | })(); 14 | } 15 | 16 | // Work around https://bugs.chromium.org/p/chromium/issues/detail?id=428044 17 | document.body.style.opacity = 0; 18 | document.body.style.transition = 'opacity ease-out .4s'; 19 | requestAnimationFrame(function() { 20 | document.body.style.opacity = 1; 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /dev/react-dev-tools/popups/outdated.html: -------------------------------------------------------------------------------- 1 | 2 | 17 |

18 | This page is using an outdated version of React. ⌛ 19 |

20 |

21 | We recommend updating React to ensure that you receive important bugfixes and performance improvements. 22 |
23 |
24 | You can find the upgrade instructions on the React blog. 25 |

26 |
27 |

28 | Open the developer tools, and the React tab will appear to the right. 29 |

30 | -------------------------------------------------------------------------------- /dev/react-dev-tools/popups/development.html: -------------------------------------------------------------------------------- 1 | 2 | 17 |

18 | This page is using the development build of React. 🚧 19 |

20 |

21 | Note that the development build is not suitable for production. 22 |
23 | Make sure to use the production build before deployment. 24 |

25 |
26 |

27 | Open the developer tools, and the React tab will appear to the right. 28 |

29 | -------------------------------------------------------------------------------- /app/components/finder-button.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import React from 'react' 4 | import * as Button from './button' 5 | import Icon from './icon' 6 | import { resolve } from 'path' 7 | import { shell } from 'electron' 8 | import { DAT_ENV } from '../consts/env' 9 | 10 | const alt = 11 | { 12 | darwin: 'Finder', 13 | win32: 'Explorer' 14 | }[DAT_ENV.platform] || 'FileManager' 15 | 16 | const FinderButton = ({ dat, onClick }) => ( 17 | } 19 | className='row-action btn-finder' 20 | title={`Open in ${alt}`} 21 | onClick={ev => { 22 | ev.stopPropagation() 23 | shell.openExternal(`file://${resolve(dat.path)}`, () => {}) 24 | }} 25 | /> 26 | ) 27 | 28 | export default FinderButton 29 | -------------------------------------------------------------------------------- /app/containers/table-row.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import { connect } from 'react-redux' 4 | import TableRow from '../components/table-row' 5 | import { 6 | shareDat, 7 | deleteDat, 8 | togglePause, 9 | inspectDat, 10 | updateTitle 11 | } from '../actions' 12 | 13 | const mapStateToProps = (state, ownProps) => ({ 14 | dat: ownProps.dat 15 | }) 16 | 17 | const mapDispatchToProps = dispatch => ({ 18 | shareDat: link => dispatch(shareDat(link)), 19 | onDeleteDat: key => dispatch(deleteDat(key)), 20 | inspectDat: key => dispatch(inspectDat(key)), 21 | onTogglePause: dat => dispatch(togglePause(dat)), 22 | updateTitle: (key, title) => dispatch(updateTitle(key, title)) 23 | }) 24 | 25 | const TableRowContainer = connect(mapStateToProps, mapDispatchToProps)(TableRow) 26 | 27 | export default TableRowContainer 28 | -------------------------------------------------------------------------------- /dev/react-dev-tools/popups/unminified.html: -------------------------------------------------------------------------------- 1 | 2 | 17 |

18 | This page is using an unminified build of React. 🚧 19 |

20 |

21 | The React build on this page appears to be unminified. 22 |
23 | This makes its size larger, and causes React to run slower. 24 |
25 |
26 | Make sure to set up minification before deployment. 27 |

28 |
29 |

30 | Open the developer tools, and the React tab will appear to the right. 31 |

32 | -------------------------------------------------------------------------------- /unit-tests/table.js: -------------------------------------------------------------------------------- 1 | import test from 'tape' 2 | import React from 'react' 3 | import { shallow } from 'enzyme' 4 | import Table from '../app/components/table' 5 | import TableRowContainer from '../app/containers/table-row' 6 | 7 | test('table should render columns (Link, Status, Size, Peers)', t => { 8 | const show = true 9 | const wrapper = shallow() 10 | 11 | const columns = wrapper.find('.tl').map(node => node.text().toLowerCase()) 12 | 13 | t.deepLooseEqual(columns.sort(), ['link', 'status', 'size', 'peers'].sort()) 14 | 15 | t.end() 16 | }) 17 | 18 | test('table should render same number of table rows as given dats', t => { 19 | const show = true 20 | const wrapper = shallow(
) 21 | 22 | t.equal(wrapper.find(TableRowContainer).length, 3) 23 | 24 | t.end() 25 | }) 26 | -------------------------------------------------------------------------------- /dev/react-dev-tools/panel.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 26 | 27 | 28 | 29 |
Unable to find React on the page.
30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /dev/react-dev-tools/popups/deadcode.html: -------------------------------------------------------------------------------- 1 | 2 | 17 |

18 | This page includes an extra development build of React. 🚧 19 |

20 |

21 | The React build on this page includes both development and production versions because dead code elimination has not been applied correctly. 22 |
23 |
24 | This makes its size larger, and causes React to run slower. 25 |
26 |
27 | Make sure to set up dead code elimination before deployment. 28 |

29 |
30 |

31 | Open the developer tools, and the React tab will appear to the right. 32 |

33 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const nodeExternals = require('webpack-node-externals') 2 | const path = require('path') 3 | module.exports = (_, argv) => ({ 4 | entry: path.normalize(`${__dirname}/app/index.js`), 5 | target: 'electron-main', 6 | externals: [nodeExternals({ 7 | whitelist: /react-file-drop/ 8 | })], 9 | output: { 10 | path: path.normalize(`${__dirname}/static`), 11 | filename: 'bundle.js', 12 | libraryTarget: 'commonjs2' 13 | }, 14 | optimization: { 15 | nodeEnv: argv.mode 16 | }, 17 | devtool: 'inline-source-map', 18 | node: { 19 | __dirname: true 20 | }, 21 | module: { 22 | rules: [ 23 | { 24 | test: /\.js$/, 25 | include: path.normalize(`${__dirname}/app`), 26 | loader: 'babel-loader', 27 | query: { 28 | presets: ['@babel/preset-react'], 29 | plugins: [ 30 | '@babel/plugin-transform-modules-commonjs' 31 | ] 32 | } 33 | } 34 | ] 35 | } 36 | }) 37 | -------------------------------------------------------------------------------- /app/containers/dialog.js: -------------------------------------------------------------------------------- 1 | import { Link, Confirm, Alert } from '../components/dialog' 2 | import { 3 | copyLink, 4 | closeShareDat, 5 | confirmDeleteDat, 6 | cancelDeleteDat, 7 | closeAlert 8 | } from '../actions' 9 | import { connect } from 'react-redux' 10 | 11 | export const LinkContainer = connect( 12 | state => ({ 13 | link: state.dialogs.link.link, 14 | copied: state.dialogs.link.copied 15 | }), 16 | dispatch => ({ 17 | onCopy: link => dispatch(copyLink(link)), 18 | onExit: () => dispatch(closeShareDat()) 19 | }) 20 | )(Link) 21 | 22 | export const ConfirmContainer = connect( 23 | state => ({ 24 | dat: state.dialogs.delete.dat 25 | }), 26 | dispatch => ({ 27 | onConfirm: dat => dispatch(confirmDeleteDat(dat)), 28 | onExit: () => dispatch(cancelDeleteDat()) 29 | }) 30 | )(Confirm) 31 | 32 | export const AlertContainer = connect( 33 | state => ({ 34 | alert: state.dialogs.alert 35 | }), 36 | dispatch => ({ 37 | onExit: () => dispatch(closeAlert()) 38 | }) 39 | )(Alert) 40 | -------------------------------------------------------------------------------- /app/containers/inspect.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import SCREEN from '../consts/screen' 4 | import Inspect from '../components/inspect' 5 | 6 | import { 7 | closeInspectDat, 8 | addDat, 9 | hideDownloadScreen, 10 | cancelDownloadDat, 11 | changeDownloadPath 12 | } from '../actions' 13 | 14 | import { connect } from 'react-redux' 15 | 16 | const mapStateToProps = state => ({ 17 | dat: 18 | state.screen === SCREEN.INSPECT 19 | ? state.dats[state.inspect.key] 20 | : state.screen === SCREEN.DOWNLOAD 21 | ? state.dats[state.downloadDatKey] 22 | : null, 23 | screen: state.screen 24 | }) 25 | 26 | const mapDispatchToProps = dispatch => ({ 27 | closeInspectDat: () => dispatch(closeInspectDat()), 28 | addDat: ({ key, path }) => dispatch(addDat({ key, path })), 29 | hideDownloadScreen: () => dispatch(hideDownloadScreen()), 30 | cancelDownloadDat: key => dispatch(cancelDownloadDat(key)), 31 | changeDownloadPath: key => dispatch(changeDownloadPath(key)) 32 | }) 33 | 34 | const InspectContainer = connect(mapStateToProps, mapDispatchToProps)(Inspect) 35 | 36 | export default InspectContainer 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Dat Project 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/utils/waitForMatch.js: -------------------------------------------------------------------------------- 1 | var wait = require('./wait') 2 | 3 | module.exports = function waitForMatch (t, app, selector, regexp, ms, reverse) { 4 | if (reverse) { 5 | reverse = true 6 | } else { 7 | reverse = false 8 | } 9 | if (!ms) { 10 | ms = 15000 11 | } 12 | var lastValue 13 | var end = Date.now() + ms 14 | function check () { 15 | if (Date.now() > end) { 16 | return Promise.reject( 17 | new Error( 18 | `Timeout after ${ms}ms tryin to match "${selector}" with ${String( 19 | regexp 20 | )}; last value: ${lastValue}` 21 | ) 22 | ) 23 | } 24 | return app.client 25 | .getText(selector) 26 | .then(function (text) { 27 | lastValue = text 28 | var match = regexp.test(text) ? !reverse : reverse 29 | if (!match) { 30 | return Promise.reject(new Error('no-match')) 31 | } 32 | t.ok(true, '"' + selector + '" matches ' + String(regexp)) 33 | return Promise.resolve(text) 34 | }) 35 | .catch(function (e) { 36 | return wait(100).then(check) 37 | }) 38 | } 39 | return check() 40 | } 41 | -------------------------------------------------------------------------------- /app/containers/drag-drop.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import FileDrop from 'react-file-drop' 4 | import { connect } from 'react-redux' 5 | import { dropFolder } from '../actions' 6 | import Icon from '../components/icon' 7 | 8 | const mapStateToProps = state => ({}) 9 | 10 | const mapDispatchToProps = dispatch => ({ 11 | onDrop: list => dispatch(dropFolder(list[0])) 12 | }) 13 | 14 | const DropFrame = styled(FileDrop)` 15 | .file-drop-target { 16 | position: fixed; 17 | top: 0; 18 | left: 0; 19 | width: 100vw; 20 | height: 100vh; 21 | background: rgba(42, 202, 75, 0.6); 22 | align-items: center; 23 | justify-content: center; 24 | z-index: 1; 25 | display: none; 26 | } 27 | .file-drop-dragging-over-frame { 28 | display: flex; 29 | } 30 | ` 31 | 32 | const DropIcon = styled(Icon)` 33 | width: 128px; 34 | color: white; 35 | ` 36 | 37 | const DragDropContainer = connect(mapStateToProps, mapDispatchToProps)(function ( 38 | props 39 | ) { 40 | return ( 41 | 42 | 43 | 44 | ) 45 | }) 46 | 47 | export default DragDropContainer 48 | -------------------------------------------------------------------------------- /app/components/file-list.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import React from 'react' 4 | import styled from 'styled-components' 5 | import bytes from 'prettier-bytes' 6 | 7 | const FileListTable = styled.table` 8 | width: 100%; 9 | border-collapse: collapse; 10 | tr:nth-child(odd) td { 11 | background-color: var(--color-white); 12 | } 13 | tr:nth-child(even) td { 14 | background-color: var(--color-neutral-10); 15 | } 16 | td { 17 | border: 0; 18 | padding: 0.25rem; 19 | } 20 | td:last-child { 21 | width: 4rem; 22 | text-align: right; 23 | } 24 | ` 25 | 26 | const FileList = ({ dat, fallback = null }) => { 27 | if (!dat || !dat.files || !dat.files.length) return fallback 28 | return ( 29 | 30 | 31 | {dat.files.map(file => { 32 | const size = 33 | Number(file.size) === file.size && file.isFile 34 | ? bytes(file.size) 35 | : '' 36 | return ( 37 | 38 | 39 | 40 | 41 | ) 42 | })} 43 | 44 | 45 | ) 46 | } 47 | 48 | export default FileList 49 | -------------------------------------------------------------------------------- /preload.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | /** 3 | * This file exists for security reasons! 4 | * 5 | * It prepares by removing dangerous scripts from the global scopes 6 | * Before running the app. 7 | * 8 | * See: https://electronjs.org/docs/tutorial/security 9 | * & https://eslint.org/docs/rules/no-implied-eval 10 | */ 11 | const platform = require('os').platform() 12 | window.__defineGetter__('DAT_ENV', () => ({ platform })) 13 | 14 | // eslint-disable-next-line no-eval 15 | window.eval = global.eval = function () { 16 | throw new Error('Sorry, this app does not support window.eval().') 17 | } 18 | const setTimeout = global.setTimeout 19 | window.setTimeout = global.setTimeout = function (fn, ms) { 20 | if (typeof fn !== 'function') { 21 | throw new Error('Sorry, this app does not support setTimeout() with a string') 22 | } 23 | return setTimeout(fn, ms) 24 | } 25 | const setInterval = global.setInterval 26 | window.setInterval = global.setInterval = function (fn, ms) { 27 | if (typeof fn !== 'function') { 28 | throw new Error('Sorry, this app does not support setInterval() with a string') 29 | } 30 | return setInterval(fn, ms) 31 | } 32 | process.once('loaded', () => { 33 | document.addEventListener('DOMContentLoaded', () => require('./static/bundle')) 34 | }) 35 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node CD 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ${{ matrix.os }} 8 | 9 | strategy: 10 | matrix: 11 | os: [windows-latest] 12 | 13 | steps: 14 | - name: Context 15 | env: 16 | GITHUB_CONTEXT: ${{ toJson(github) }} 17 | run: echo "$GITHUB_CONTEXT" 18 | - uses: actions/checkout@v1 19 | with: 20 | fetch-depth: 1 21 | - name: Use Node.js 12.4 22 | uses: actions/setup-node@v1 23 | with: 24 | node-version: 12.4 25 | - name: npm install 26 | run: | 27 | npm install 28 | - name: Test 29 | run: | 30 | npm test 31 | - name: Build frontend 32 | run: | 33 | npm run build:prod 34 | env: 35 | CI: true 36 | - name: Publish 37 | run: | 38 | npm run dist:publish 39 | env: 40 | CI: true 41 | GITHUB_CI_REF: ${{ github.ref }} 42 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 43 | - name: Cleanup artifacts 44 | run: | 45 | npx rimraf@2 'dist/!(*.exe|*.deb|*.AppImage|*.dmg)' 46 | shell: bash 47 | - name: Upload artifacts 48 | uses: actions/upload-artifact@v1 49 | with: 50 | name: ${{ matrix.os }} 51 | path: dist 52 | -------------------------------------------------------------------------------- /app/components/app.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import React, { Fragment } from 'react' 4 | import { connect } from 'react-redux' 5 | 6 | import SCREEN from '../consts/screen' 7 | 8 | import IntroContainer from '../containers/intro' 9 | import HeaderContainer from '../containers/header' 10 | import TableContainer from '../containers/table' 11 | import * as Dialog from '../containers/dialog' 12 | import StatusBarContainer from '../containers/status-bar' 13 | import InspectContainer from '../containers/inspect' 14 | import DragDropContainer from '../containers/drag-drop' 15 | 16 | const mapStateToProps = state => ({ 17 | screen: state.screen 18 | }) 19 | 20 | const mapDispatchToProps = dispatch => ({}) 21 | 22 | export default connect(mapStateToProps, mapDispatchToProps)(function ({ 23 | screen 24 | }) { 25 | if (screen === SCREEN.INTRO) { 26 | return 27 | } 28 | return ( 29 | 30 | {/* header */} 31 | 32 | {/* /header */} 33 | 34 | {/* main */} 35 | 36 | 37 | {/* /main */} 38 | 39 | {/* footer */} 40 | 41 | {/* /footer */} 42 | 43 | 44 | 45 | 46 | 47 | 48 | ) 49 | }) 50 | -------------------------------------------------------------------------------- /dev/react-dev-tools/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "update_url": "https://clients2.google.com/service/update2/crx", 3 | 4 | "manifest_version": 2, 5 | "name": "React Developer Tools", 6 | "description": "Adds React debugging tools to the Chrome Developer Tools.", 7 | "version": "3.4.2", 8 | 9 | "minimum_chrome_version": "49", 10 | 11 | "icons": { 12 | "16": "icons/16-production.png", 13 | "32": "icons/32-production.png", 14 | "48": "icons/48-production.png", 15 | "128": "icons/128-production.png" 16 | }, 17 | 18 | "browser_action": { 19 | "default_icon": { 20 | "16": "icons/16-disabled.png", 21 | "32": "icons/32-disabled.png", 22 | "48": "icons/48-disabled.png", 23 | "128": "icons/128-disabled.png" 24 | }, 25 | 26 | "default_popup": "popups/disabled.html" 27 | }, 28 | 29 | "devtools_page": "main.html", 30 | 31 | "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'", 32 | "web_accessible_resources": [ "main.html", "panel.html", "build/backend.js"], 33 | 34 | "background": { 35 | "scripts": [ "build/background.js" ], 36 | "persistent": false 37 | }, 38 | 39 | "permissions": [ 40 | "file:///*", 41 | "http://*/*", 42 | "https://*/*" 43 | ], 44 | 45 | "content_scripts": [ 46 | { 47 | "matches": [""], 48 | "js": ["build/inject.js"], 49 | "run_at": "document_start" 50 | } 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Hello there! 2 | 3 | Thanks for considering to contribute. Dat Desktop is a tool developed by the open source community, and it’s the time and effort of people like you that makes it better! 4 | 5 | ## What kind of contributions we are looking for 6 | 7 | You can help to improve Dat Desktop by [reporting bugs](https://github.com/datproject/dat-desktop/issues) or helping with existing [issues](https://github.com/datproject/dat-desktop/issues). Help with improving documentation like the [README](https://github.com/datproject/dat-desktop/blob/master/README.md) or the [Wiki](https://github.com/datproject/dat-desktop/wiki) is also welcome. 8 | 9 | ## How to report a problem 10 | 11 | [Check here](https://github.com/datproject/dat-desktop/issues) if the problem has already been reported. If so, you're welcome to add your description of the problem in a comment on the existing issue. 12 | If there is no issue that describes your poblem, go ahead and [open a new issue](https://github.com/datproject/dat-desktop/issues/new). Please include the following information: 13 | 14 | 1. Which version of Dat Desktop are you running? 15 | 2. What operating system and processor architecture are you using? 16 | 3. What did you do? 17 | 4. What did you expect to see? 18 | 5. What did you see instead? 19 | 20 | ## Pull Requests 21 | 22 | Please give 2 week days of time for reviews on each Pull Request, as we are a distributed team of non-fulltime contributors. 23 | -------------------------------------------------------------------------------- /dev/react-dev-tools/icons/outdated.svg: -------------------------------------------------------------------------------- 1 | outdated -------------------------------------------------------------------------------- /unit-tests/file-list.js: -------------------------------------------------------------------------------- 1 | import test from 'tape' 2 | import React from 'react' 3 | import { shallow, render } from 'enzyme' 4 | import FileList from '../app/components/file-list' 5 | 6 | test('file list should render div with class pa2', t => { 7 | const files = [] 8 | const wrapper = shallow( 9 | 14 | ) 15 | 16 | t.equal(wrapper.find('.pa2').length, 0) 17 | 18 | t.end() 19 | }) 20 | 21 | test('file list should render tr(s) equal to number of files in dat', t => { 22 | const files = [ 23 | { 24 | path: '/foo', 25 | size: 30, 26 | isFile: true 27 | }, 28 | { 29 | path: '/bar', 30 | size: 30, 31 | isFile: true 32 | }, 33 | { 34 | path: '/baz', 35 | size: 30, 36 | isFile: false 37 | } 38 | ] 39 | const wrapper = render( 40 | 45 | ) 46 | 47 | t.equal(wrapper.find('tr').length, files.length) 48 | 49 | t.end() 50 | }) 51 | 52 | test('file list should render a tr(s) even if directories without isEditing and size property given', t => { 53 | const files = [ 54 | { 55 | path: '/foo' 56 | }, 57 | { 58 | path: '/bar' 59 | }, 60 | { 61 | path: '/baz' 62 | } 63 | ] 64 | const wrapper = render( 65 | 70 | ) 71 | 72 | t.equal(wrapper.find('tr').length, files.length) 73 | 74 | t.end() 75 | }) 76 | -------------------------------------------------------------------------------- /app/components/hex-content.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Swap from 'react-swap' 3 | import * as Button from './button' 4 | import Icon from './icon' 5 | 6 | const HexContent = ({ dat }) => { 7 | let hex 8 | let onHover 9 | 10 | if (dat.state === 'loading') { 11 | hex = ( 12 | } 14 | className='color-blue hover-color-blue-hover ph0' 15 | /> 16 | ) 17 | } else if (dat.paused) { 18 | hex = ( 19 | } 21 | className='color-neutral-30 hover-color-neutral-40 ph0' 22 | /> 23 | ) 24 | } else if (dat.state === 'complete') { 25 | hex = ( 26 | } 28 | className='color-green hover-color-green-hover ph0' 29 | /> 30 | ) 31 | } else { 32 | hex = ( 33 | } 35 | className='color-neutral-30 hover-color-neutral-40 ph0' 36 | /> 37 | ) 38 | } 39 | 40 | if (!dat.paused) { 41 | onHover = ( 42 | } 44 | className='color-neutral-40 ph0' 45 | /> 46 | ) 47 | } else { 48 | onHover = hex 49 | } 50 | 51 | return ( 52 | 53 |
{hex}
54 |
{onHover}
55 |
56 | ) 57 | } 58 | 59 | export default HexContent 60 | -------------------------------------------------------------------------------- /unit-tests/hex-content.js: -------------------------------------------------------------------------------- 1 | import test from 'tape' 2 | import React from 'react' 3 | import { shallow } from 'enzyme' 4 | import HexContent from '../app/components/hex-content' 5 | 6 | test('hexagon should be blue when loading dat', t => { 7 | const wrapper = shallow( 8 | 14 | ) 15 | 16 | t.equal(wrapper.find('.color-blue.hover-color-blue-hover').length, 2) 17 | 18 | t.end() 19 | }) 20 | 21 | test('hexagon should be neutral colored when dat is paused', t => { 22 | const wrapper = shallow( 23 | 29 | ) 30 | 31 | t.equal(wrapper.find('.color-neutral-30.hover-color-neutral-40').length, 2) 32 | 33 | t.end() 34 | }) 35 | 36 | test('hexagon should be green colored when dat is completed', t => { 37 | const wrapper = shallow( 38 | 44 | ) 45 | 46 | t.equal(wrapper.find('.color-green.hover-color-green-hover').length, 1) 47 | 48 | t.end() 49 | }) 50 | 51 | test('hexagon should be neutral colored when dat is resumed but neighter loading nor completed', t => { 52 | const wrapper = shallow( 53 | 59 | ) 60 | 61 | t.equal(wrapper.find('.color-neutral-30.hover-color-neutral-40').length, 1) 62 | 63 | t.end() 64 | }) 65 | -------------------------------------------------------------------------------- /dev/react-dev-tools/icons/disabled.svg: -------------------------------------------------------------------------------- 1 | disabled -------------------------------------------------------------------------------- /dev/react-dev-tools/icons/production.svg: -------------------------------------------------------------------------------- 1 | production -------------------------------------------------------------------------------- /unit-tests/title-field.js: -------------------------------------------------------------------------------- 1 | import test from 'tape' 2 | import React from 'react' 3 | import { shallow } from 'enzyme' 4 | import TitleField from '../app/components/title-field' 5 | import { 6 | Plain as PlainButton, 7 | Green as GreenButton 8 | } from '../app/components/button' 9 | import Icon from '../app/components/icon' 10 | 11 | test('title field show not be editable if dat is not writable', t => { 12 | const wrapper = shallow() 13 | 14 | t.equal(wrapper.find(PlainButton).length, 0) 15 | t.equal(wrapper.find(GreenButton).length, 0) 16 | 17 | t.end() 18 | }) 19 | 20 | test('title field show edit button if dat is editable and is not editing', t => { 21 | const wrapper = shallow() 22 | 23 | t.equal(wrapper.find(Icon).length, 1) 24 | 25 | t.end() 26 | }) 27 | 28 | test('title field show plain button if title value is equal to input field value when editing', async t => { 29 | const field = 30 | const wrapper = shallow(field) 31 | wrapper.setState({ 32 | editing: true, 33 | modified: false 34 | }) 35 | 36 | t.equal(wrapper.find(PlainButton).length, 1) 37 | t.equal(wrapper.find(GreenButton).length, 0) 38 | 39 | t.end() 40 | }) 41 | 42 | test('title field show green button if title value is not equal to input field value when editing', t => { 43 | const field = 44 | const wrapper = shallow(field) 45 | wrapper.setState({ 46 | editing: true, 47 | modified: true 48 | }) 49 | 50 | t.equal(wrapper.find(PlainButton).length, 0) 51 | t.equal(wrapper.find(GreenButton).length, 1) 52 | 53 | t.end() 54 | }) 55 | -------------------------------------------------------------------------------- /lib/auto-updater.js: -------------------------------------------------------------------------------- 1 | const os = require('os') 2 | const { app, dialog, autoUpdater } = require('electron') 3 | const ms = require('ms') 4 | 5 | module.exports = ({ log }) => { 6 | const onerror = err => err && log(err.stack || err) 7 | 8 | const platform = `${os.platform()}_${os.arch()}` 9 | const version = app.getVersion() 10 | 11 | try { 12 | autoUpdater.setFeedURL(`http://dat.land:6000/update/${platform}/${version}`) 13 | } catch (e) { 14 | onerror(e) 15 | return 16 | } 17 | autoUpdater.on('error', onerror) 18 | autoUpdater.on('checking-for-update', () => log('checking for update')) 19 | autoUpdater.on('update-available', () => 20 | log('update available, downloading…') 21 | ) 22 | autoUpdater.on('update-not-available', () => log('update not available')) 23 | autoUpdater.on('download-progress', p => 24 | log('download progress ' + p.percent) 25 | ) 26 | autoUpdater.once('update-downloaded', (ev, notes, version) => { 27 | log('update downloaded') 28 | 29 | dialog.showMessageBox( 30 | { 31 | type: 'question', 32 | buttons: ['Install and Relaunch', 'Dismiss'], 33 | defaultId: 0, 34 | title: 'A new version of Dat Desktop is ready to install!', 35 | message: `Dat Desktop ${version} has been downloaded and is ready to use! Would you like to install it and relaunch Dat Desktop now?` 36 | }, 37 | res => { 38 | const update = res === 0 39 | if (!update) return log('dismiss') 40 | log('updating…') 41 | autoUpdater.quitAndInstall() 42 | } 43 | ) 44 | }) 45 | 46 | setTimeout(() => autoUpdater.checkForUpdates(), ms('10s')) 47 | setInterval(() => autoUpdater.checkForUpdates(), ms('30m')) 48 | } 49 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | ### First time contributor checklist: 11 | 12 | - [ ] I have read the [README](https://github.com/digidem/mapeo-desktop/blob/master/README.md) 13 | 14 | ### Contributor checklist: 15 | 16 | - [ ] My commits are in nice logical chunks with [good commit messages](http://chris.beams.io/posts/git-commit/) 17 | - [ ] If my changes depend upon an update to a dependency, I have updated the package-lock.json file using `npm install --package-lock` 18 | - [ ] I have used the app myself and all of the buttons work. 19 | - [ ] I have tested the application with [the commandline 20 | tool](https://github.com/datproject/dat). 21 | - [ ] My changes have been tested with the integration tests, and all tests pass. 22 | - [ ] My changes are ready to be shipped to users on Mac and Linux. 23 | 24 | ### Description 25 | 26 | 38 | 39 | -------------------------------------------------------------------------------- /app/components/empty.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | import Icon from './icon' 4 | 5 | const Main = styled.main` 6 | position: relative; 7 | .skeleton { 8 | position: fixed; 9 | top: 3.5rem; 10 | left: 1.25rem; 11 | width: 232px; 12 | max-width: 100vw; 13 | } 14 | .dotted-lines { 15 | position: absolute; 16 | top: 0.25rem; 17 | right: 5.5rem; 18 | width: 17rem; 19 | } 20 | .create-new-dat, 21 | .link { 22 | position: absolute; 23 | width: 15rem; 24 | color: var(--color-neutral-30); 25 | svg { 26 | display: inline-block; 27 | width: 2rem; 28 | height: 2rem; 29 | } 30 | } 31 | .create-new-dat { 32 | top: 14.5rem; 33 | right: 4rem; 34 | } 35 | .link { 36 | top: 6rem; 37 | right: 8.5rem; 38 | svg { 39 | margin-bottom: -0.75rem; 40 | } 41 | } 42 | ` 43 | 44 | const Empty = () => ( 45 |
46 | 47 |
48 | 49 |
50 | 51 |

Download A Dat

52 |

53 | … and keep data up-to-date 54 |
55 | by entering the link here. 56 |

57 |
58 |
59 | 60 |

Share a Folder

61 |

62 | … and sync changes by sharing 63 |
64 | the link with someone else. 65 |

66 |
67 |
68 |
69 | ) 70 | 71 | export default Empty 72 | -------------------------------------------------------------------------------- /dev/react-dev-tools/build/contentScript.js: -------------------------------------------------------------------------------- 1 | !function(modules) { 2 | function __webpack_require__(moduleId) { 3 | if (installedModules[moduleId]) return installedModules[moduleId].exports; 4 | var module = installedModules[moduleId] = { 5 | exports: {}, 6 | id: moduleId, 7 | loaded: !1 8 | }; 9 | return modules[moduleId].call(module.exports, module, module.exports, __webpack_require__), 10 | module.loaded = !0, module.exports; 11 | } 12 | var installedModules = {}; 13 | return __webpack_require__.m = modules, __webpack_require__.c = installedModules, 14 | __webpack_require__.p = "", __webpack_require__(0); 15 | }([ function(module, exports) { 16 | "use strict"; 17 | function handleMessageFromDevtools(message) { 18 | window.postMessage({ 19 | source: "react-devtools-content-script", 20 | payload: message 21 | }, "*"); 22 | } 23 | function handleMessageFromPage(evt) { 24 | evt.source === window && evt.data && "react-devtools-bridge" === evt.data.source && port.postMessage(evt.data.payload); 25 | } 26 | function handleDisconnect() { 27 | window.removeEventListener("message", handleMessageFromPage), window.postMessage({ 28 | source: "react-devtools-content-script", 29 | payload: { 30 | type: "event", 31 | evt: "shutdown" 32 | } 33 | }, "*"); 34 | } 35 | var port = chrome.runtime.connect({ 36 | name: "content-script" 37 | }); 38 | port.onMessage.addListener(handleMessageFromDevtools), port.onDisconnect.addListener(handleDisconnect), 39 | window.addEventListener("message", handleMessageFromPage), window.postMessage({ 40 | source: "react-devtools-content-script", 41 | hello: !0 42 | }, "*"); 43 | } ]); -------------------------------------------------------------------------------- /assets/table-skeleton.svg: -------------------------------------------------------------------------------- 1 | table-skeleton -------------------------------------------------------------------------------- /assets/intro-1.svg: -------------------------------------------------------------------------------- 1 | intro-1 -------------------------------------------------------------------------------- /assets/logo-dat-desktop.svg: -------------------------------------------------------------------------------- 1 | logo-dat-desktop -------------------------------------------------------------------------------- /dev/react-dev-tools/build/main.js: -------------------------------------------------------------------------------- 1 | !function(modules) { 2 | function __webpack_require__(moduleId) { 3 | if (installedModules[moduleId]) return installedModules[moduleId].exports; 4 | var module = installedModules[moduleId] = { 5 | exports: {}, 6 | id: moduleId, 7 | loaded: !1 8 | }; 9 | return modules[moduleId].call(module.exports, module, module.exports, __webpack_require__), 10 | module.loaded = !0, module.exports; 11 | } 12 | var installedModules = {}; 13 | return __webpack_require__.m = modules, __webpack_require__.c = installedModules, 14 | __webpack_require__.p = "", __webpack_require__(0); 15 | }([ function(module, exports) { 16 | "use strict"; 17 | function createPanelIfReactLoaded() { 18 | panelCreated || chrome.devtools.inspectedWindow.eval("!!(\n (window.__REACT_DEVTOOLS_GLOBAL_HOOK__ && Object.keys(window.__REACT_DEVTOOLS_GLOBAL_HOOK__._renderers).length) || window.React\n )", function(pageHasReact, err) { 19 | pageHasReact && !panelCreated && (clearInterval(loadCheckInterval), panelCreated = !0, 20 | chrome.devtools.panels.create("React", "", "panel.html", function(panel) { 21 | var reactPanel = null; 22 | panel.onShown.addListener(function(window) { 23 | window.panel.getNewSelection(), reactPanel = window.panel, reactPanel.resumeTransfer(); 24 | }), panel.onHidden.addListener(function() { 25 | reactPanel && (reactPanel.hideHighlight(), reactPanel.pauseTransfer()); 26 | }); 27 | })); 28 | }); 29 | } 30 | var panelCreated = !1; 31 | chrome.devtools.network.onNavigated.addListener(function() { 32 | createPanelIfReactLoaded(); 33 | }); 34 | var loadCheckInterval = setInterval(function() { 35 | createPanelIfReactLoaded(); 36 | }, 1e3); 37 | createPanelIfReactLoaded(); 38 | } ]); -------------------------------------------------------------------------------- /app/components/table.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import React from 'react' 4 | import styled from 'styled-components' 5 | import TableRowContainer from '../containers/table-row' 6 | import { Tr } from './table-row' 7 | import Empty from './empty' 8 | 9 | const StyledTable = styled.table` 10 | width: 100%; 11 | max-width: 80rem; 12 | margin: 0 auto; 13 | border-collapse: collapse; 14 | th, 15 | td { 16 | padding-right: 0.75rem; 17 | padding-left: 0.75rem; 18 | &:nth-child(2) { 19 | padding-left: 0; 20 | } 21 | } 22 | th { 23 | height: 4rem; 24 | font-size: 0.8125rem; 25 | font-weight: normal; 26 | color: var(--color-neutral-60); 27 | border-bottom: 1px solid var(--color-neutral-20); 28 | &:first-child { 29 | border: none; 30 | } 31 | } 32 | td { 33 | height: 4rem; 34 | vertical-align: top; 35 | padding-top: 1rem; 36 | } 37 | tr:hover td { 38 | background-color: var(--color-neutral--04); 39 | } 40 | ` 41 | 42 | const Table = ({ dats, show }) => { 43 | if (!show) return null 44 | 45 | if (!Object.keys(dats).length) return 46 | 47 | return ( 48 |
49 | 50 |
51 | 52 | 54 | 55 | 56 | 57 | 59 | 60 | 61 |
62 | 63 |
64 | {Object.keys(dats).map(key => ( 65 | 66 | ))} 67 | 68 | 69 | 70 | 71 | ) 72 | } 73 | 74 | export default Table 75 | -------------------------------------------------------------------------------- /app/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import React from 'react' 4 | import { render } from 'react-dom' 5 | import { Provider } from 'react-redux' 6 | import { createStore, applyMiddleware, compose } from 'redux' 7 | import datDesktopApp from './reducers' 8 | import { addDat } from './actions' 9 | import App from './components/app' 10 | import logger from 'redux-logger' 11 | import thunk from 'redux-thunk' 12 | import { ipcRenderer as ipc, remote } from 'electron' 13 | import DatMiddleware from './actions/dat-middleware' 14 | import minimist from 'minimist' 15 | import path from 'path' 16 | import { homedir } from 'os' 17 | import datIcons from 'dat-icons' 18 | 19 | const argv = minimist(remote.process.argv.slice(2), { 20 | default: { 21 | db: path.join(homedir(), '.dat-desktop'), 22 | data: path.join(remote.app.getPath('downloads'), '/dat') 23 | } 24 | }) 25 | 26 | const datMiddleware = new DatMiddleware({ 27 | dataDir: argv.db, 28 | downloadsDir: argv.data 29 | }) 30 | const isDev = process.env.NODE_ENV === 'development' 31 | 32 | const store = createStore( 33 | datDesktopApp, 34 | compose( 35 | applyMiddleware( 36 | store => datMiddleware.middleware(store), 37 | thunk, 38 | isDev ? logger : storage => dispatch => dispatch 39 | ) 40 | ) 41 | ) 42 | 43 | document.title = 'Dat Desktop | Welcome' 44 | 45 | datMiddleware 46 | .loadFromDisk() 47 | .then(function () { 48 | // # addGlobalComponents 49 | // Adding global components only once to the DOM. 50 | const svg = document.body.appendChild(datIcons()) 51 | 52 | // remove titleTag from SVG. 53 | // titleTag is provide uncontrollable "tooltip". 54 | Array.from(svg.querySelectorAll('title')).forEach(node => 55 | node.parentNode.removeChild(node) 56 | ) 57 | }) 58 | .then(function () { 59 | render( 60 | 61 | 62 | , 63 | document.getElementById('app-root') 64 | ) 65 | }) 66 | .catch(err => { 67 | console.log(err.stack || err) 68 | }) 69 | 70 | ipc.on('log', (_, str) => console.log(str)) 71 | ipc.on('link', key => store.dispatch(addDat({ key }))) 72 | ipc.on('file', path => store.dispatch(addDat({ path }))) 73 | -------------------------------------------------------------------------------- /app/components/dat-import.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import React from 'react' 4 | import styled from 'styled-components' 5 | import Icon from './icon' 6 | 7 | const Label = styled.label` 8 | --icon-height: 1.2rem; 9 | color: var(--color-neutral-30); 10 | .icon-link { 11 | padding-top: 0.42rem; 12 | padding-left: 0.5rem; 13 | pointer-events: none; 14 | width: var(--icon-height); 15 | height: var(--icon-height); 16 | } 17 | input { 18 | height: 2rem; 19 | width: 7.5rem; 20 | padding-right: 0.5rem; 21 | padding-left: 2rem; 22 | border-radius: 2px; 23 | border: 1px solid transparent; 24 | background-color: transparent; 25 | color: var(--color-neutral-30); 26 | opacity: 1; 27 | text-transform: uppercase; 28 | letter-spacing: 0.025em; 29 | transition-property: width; 30 | transition-duration: 0.15s; 31 | transition-timing-function: ease-in; 32 | &::-webkit-input-placeholder { 33 | color: var(--color-neutral-30); 34 | opacity: 1; 35 | } 36 | &:hover, 37 | &:hover::-webkit-input-placeholder, 38 | &:hover + svg { 39 | color: var(--color-white); 40 | } 41 | &:focus, 42 | &:active { 43 | width: 14rem; 44 | outline: none; 45 | background-color: var(--color-white); 46 | color: var(--color-neutral); 47 | } 48 | &:focus::-webkit-input-placeholder, 49 | &:active::-webkit-input-placeholder, 50 | &:focus + svg, 51 | &:active + svg { 52 | color: var(--color-neutral-50); 53 | } 54 | } 55 | ` 56 | 57 | const DatImport = ({ requestDownload, downloadSparseDat }) => { 58 | const onKeyDown = e => { 59 | const value = e.target.value 60 | if (e.key !== 'Enter' || !value) return 61 | e.target.value = '' 62 | downloadSparseDat({ key: value, paused: false, sparse: true }) 63 | requestDownload(value) 64 | } 65 | 66 | return ( 67 | 77 | ) 78 | } 79 | 80 | export default DatImport 81 | -------------------------------------------------------------------------------- /unit-tests/intro.js: -------------------------------------------------------------------------------- 1 | import test from 'tape' 2 | import React from 'react' 3 | import { shallow } from 'enzyme' 4 | import Intro from '../app/components/intro' 5 | import { 6 | Green as GreenButton, 7 | Plain as PlainButton 8 | } from '../app/components/button' 9 | 10 | test('intro screen should render only empty div when show is false', t => { 11 | const wrapper = shallow() 12 | 13 | t.equal(wrapper.find('div').length, 1) 14 | 15 | t.end() 16 | }) 17 | 18 | test('intro screen should render only one

element when screen given', t => { 19 | const fn = () => {} 20 | const show = true 21 | const wrapper = shallow( 22 | 23 | ) 24 | 25 | t.equal(wrapper.find('p').length, 1) 26 | 27 | t.end() 28 | }) 29 | 30 | test('intro screen should not render plain button when screen is 1', t => { 31 | const fn = () => {} 32 | const show = true 33 | const wrapper = shallow( 34 | 35 | ) 36 | 37 | t.equal(wrapper.find(PlainButton).length, 0) 38 | 39 | t.end() 40 | }) 41 | 42 | test('intro screen should render plain button when screen is not 1', t => { 43 | const fn = () => {} 44 | const show = true 45 | const wrapper = shallow( 46 | 47 | ) 48 | 49 | t.equal(wrapper.find(PlainButton).length, 1) 50 | 51 | t.end() 52 | }) 53 | 54 | test('intro screen should render Done button when screen is not less than 5', t => { 55 | const fn = () => {} 56 | const show = true 57 | const wrapper = shallow( 58 | 59 | ) 60 | 61 | t.equal( 62 | wrapper 63 | .find(GreenButton) 64 | .children() 65 | .text(), 66 | 'Done' 67 | ) 68 | 69 | t.end() 70 | }) 71 | 72 | test('intro screen should render Next button when screen is less than 5', t => { 73 | const fn = () => {} 74 | const show = true 75 | const wrapper = shallow( 76 | 77 | ) 78 | 79 | t.equal( 80 | wrapper 81 | .find(GreenButton) 82 | .children() 83 | .text(), 84 | 'Next' 85 | ) 86 | 87 | t.end() 88 | }) 89 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![deprecated](http://badges.github.io/stability-badges/dist/deprecated.svg)](https://dat-ecosystem.org/) 2 | 3 | More info on active projects and modules at [dat-ecosystem.org](https://dat-ecosystem.org/) 4 | 5 | --- 6 | 7 | # Dat Desktop 8 | 9 | **Peer to peer data versioning & syncronization.** 10 | 11 | [!["Build Status"](https://img.shields.io/travis/dat-land/dat-desktop/master.svg?style=flat-square)](https://travis-ci.org/dat-land/dat-desktop) 12 | [!["Standard](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square)](https://standardjs.com) 13 | 14 | ![screenshot](assets/screenshot.png) 15 | 16 | ## Table of Content 17 | 18 | - [Download](https://github.com/datproject/dat-desktop/releases) 19 | - [FAQ](#faq) 20 | - [License](#licenses) 21 | 22 | 23 | ## Contributing 24 | 25 | To run _Dat Desktop_ in development mode: 26 | 27 | ```sh 28 | node --version # v12.4.0 29 | npm install # install dependencies 30 | npm start # start the application 31 | ``` 32 | 33 | To create binary packages run: 34 | 35 | ```sh 36 | npm install # install dependencies 37 | npm run dist :os # compile the app into an binary package 38 | ``` 39 | 40 | ## FAQ 41 | 42 | ### How to speed up downloading Electron 43 | 44 | If you’re not in Europe or the US, you might want to use a different mirror for 45 | `electron`. You can set the `ELECTRON_MIRROR` variable to point to a different 46 | provider: 47 | 48 | ```sh 49 | # Europe / US 50 | $ npm install 51 | 52 | # Asia / Oceania 53 | $ ELECTRON_MIRROR="https://npm.taobao.org/mirrors/electron/" npm install 54 | ``` 55 | 56 | ## Licenses 57 | 58 | [MIT License](./LICENSE) 59 | 60 | ### Font Attribution & License 61 | 62 | SourceSansPro-Regular.ttf: Copyright 2010, 2012 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe Systems Incorporated in the United States and/or other countries. [SIL Open Font License, 1.1](http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL) 63 | 64 | SourceCodePro-Regular.ttf: Copyright 2010, 2012 Adobe Systems Incorporated. All Rights Reserved. [SIL Open Font License, 1.1](http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=OFL) 65 | -------------------------------------------------------------------------------- /static/base.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'SourceSansPro'; 3 | src: url('fonts/SourceSansPro-Regular.ttf') format('truetype') 4 | } 5 | 6 | @font-face { 7 | font-family: 'SourceCodePro'; 8 | src: url('fonts/SourceCodePro-Regular.ttf') format('truetype') 9 | } 10 | 11 | html, body, div, span, applet, object, iframe, 12 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 13 | a, abbr, acronym, address, big, cite, code, 14 | del, dfn, em, img, ins, kbd, q, s, samp, 15 | small, strike, strong, sub, sup, tt, var, 16 | b, u, i, center, 17 | dl, dt, dd, ol, ul, li, 18 | fieldset, form, label, legend, 19 | table, caption, tbody, tfoot, thead, tr, th, td, 20 | article, aside, canvas, details, embed, 21 | figure, figcaption, footer, header, hgroup, 22 | menu, nav, output, ruby, section, summary, 23 | time, mark, audio, video { 24 | margin: 0; 25 | padding: 0; 26 | border: 0; 27 | } 28 | 29 | html { 30 | overflow: auto; 31 | } 32 | 33 | body { 34 | line-height: 1.5; 35 | overflow: hidden; 36 | min-width: 800px; 37 | height: 100vh; 38 | } 39 | 40 | main { 41 | background-color: var(--color-white); 42 | } 43 | 44 | body, input, textarea, select, button { 45 | font-family: 'SourceSansPro', sans-serif; 46 | &:focus { 47 | outline: none; 48 | } 49 | } 50 | 51 | pre, code { 52 | font-family: 'SourceCodePro', monospace; 53 | } 54 | 55 | input, textarea, select, button { 56 | -webkit-user-drag: none; 57 | } 58 | 59 | html { 60 | -webkit-user-select: none; 61 | -webkit-user-drag: none; 62 | cursor: default; 63 | } 64 | 65 | h1, h2, h3, h4, h5, h6 { 66 | font-size: 1rem; 67 | font-weight: bold; 68 | } 69 | 70 | img { 71 | -webkit-user-drag: none; 72 | } 73 | 74 | button { 75 | border: none; 76 | } 77 | 78 | .is-selectable { 79 | -webkit-user-select: auto; 80 | cursor: auto; 81 | } 82 | 83 | .is-draggable { 84 | -webkit-app-region: drag; 85 | } 86 | 87 | /* fading animation to highlight new dats */ 88 | .fade-highlight { 89 | animation: fade-highlight 2.25s ease-in-out; 90 | } 91 | @keyframes fade-highlight { 92 | 0% { background-color: rgba(42,202,75,.2); } 93 | 35% { background-color: rgba(42,202,75,.2); } 94 | 100% { background-color: rgba(42,202,75,0); } 95 | } 96 | 97 | #app-root { 98 | display: flex; 99 | flex-direction: column; 100 | height: 100vh; 101 | } 102 | 103 | #app-root > main { 104 | flex: 1; 105 | } -------------------------------------------------------------------------------- /unit-tests/status.js: -------------------------------------------------------------------------------- 1 | import test from 'tape' 2 | import React from 'react' 3 | import { shallow } from 'enzyme' 4 | import Status from '../app/components/status' 5 | 6 | test('progress text should read "Paused" when dat is paused', t => { 7 | const wrapper = shallow( 8 | 20 | ) 21 | 22 | t.equal( 23 | wrapper 24 | .find('.f7.f6-l') 25 | .children() 26 | .children() 27 | .text(), 28 | 'Paused.' 29 | ) 30 | 31 | t.end() 32 | }) 33 | 34 | test('progress text should show upload speed when dat is completed', t => { 35 | const wrapper = shallow( 36 | 48 | ) 49 | 50 | t.equal( 51 | wrapper 52 | .find('.f7.f6-l') 53 | .children() 54 | .children() 55 | .text(), 56 | 'Complete. ↑ 30 B/s' 57 | ) 58 | 59 | t.end() 60 | }) 61 | 62 | test('progress text should show wait message when dat is stale', t => { 63 | const wrapper = shallow( 64 | 76 | ) 77 | 78 | t.equal( 79 | wrapper 80 | .find('.f7.f6-l') 81 | .children() 82 | .children() 83 | .text(), 84 | 'waiting for peers…' 85 | ) 86 | 87 | t.end() 88 | }) 89 | 90 | test('progress text should show up/down speed when dat is loading', t => { 91 | const wrapper = shallow( 92 | 104 | ) 105 | 106 | t.equal( 107 | wrapper 108 | .find('.f7.f6-l') 109 | .children() 110 | .children() 111 | .text(), 112 | '↓ 40 B/s↑ 30 B/s' 113 | ) 114 | 115 | t.end() 116 | }) 117 | -------------------------------------------------------------------------------- /app/components/header.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import React, { Fragment } from 'react' 4 | import styled from 'styled-components' 5 | import { transparentize } from 'polished' 6 | import { neutral } from 'dat-colors' 7 | import DatImport from '../containers/dat-import' 8 | import * as Button from './button' 9 | import Icon from './icon' 10 | 11 | const Container = styled.header` 12 | position: fixed; 13 | z-index: 1; 14 | width: 100%; 15 | height: 2.5rem; 16 | padding: 0.25rem 0.75rem; 17 | display: flex; 18 | align-items: center; 19 | justify-content: flex-end; 20 | -webkit-app-region: drag; 21 | background-color: var(--color-neutral); 22 | color: var(--color-white); 23 | & ~ main { 24 | margin-top: 2.5rem; 25 | } 26 | ` 27 | 28 | const HideLayer = styled.div` 29 | position: fixed; 30 | background: ${transparentize(0.85, neutral)}; 31 | width: 100vw; 32 | height: 100vh; 33 | left: 0; 34 | top: 0; 35 | ` 36 | 37 | const Header = ({ onShare, onMenu, onReport, menuVisible, version }) => { 38 | const toggleMenu = () => onMenu(!menuVisible) 39 | return ( 40 | 41 | {menuVisible && } 42 | 43 | 44 | } 46 | className='b--transparent v-mid color-neutral-30 hover-color-white f7 f6-l btn-share-folder' 47 | onClick={onShare} 48 | > 49 | Share Folder 50 | 51 | } 53 | className='ml2 v-mid color-neutral-20 hover-color-white pointer btn-toggle-menu' 54 | onClick={toggleMenu} 55 | /> 56 | {menuVisible && ( 57 |

61 |

Dat Desktop {version}

62 |

63 | Dat Desktop is a peer to peer data versioning and syncronization app. 64 |

65 |

66 | 71 | Report Bug 72 | 73 |

74 |
75 | )} 76 | 77 | 78 | ) 79 | } 80 | 81 | export default Header 82 | -------------------------------------------------------------------------------- /dev/react-dev-tools/icons/deadcode.svg: -------------------------------------------------------------------------------- 1 | development780780 -------------------------------------------------------------------------------- /dev/react-dev-tools/icons/development.svg: -------------------------------------------------------------------------------- 1 | development780780 -------------------------------------------------------------------------------- /dev/react-dev-tools/build/background.js: -------------------------------------------------------------------------------- 1 | !function(modules) { 2 | function __webpack_require__(moduleId) { 3 | if (installedModules[moduleId]) return installedModules[moduleId].exports; 4 | var module = installedModules[moduleId] = { 5 | exports: {}, 6 | id: moduleId, 7 | loaded: !1 8 | }; 9 | return modules[moduleId].call(module.exports, module, module.exports, __webpack_require__), 10 | module.loaded = !0, module.exports; 11 | } 12 | var installedModules = {}; 13 | return __webpack_require__.m = modules, __webpack_require__.c = installedModules, 14 | __webpack_require__.p = "", __webpack_require__(0); 15 | }([ function(module, exports) { 16 | "use strict"; 17 | function isNumeric(str) { 18 | return +str + "" === str; 19 | } 20 | function installContentScript(tabId) { 21 | chrome.tabs.executeScript(tabId, { 22 | file: "/build/contentScript.js" 23 | }, function() {}); 24 | } 25 | function doublePipe(one, two) { 26 | function lOne(message) { 27 | two.postMessage(message); 28 | } 29 | function lTwo(message) { 30 | one.postMessage(message); 31 | } 32 | function shutdown() { 33 | one.onMessage.removeListener(lOne), two.onMessage.removeListener(lTwo), one.disconnect(), 34 | two.disconnect(); 35 | } 36 | one.onMessage.addListener(lOne), two.onMessage.addListener(lTwo), one.onDisconnect.addListener(shutdown), 37 | two.onDisconnect.addListener(shutdown); 38 | } 39 | function setIconAndPopup(reactBuildType, tabId) { 40 | chrome.browserAction.setIcon({ 41 | tabId: tabId, 42 | path: { 43 | "16": "icons/16-" + reactBuildType + ".png", 44 | "32": "icons/32-" + reactBuildType + ".png", 45 | "48": "icons/48-" + reactBuildType + ".png", 46 | "128": "icons/128-" + reactBuildType + ".png" 47 | } 48 | }), chrome.browserAction.setPopup({ 49 | tabId: tabId, 50 | popup: "popups/" + reactBuildType + ".html" 51 | }); 52 | } 53 | var ports = {}, IS_FIREFOX = navigator.userAgent.indexOf("Firefox") >= 0; 54 | chrome.runtime.onConnect.addListener(function(port) { 55 | var tab = null, name = null; 56 | isNumeric(port.name) ? (tab = port.name, name = "devtools", installContentScript(+port.name)) : (tab = port.sender.tab.id, 57 | name = "content-script"), ports[tab] || (ports[tab] = { 58 | devtools: null, 59 | "content-script": null 60 | }), ports[tab][name] = port, ports[tab].devtools && ports[tab]["content-script"] && doublePipe(ports[tab].devtools, ports[tab]["content-script"]); 61 | }), IS_FIREFOX && chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) { 62 | tab.active && "loading" === changeInfo.status && setIconAndPopup("disabled", tabId); 63 | }), chrome.runtime.onMessage.addListener(function(req, sender) { 64 | if (req.hasDetectedReact && sender.tab) { 65 | var reactBuildType = req.reactBuildType; 66 | sender.url.indexOf("facebook.github.io/react") !== -1 && (reactBuildType = "production"), 67 | setIconAndPopup(reactBuildType, sender.tab.id); 68 | } 69 | }); 70 | } ]); -------------------------------------------------------------------------------- /app/components/button.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import React from 'react' 4 | import styled from 'styled-components' 5 | 6 | const BaseButton = styled.button.attrs(props => ({ 7 | ...props 8 | }))` 9 | text-transform: uppercase; 10 | letter-spacing: 0.025em; 11 | cursor: pointer; 12 | background-color: transparent; 13 | .icon-only { 14 | .btn-text { 15 | display: none; 16 | } 17 | } 18 | :hover, 19 | :focus { 20 | outline: 0; 21 | } 22 | ` 23 | 24 | const HeaderButton = styled(BaseButton)` 25 | color: var(--color-neutral-30); 26 | height: 2rem; 27 | :hover, 28 | :focus { 29 | color: var(--color-white); 30 | } 31 | ` 32 | 33 | var PlainButton = styled(BaseButton)` 34 | padding: 0.5rem 0.75rem; 35 | font-size: 0.75rem; 36 | background-color: transparent; 37 | color: var(--color-neutral-40); 38 | :hover, 39 | :focus { 40 | color: var(--color-neutral-70); 41 | } 42 | ` 43 | 44 | var TextButton = styled(BaseButton)` 45 | font-size: 0.75rem; 46 | background-color: transparent; 47 | color: var(--color-neutral-40); 48 | :hover, 49 | :focus { 50 | color: var(--color-neutral-70); 51 | } 52 | ` 53 | 54 | var GreenButton = styled(BaseButton)` 55 | padding: 0.5rem 0.75rem; 56 | font-size: 0.75rem; 57 | background-color: var(--color-green); 58 | color: var(--color-neutral-04); 59 | :hover, 60 | :focus { 61 | background-color: var(--color-green-hover); 62 | color: var(--color-white); 63 | } 64 | ` 65 | 66 | var RedButton = styled(BaseButton)` 67 | padding: 0.5rem 0.75rem; 68 | font-size: 0.75rem; 69 | background-color: var(--color-red); 70 | color: var(--color-neutral-04); 71 | :hover, 72 | :focus { 73 | background-color: var(--color-red-hover); 74 | color: var(--color-white); 75 | } 76 | ` 77 | 78 | const InnerWrapper = styled.div` 79 | display: flex; 80 | flex-wrap: nowrap; 81 | flex-direction: row; 82 | justify-content: center; 83 | align-items: center; 84 | ` 85 | 86 | export const Header = ({ children, icon, ...props }) => ( 87 | 88 | {children} 89 | 90 | ) 91 | 92 | export const Icon = ({ icon, ...props }) => ( 93 | 94 | {icon} 95 | 96 | ) 97 | 98 | export const Plain = ({ children, ...props }) => ( 99 | 100 | {children} 101 | 102 | ) 103 | 104 | export const Text = ({ children, icon, ...props }) => ( 105 | 106 | {children} 107 | 108 | ) 109 | 110 | export const Green = ({ children, icon, ...props }) => ( 111 | 112 | {children} 113 | 114 | ) 115 | 116 | export const Red = ({ children, icon, ...props }) => ( 117 | 118 | {children} 119 | 120 | ) 121 | 122 | const InnerWrapperComponent = ({ children, icon }) => ( 123 | 124 | {icon} 125 | {children} 126 | 127 | ) 128 | -------------------------------------------------------------------------------- /unit-tests/inspect.js: -------------------------------------------------------------------------------- 1 | import test from 'tape' 2 | import React from 'react' 3 | import { shallow } from 'enzyme' 4 | import Inspect from '../app/components/inspect' 5 | 6 | test('should render empty', t => { 7 | const wrapper = shallow( {}} />) 8 | 9 | t.equal(wrapper.isEmptyRender(), true) 10 | 11 | t.end() 12 | }) 13 | 14 | test('should set title to dat key when metadata is not present on dat', t => { 15 | const key = '40a7f6b6147ae695bcbcff432f684c7bb5291ea339c28c1755196cdeb80bd3f8' 16 | const wrapper = shallow( 17 | {}} 25 | /> 26 | ) 27 | 28 | t.equal(wrapper.find('h2').text(), key) 29 | 30 | t.end() 31 | }) 32 | 33 | test('should show default values when metadata is not present on dat ', t => { 34 | const key = '40a7f6b6147ae695bcbcff432f684c7bb5291ea339c28c1755196cdeb80bd3f8' 35 | const wrapper = shallow( 36 | {}} 45 | /> 46 | ) 47 | 48 | t.equal( 49 | wrapper 50 | .children() 51 | .find('[data-test="key"]') 52 | .childAt(0) 53 | .text(), 54 | key 55 | ) 56 | t.equal( 57 | wrapper 58 | .children() 59 | .find('[data-test="size"]') 60 | .childAt(0) 61 | .text(), 62 | '0 B' 63 | ) 64 | t.equal( 65 | wrapper 66 | .children() 67 | .find('[data-test="author"]') 68 | .childAt(0) 69 | .text(), 70 | 'N/A' 71 | ) 72 | t.equal( 73 | wrapper 74 | .children() 75 | .find('[data-test="description"]') 76 | .childAt(0) 77 | .text(), 78 | 'N/A' 79 | ) 80 | 81 | t.end() 82 | }) 83 | 84 | test('should show info when present on dat', t => { 85 | const key = '40a7f6b6147ae695bcbcff432f684c7bb5291ea339c28c1755196cdeb80bd3f8' 86 | const wrapper = shallow( 87 | {}} 101 | /> 102 | ) 103 | 104 | t.equal( 105 | wrapper 106 | .children() 107 | .find('[data-test="key"]') 108 | .childAt(0) 109 | .text(), 110 | key 111 | ) 112 | t.equal( 113 | wrapper 114 | .children() 115 | .find('[data-test="size"]') 116 | .childAt(0) 117 | .text(), 118 | '9 B' 119 | ) 120 | t.equal( 121 | wrapper 122 | .children() 123 | .find('[data-test="peers"]') 124 | .childAt(0) 125 | .text(), 126 | '2' 127 | ) 128 | t.equal( 129 | wrapper 130 | .children() 131 | .find('[data-test="author"]') 132 | .childAt(0) 133 | .text(), 134 | 'A-author' 135 | ) 136 | t.equal( 137 | wrapper 138 | .children() 139 | .find('[data-test="description"]') 140 | .childAt(0) 141 | .text(), 142 | 'A-desc' 143 | ) 144 | t.equal( 145 | wrapper 146 | .children() 147 | .find('[data-test="path"]') 148 | .childAt(0) 149 | .text(), 150 | 'A-path' 151 | ) 152 | 153 | t.end() 154 | }) 155 | -------------------------------------------------------------------------------- /app/actions/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import { encode } from 'dat-encoding' 4 | import { clipboard, remote, shell } from 'electron' 5 | import fs from 'fs' 6 | import promisify from 'util-promisify' 7 | import path from 'path' 8 | 9 | const stat = promisify(fs.stat) 10 | 11 | function showOpenDialog (props) { 12 | if (process.env.RUNNING_IN_SPECTRON && process.env.OPEN_RESULT) { 13 | return new Promise((resolve, reject) => { 14 | resolve({ 15 | cancelled: false, 16 | filePaths: [path.resolve(__dirname, process.env.OPEN_RESULT)] 17 | }) 18 | }) 19 | } 20 | return remote.dialog.showOpenDialog(props) 21 | } 22 | 23 | export const shareDat = key => ({ type: 'DIALOGS_LINK_OPEN', key }) 24 | export const copyLink = link => { 25 | clipboard.writeText(link) 26 | return { type: 'DIALOGS_LINK_COPY' } 27 | } 28 | export const closeShareDat = () => ({ type: 'DIALOGS_LINK_CLOSE' }) 29 | export const closeAlert = () => ({ type: 'DIALOGS_ALERT_CLOSE' }) 30 | 31 | export const createDat = () => dispatch => { 32 | showOpenDialog({ 33 | properties: ['openDirectory'] 34 | }) 35 | .then(({ filePaths, cancelled }) => { 36 | if (cancelled) return 37 | if (!filePaths) { 38 | console.error('Did not get files from the open dialog, closing') 39 | return 40 | } 41 | const path = filePaths[0] 42 | addDat({ path })(dispatch) 43 | }) 44 | .catch(err => { 45 | console.error(err) 46 | }) 47 | } 48 | export const requestDownload = key => ({ 49 | type: 'REQUEST_DOWNLOAD', 50 | key: encode(key) 51 | }) 52 | export const hideDownloadScreen = () => ({ type: 'HIDE_DOWNLOAD_SCREEN' }) 53 | export const cancelDownloadDat = key => dispatch => 54 | dispatch({ type: 'CANCEL_DOWNLOAD_DAT', key }) 55 | export const changeDownloadPath = key => dispatch => { 56 | const files = showOpenDialog({ 57 | properties: ['openDirectory'] 58 | }) 59 | if (!files || !files.length) return 60 | const path = files[0] 61 | dispatch({ type: 'CHANGE_DOWNLOAD_PATH', key, path }) 62 | } 63 | 64 | export const downloadSparseDat = ({ key }) => dispatch => 65 | dispatch({ type: 'DOWNLOAD_SPARSE_DAT', key }) 66 | export const addDat = ({ key, path, paused, ...opts }) => dispatch => 67 | dispatch({ type: 'TRY_ADD_DAT', key, path, paused, ...opts }) 68 | export const deleteDat = key => ({ type: 'DIALOGS_DELETE_OPEN', key }) 69 | export const confirmDeleteDat = key => dispatch => { 70 | dispatch({ type: 'REMOVE_DAT', key }) 71 | dispatch({ type: 'DIALOGS_DELETE_CLOSE' }) 72 | } 73 | export const cancelDeleteDat = () => ({ type: 'DIALOGS_DELETE_CLOSE' }) 74 | export const togglePause = ({ key, paused }) => dispatch => 75 | dispatch({ type: 'TOGGLE_PAUSE', paused, key }) 76 | 77 | export const inspectDat = key => dispatch => { 78 | dispatch({ type: 'INSPECT_DAT', key }) 79 | } 80 | export const closeInspectDat = () => ({ type: 'INSPECT_DAT_CLOSE' }) 81 | export const dropFolder = folder => async dispatch => { 82 | const isDirectory = (await stat(folder.path)).isDirectory() 83 | if (!isDirectory) return 84 | addDat({ path: folder.path })(dispatch) 85 | } 86 | 87 | export const openHomepage = () => shell.openExternal('https://datproject.org/') 88 | export const nextIntro = screen => ({ type: 'NEXT_INTRO', screen }) 89 | export const hideIntro = () => ({ type: 'HIDE_INTRO' }) 90 | 91 | export const updateTitle = (key, title) => async dispatch => 92 | dispatch({ 93 | type: 'UPDATE_TITLE', 94 | key, 95 | title 96 | }) 97 | 98 | export const toggleMenu = visible => dispatch => { 99 | dispatch({ type: 'TOGGLE_MENU', visible }) 100 | } 101 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ### [3.0.2](https://github.com/datproject/dat-desktop/compare/v3.0.1...v3.0.2) (2020-02-20) 6 | 7 | 8 | ### Bug Fixes 9 | 10 | * add standard version ([c6788cf](https://github.com/datproject/dat-desktop/commit/c6788cf)) 11 | * Windows download issue, an attempt to squash it! ([dd270f1](https://github.com/datproject/dat-desktop/commit/dd270f1)) 12 | 13 | 14 | 15 | ### [3.0.1](https://github.com/datproject/dat-desktop/compare/v3.0.0...v3.0.1) (2020-02-18) 16 | 17 | 18 | ### Bug Fixes 19 | 20 | * Fix a bug where sometimes download dir wouldn't exist and crash ([7e1f2a7](https://github.com/datproject/dat-desktop/commit/7e1f2a741c708772500a505e7f21b76456af1c92)) 21 | * Should no longer crash on startup for Mac. ([1bd0436](https://github.com/datproject/dat-desktop/commit/1bd043692129fe2436c15b8f6d2371bd9ea6f8bc)) 22 | * When downloading a dat, ensure Download button is visible. ([4a573ed](https://github.com/datproject/dat-desktop/commit/4a573ed385d7f3df7ac5f4f687f4e9db40397556)) 23 | 24 | ## [3.0.0](https://github.com/datproject/dat-desktop/compare/v2.1.0...v3.0.0) (2020-02-18) 25 | 26 | ### Features 27 | 28 | * Windows builds! 29 | 30 | ### Bug Fixes 31 | 32 | * add package-lock ([8420cbf](https://github.com/datproject/dat-desktop/commit/8420cbf44b7a05902dac014663886cd96e71b61d)) 33 | * avoid the screen white-out. ([f53397a](https://github.com/datproject/dat-desktop/commit/f53397aec79739b54d848a46726b242fee759ec1)) 34 | * buttons will click now ([f3aceb9](https://github.com/datproject/dat-desktop/commit/f3aceb9217ae4a97a5116a9f6e49b8de9de690f8)) 35 | * changing title still keep on the next session. ([2f826ac](https://github.com/datproject/dat-desktop/commit/2f826ac35155eda883773d840b76116c4bcc9630)) 36 | * deprecated syntax by current styled-components. ([6340267](https://github.com/datproject/dat-desktop/commit/634026755d15526edaac5a0888da9bc312915934)) 37 | * Even though if press "cancel" on download-preview-screen, still listed in dat-list. ([894225b](https://github.com/datproject/dat-desktop/commit/894225bedaa06b68835de9b4f386f74822b9f94b)) 38 | * https://github.com/dat-land/dat-desktop/pull/600#issuecomment-440926308 ([5d05d27](https://github.com/datproject/dat-desktop/commit/5d05d2737f0a504f37f1024620de75da44149b26)), closes [/github.com/dat-land/dat-desktop/pull/600#issuecomment-440926308](https://github.com/datproject//github.com/dat-land/dat-desktop/pull/600/issues/issuecomment-440926308) 39 | * overlay on menu open. ([96cf727](https://github.com/datproject/dat-desktop/commit/96cf7279e347dd9e64cfc1ce418af892ccf7a71e)) 40 | * overlay to be not enough filled the. ([c3698de](https://github.com/datproject/dat-desktop/commit/c3698de98c63341489291fcbe723dc45db7dc88f)) 41 | * Re-introduced --publish=onTagOrDraft flag, missing for the release management. ([ffff3d6](https://github.com/datproject/dat-desktop/commit/ffff3d6f81906ca5dfe7a4e829ee237886f81541)) 42 | * set to disable the hover action to dat-list-header. ([96f34c5](https://github.com/datproject/dat-desktop/commit/96f34c597d795ee8367c119a8860e6ef69c26ae6)) 43 | * to be fixed position the list header. ([5f198cb](https://github.com/datproject/dat-desktop/commit/5f198cb32e63c1766977134c73e8060818482891)) 44 | * unit-test ([840b108](https://github.com/datproject/dat-desktop/commit/840b1086bf73a5fff81697d39d4e1ef522e727b6)) 45 | * update Electron, comment deprecated makeSingleInstance call ([3a88c9f](https://github.com/datproject/dat-desktop/commit/3a88c9f42aff12a12f8c43defcbae75a9ae22073)) 46 | * update to electron changed showOpenDialog api ([834b23a](https://github.com/datproject/dat-desktop/commit/834b23aed094007d6b8dd144d85fa5050f09f6bb)) 47 | -------------------------------------------------------------------------------- /unit-tests/table-row.js: -------------------------------------------------------------------------------- 1 | import test from 'tape' 2 | import React from 'react' 3 | import { shallow } from 'enzyme' 4 | import TableRow from '../app/components/table-row' 5 | 6 | test('table row should render author as Anonymous if not present on dat', t => { 7 | const fn = () => {} 8 | const wrapper = shallow( 9 | 19 | ) 20 | 21 | t.equal(wrapper.find('.author').text(), 'Anonymous • ') 22 | 23 | t.end() 24 | }) 25 | 26 | test('table row should render author name if present on dat', t => { 27 | const fn = () => {} 28 | const wrapper = shallow( 29 | 41 | ) 42 | 43 | t.equal(wrapper.find('.author').text(), 'A-author • ') 44 | 45 | t.end() 46 | }) 47 | 48 | test('table row should render writable state as Read-only if not present on dat', t => { 49 | const fn = () => {} 50 | const wrapper = shallow( 51 | 61 | ) 62 | 63 | t.equal(wrapper.find('.title').text(), 'Read-only') 64 | 65 | t.end() 66 | }) 67 | 68 | test('table row should render writable state as Read & Write if dat is writable', t => { 69 | const fn = () => {} 70 | const wrapper = shallow( 71 | 82 | ) 83 | 84 | t.equal(wrapper.find('.title').text(), 'Read & Write') 85 | 86 | t.end() 87 | }) 88 | 89 | test('table row should render size equals to 0 when length is not defined on dat', t => { 90 | const fn = () => {} 91 | const wrapper = shallow( 92 | 103 | ) 104 | 105 | t.equal(wrapper.find('.size').text(), '0 B') 106 | 107 | t.end() 108 | }) 109 | 110 | test('table row should render size equals to length property on stats', t => { 111 | const fn = () => {} 112 | const wrapper = shallow( 113 | 126 | ) 127 | 128 | t.equal(wrapper.find('.size').text(), '40 B') 129 | 130 | t.end() 131 | }) 132 | 133 | test('table row should render peers', t => { 134 | const fn = () => {} 135 | const wrapper = shallow( 136 | 150 | ) 151 | 152 | t.equal(wrapper.find('.network').text(), '2') 153 | 154 | t.end() 155 | }) 156 | -------------------------------------------------------------------------------- /app/components/status.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import React from 'react' 4 | import styled from 'styled-components' 5 | import bytes from 'prettier-bytes' 6 | 7 | const ProgressBar = styled.div` 8 | --progress-height: 0.5rem; 9 | --bar-height: var(--progress-height); 10 | --counter-width: 3rem; 11 | --tile-width: 28px; 12 | --stripe-width: 5px; 13 | min-width: 8rem; 14 | max-width: 24rem; 15 | overflow: hidden; 16 | padding-top: 0.4rem; 17 | .bar { 18 | height: var(--progress-height); 19 | width: calc(100% - var(--counter-width)); 20 | float: left; 21 | overflow: hidden; 22 | background-color: var(--color-neutral-20); 23 | border-radius: 2px; 24 | } 25 | .line { 26 | width: 0%; 27 | height: var(--progress-height); 28 | background-color: var(--color-blue); 29 | border-radius: 2px; 30 | } 31 | .line-loading { 32 | overflow: hidden; 33 | position: relative; 34 | height: var(--bar-height); 35 | &:before { 36 | content: ''; 37 | width: 100%; 38 | height: var(--bar-height); 39 | position: absolute; 40 | top: 0; 41 | left: 0; 42 | background-image: repeating-linear-gradient( 43 | -45deg, 44 | transparent, 45 | transparent var(--stripe-width), 46 | rgba(255, 255, 255, 0.1) var(--stripe-width), 47 | rgba(255, 255, 255, 0.1) calc(2 * var(--stripe-width)) 48 | ); 49 | background-size: var(--tile-width) var(--bar-height); 50 | animation: move-bg 0.75s linear infinite; 51 | } 52 | } 53 | .line-complete { 54 | background-color: var(--color-green); 55 | } 56 | .line-paused { 57 | background-color: var(--color-neutral-40); 58 | } 59 | .counter { 60 | float: right; 61 | min-width: var(--counter-width); 62 | margin-top: -0.4rem; 63 | text-align: right; 64 | } 65 | 66 | @keyframes move-bg { 67 | 0% { 68 | background-position: 28px 0; 69 | } 70 | 100% { 71 | background-position: 0 0; 72 | } 73 | } 74 | ` 75 | 76 | const ProgressSubline = styled.span` 77 | .arrow { 78 | vertical-align: top; 79 | } 80 | ` 81 | 82 | const speed = n => `${bytes(n || 0)}/s` 83 | 84 | const Status = ({ dat }) => { 85 | const progress = Math.floor((dat.progress || 0) * 100) 86 | const progressbarLine = 87 | dat.state === 'loading' 88 | ? 'line-loading' 89 | : dat.paused || dat.state === 'stale' ? 'line-paused' : 'line-complete' 90 | const netStats = dat.stats.network 91 | 92 | let progressText 93 | 94 | if (dat.paused) { 95 | progressText = 'Paused.' 96 | } else { 97 | switch (dat.state) { 98 | case 'complete': 99 | progressText = `Complete. ↑ ${speed(netStats.up)}` 100 | break 101 | case 'loading': 102 | progressText = ( 103 | 104 | {speed(netStats.down)} 105 | {speed(netStats.up)} 106 | 107 | ) 108 | break 109 | case 'stale': 110 | progressText = 'waiting for peers…' 111 | break 112 | default: 113 | progressText = 'Paused.' 114 | } 115 | } 116 | 117 | return ( 118 |
119 | 120 |
{progress}%
121 |
122 |
126 |
127 | 128 |

129 | {progressText} 130 |

131 |
132 | ) 133 | } 134 | 135 | export default Status 136 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { app, BrowserWindow, shell, Menu, ipcMain } = require('electron') 4 | const { neutral } = require('dat-colors') 5 | const autoUpdater = require('./lib/auto-updater') 6 | const defaultMenu = require('electron-default-menu') 7 | const doctor = require('dat-doctor') 8 | const path = require('path') 9 | const isDev = process.env.NODE_ENV === 'development' 10 | const isTest = process.env.NODE_ENV === 'test' 11 | const { Writable } = require('stream') 12 | 13 | if (typeof process.env.NODE_V === 'string' && process.env.NODE_V !== process.version) { 14 | console.error(` 15 | WARNING: 16 | You are using a different version of Node than is used in this electron release! 17 | - Used Version: ${process.env.NODE_V} 18 | - Electron's Node Version: ${process.version} 19 | 20 | We recommend running: 21 | 22 | $ nvm install ${process.version}; npm rebuild; 23 | 24 | `) 25 | } 26 | 27 | const menu = defaultMenu(app, shell) 28 | menu[menu.length - 1].submenu.push({ 29 | label: 'Doctor', 30 | click: () => { 31 | win.webContents.openDevTools({ mode: 'detach' }) 32 | const out = Writable({ 33 | write (chunk, env, done) { 34 | if (win) win.webContents.send('log', chunk.toString()) 35 | done() 36 | } 37 | }) 38 | doctor({ out }) 39 | } 40 | }) 41 | 42 | let win 43 | let watchProcess 44 | 45 | app.on('ready', () => { 46 | if (isDev) { 47 | BrowserWindow.addDevToolsExtension(path.join(__dirname, 'dev', 'react-dev-tools')) 48 | watchAndReload() 49 | } 50 | win = new BrowserWindow({ 51 | // Extending the size of the browserwindow to make sure that the developer bar is visible. 52 | width: 800 + (isDev ? 50 : 0), 53 | height: 600 + (isDev ? 200 : 0), 54 | titleBarStyle: 'hiddenInset', 55 | minWidth: 640, 56 | minHeight: 395, 57 | backgroundColor: neutral, 58 | icon: path.resolve(`${__dirname}/build/icon.png`), 59 | // Spectron doesn't work with "preload" enabled, loading is handled in index.html 60 | webPreferences: (!isTest 61 | ? { 62 | nodeIntegration: false, 63 | preload: `${__dirname}/preload.js` 64 | } 65 | : { 66 | nodeIntegration: true 67 | } 68 | ) 69 | }) 70 | win.loadURL(`file://${__dirname}/index.html#${process.env.NODE_ENV}`) 71 | Menu.setApplicationMenu(Menu.buildFromTemplate(menu)) 72 | 73 | ipcMain.on('progress', (_, progress) => win && win.setProgressBar(progress)) 74 | 75 | if (isDev) { 76 | win.webContents.openDevTools() 77 | } else { 78 | const log = str => win && win.webContents.send('log', str) 79 | autoUpdater({ log }) 80 | } 81 | }) 82 | 83 | app.on('will-finish-launching', () => { 84 | app.on('open-url', (_, url) => win.webContents.send('link', url)) 85 | app.on('open-file', (_, path) => win.webContents.send('file', path)) 86 | }) 87 | 88 | app.on('window-all-closed', () => { 89 | if (watchProcess) { 90 | watchProcess.close() 91 | watchProcess = null 92 | } 93 | app.quit() 94 | }) 95 | 96 | // const quit = app.makeSingleInstance(() => { 97 | // if (!win) return 98 | // if (win.isMinimized()) win.restore() 99 | // win.focus() 100 | // }) 101 | // 102 | // if (quit) app.quit() 103 | 104 | function watchAndReload () { 105 | let gaze 106 | let first = true 107 | try { 108 | gaze = require('gaze') 109 | } catch (e) { 110 | console.warn('Gaze is not installed, wont be able to reload the app') 111 | // In case dev dependencies are not installed 112 | return 113 | } 114 | gaze([ 115 | `preload.js`, 116 | `static/**/*` 117 | ], { 118 | debounceDelay: 60, 119 | cwd: __dirname 120 | }, (err, process) => { 121 | if (err) { 122 | console.warn('Gaze doesnt run well, wont be able to reload the app') 123 | console.warn(err) 124 | return 125 | } 126 | watchProcess = process 127 | watchProcess.on('all', () => { 128 | if (first) { 129 | first = false 130 | return 131 | } 132 | win && win.reload() 133 | }) 134 | }) 135 | } 136 | -------------------------------------------------------------------------------- /assets/intro-4.svg: -------------------------------------------------------------------------------- 1 | intro-4 -------------------------------------------------------------------------------- /app/components/title-field.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import ReactDOM from 'react-dom' 3 | import styled from 'styled-components' 4 | import Icon from './icon' 5 | import { Plain as PlainButton, Green as GreenButton } from './button' 6 | 7 | const Overlay = styled.div` 8 | position: fixed; 9 | top: 0; 10 | left: 0; 11 | width: 100vw; 12 | height: 100vh; 13 | background-color: rgba(0, 0, 0, 0.2); 14 | z-index: 1; 15 | ` 16 | 17 | const EditableFieldWrapper = styled.div` 18 | position: relative; 19 | h2 { 20 | position: relative; 21 | } 22 | .indicator { 23 | position: absolute; 24 | display: none; 25 | top: 0.25rem; 26 | right: 0; 27 | width: 0.75rem; 28 | } 29 | &:hover, 30 | &:focus { 31 | h2 { 32 | color: var(--color-blue); 33 | } 34 | .indicator { 35 | display: block; 36 | } 37 | } 38 | ` 39 | 40 | const ActiveEditableFieldWrapper = styled(EditableFieldWrapper)` 41 | z-index: 1; 42 | ` 43 | 44 | const InputFieldStyle = styled.input` 45 | :focus { 46 | outline: none; 47 | } 48 | ` 49 | 50 | class InputField extends Component { 51 | componentDidMount () { 52 | const input = ReactDOM.findDOMNode(this) 53 | input.focus() 54 | input.select() 55 | } 56 | render () { 57 | return 58 | } 59 | } 60 | 61 | class TitleField extends Component { 62 | constructor (props) { 63 | super(props) 64 | this.titleInput = React.createRef() 65 | } 66 | 67 | onclick (ev) { 68 | ev.stopPropagation() 69 | ev.preventDefault() 70 | this.setState({ editing: true }) 71 | } 72 | 73 | commit () { 74 | const oldValue = this.props.value 75 | const newValue = ReactDOM.findDOMNode(this.titleInput.current).value 76 | if (oldValue !== newValue) { 77 | this.props.onChange(newValue) 78 | } 79 | this.cancel() 80 | } 81 | 82 | cancel () { 83 | this.setState({ 84 | modified: false, 85 | editing: false 86 | }) 87 | } 88 | 89 | handleKeyup (ev) { 90 | ev.stopPropagation() 91 | 92 | if (ev.key === 'Escape') { 93 | ev.preventDefault() 94 | this.cancel() 95 | return 96 | } 97 | 98 | if (ev.key === 'Enter') { 99 | ev.preventDefault() 100 | this.commit() 101 | return 102 | } 103 | 104 | const oldValue = this.props.value 105 | const newValue = ev.target.value 106 | const modified = oldValue !== newValue 107 | this.setState({ modified }) 108 | } 109 | 110 | render () { 111 | const { writable, value } = this.props 112 | const { editing, modified } = this.state || {} 113 | if (editing && writable) { 114 | return ( 115 |
e.stopPropagation()}> 116 | this.cancel()} /> 117 | 118 | {/* why innerRef in following component? check here - styled-components/styled-components#102 */} 119 | this.handleKeyup(ev)} 123 | ref={this.titleInput} 124 | /> 125 | {modified ? ( 126 | this.commit()}>Save 127 | ) : ( 128 | this.cancel()}>Save 129 | )} 130 | 131 |
132 | ) 133 | } 134 | 135 | if (writable) { 136 | return ( 137 | 138 |

this.onclick(ev)} 142 | > 143 | {value} 144 | 148 |

149 |
150 | ) 151 | } 152 | 153 | return ( 154 |
155 |

{value}

156 |
157 | ) 158 | } 159 | } 160 | 161 | export default TitleField 162 | -------------------------------------------------------------------------------- /app/components/intro.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import React, { Fragment, Component } from 'react' 4 | import styled from 'styled-components' 5 | import { Green as GreenButton, Plain as PlainButton } from './button' 6 | 7 | const Intro = styled.main` 8 | position: relative; 9 | height: 100%; 10 | background-color: var(--color-neutral); 11 | color: var(--color-white); 12 | display: flex; 13 | flex-direction: column; 14 | align-items: center; 15 | justify-content: center; 16 | -webkit-app-region: drag; 17 | overflow: hidden; 18 | ` 19 | const Content = styled.div` 20 | position: relative; 21 | flex: 1; 22 | width: 100%; 23 | padding: 3rem 2rem; 24 | ` 25 | 26 | const Footer = styled.div` 27 | position: relative; 28 | width: 100%; 29 | padding: 1rem; 30 | display: flex; 31 | justify-content: space-between; 32 | button { 33 | min-width: 5rem; 34 | } 35 | ` 36 | const Image = styled.img` 37 | max-width: 100%; 38 | max-height: 100%; 39 | ` 40 | 41 | const StyledDots = styled.div` 42 | display: flex; 43 | justify-content: space-between; 44 | align-items: center; 45 | .dot { 46 | width: 0.5rem; 47 | height: 0.5rem; 48 | margin: 0.25rem; 49 | border-radius: 50%; 50 | background-color: var(--color-black); 51 | } 52 | .active { 53 | background-color: var(--color-blue); 54 | } 55 | ` 56 | 57 | const Dots = ({ screen }) => ( 58 | 59 | {Array(5) 60 | .fill(null) 61 | .map((_, i) => { 62 | const className = i === screen - 1 ? 'dot active' : 'dot' 63 | return
64 | })} 65 | 66 | ) 67 | 68 | class IntroScreen extends Component { 69 | constructor (props) { 70 | super(props) 71 | this.onkeydown = this.onkeydown.bind(this) 72 | } 73 | 74 | onkeydown (ev) { 75 | if (ev.code !== 'Escape') return 76 | window.removeEventListener('keydown', this.onkeydown) 77 | this.props.hide() 78 | } 79 | 80 | componentWillMount () { 81 | window.addEventListener('keydown', this.onkeydown) 82 | } 83 | 84 | componentWillUnmount () { 85 | window.removeEventListener('keydown', this.onkeydown) 86 | } 87 | 88 | render () { 89 | const { show, screen, hide, next, openHomepage } = this.props 90 | 91 | if (!show) { 92 | return ( 93 | 94 |
95 | 96 | ) 97 | } 98 | 99 | return ( 100 | 101 | 106 | 107 | { 108 | { 109 | 1:

Hey there! This is a Dat.

, 110 | 2: ( 111 |

112 | Think of it as a folder – with some magic. 113 |

114 | ), 115 | 3: ( 116 |

117 | You can turn any folder on your computer into a Dat. 118 |

119 | ), 120 | 4: ( 121 |

122 | Dats can be easily shared. Just copy the unique dat link and 123 | securely share it. 124 |

125 | ), 126 | 5: ( 127 |

128 | You can also import existing Dats. Check out{' '} 129 | openHomepage()} 133 | > 134 | datproject.org 135 | {' '} 136 | to explore open datasets. 137 |

138 | ) 139 | }[screen] 140 | } 141 |
142 | {screen === 1 ? ( 143 | next(screen)} 146 | > 147 | Get Started 148 | 149 | ) : ( 150 |
151 | hide()} className='btn-skip'> 152 | Skip Intro 153 | 154 | 155 | {screen < 5 ? ( 156 | next(screen)} className='btn-next'> 157 | Next 158 | 159 | ) : ( 160 | hide()} className='btn-next btn-done'> 161 | Done 162 | 163 | )} 164 |
165 | )} 166 |
167 | ) 168 | } 169 | } 170 | 171 | export default IntroScreen 172 | -------------------------------------------------------------------------------- /app/components/inspect.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import React from 'react' 4 | import styled from 'styled-components' 5 | import { toStr } from 'dat-encoding' 6 | import bytes from 'prettier-bytes' 7 | import Icon from './icon' 8 | import FileList from './file-list' 9 | import { 10 | Plain as PlainButton, 11 | Green as GreenButton, 12 | Text as TextButton 13 | } from './button' 14 | import SCREEN from '../consts/screen' 15 | 16 | const DetailHeader = styled.header` 17 | width: 100%; 18 | height: 4rem; 19 | flex-shrink: 0; 20 | border-bottom: 1px solid var(--color-neutral-20); 21 | ` 22 | 23 | const DetailFooter = styled.footer` 24 | width: 100%; 25 | bottom: 0; 26 | flex-shrink: 0; 27 | border-top: 1px solid var(--color-neutral-20); 28 | ` 29 | 30 | const Label = styled.div` 31 | min-width: 8rem; 32 | color: var(--color-neutral-60); 33 | text-align: right; 34 | padding: 0.25rem; 35 | padding-right: 0.5rem; 36 | ` 37 | 38 | const Column = styled.div` 39 | overflow: hidden; 40 | padding: 0.25rem; 41 | ` 42 | 43 | const Row = ({ label = null, ...props }) => { 44 | return ( 45 |
46 | {label !== null && } 47 | 48 |
49 | ) 50 | } 51 | 52 | const Inspect = ({ 53 | screen, 54 | dat, 55 | closeInspectDat, 56 | addDat, 57 | hideDownloadScreen, 58 | cancelDownloadDat, 59 | changeDownloadPath 60 | }) => { 61 | if (!dat) return null 62 | 63 | const title = 64 | dat.metadata && dat.metadata.title ? dat.metadata.title : dat.key || 'N/A' 65 | const author = 66 | dat.metadata && dat.metadata.author ? dat.metadata.author : 'N/A' 67 | const description = 68 | dat.metadata && dat.metadata.description ? dat.metadata.description : 'N/A' 69 | const size = 70 | dat.stats && Number(dat.stats.length) === dat.stats.length 71 | ? bytes(dat.stats.length) 72 | : bytes(0) 73 | const peers = isNaN(parseInt(dat.peers)) ? '…' : dat.peers 74 | 75 | return ( 76 |
77 | 78 |
79 | 80 |
81 |

{title}

82 |
83 |
84 | 85 | {toStr(dat.key)} 86 | 87 | 88 | {size} 89 | 90 | 91 | {peers} 92 | 93 | 94 | {author} 95 | 96 | 97 | {description} 98 | 99 | 100 |
104 |             {dat.path}
105 |           
106 | changeDownloadPath(dat.key)}> 107 | CHANGE... 108 | 109 |
110 | 116 | N/A
} 119 | /> 120 | 121 |
122 | {screen === SCREEN.INSPECT && ( 123 | 124 |
125 | closeInspectDat()}> 126 | ← Back to Overview 127 | 128 |
129 |
130 | )} 131 | {screen === SCREEN.DOWNLOAD && ( 132 | 133 |
134 | { 136 | addDat({ key: dat.key, path: dat.path }) 137 | hideDownloadScreen() 138 | }} 139 | > 140 | Download 141 | 142 | { 144 | cancelDownloadDat(dat.key) 145 | hideDownloadScreen() 146 | }} 147 | > 148 | Cancel 149 | 150 |
151 |
152 | )} 153 | 154 | ) 155 | } 156 | 157 | export default Inspect 158 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dat-desktop", 3 | "productName": "Dat Desktop", 4 | "version": "3.0.3", 5 | "license": "MIT", 6 | "repository": "datproject/dat-desktop", 7 | "description": "Peer to peer data sharing app built for humans.", 8 | "author": { 9 | "name": "Dat Team", 10 | "email": "community@datproject.org" 11 | }, 12 | "scripts": { 13 | "prestart": "npm run build:dev", 14 | "start": "cross-env NODE_ENV=development npm-run-all -p start:watch start:electron", 15 | "start:electron": "cross-env NODE_V=\"$(node -v)\" NODE_ENV=development electron .", 16 | "start:watch": "webpack --watch --mode=development", 17 | "test": "npm run test:deps && npm run test:lint && npm run test:unit && npm run test:integration", 18 | "test:deps": "cross-env DEBUG=* dependency-check . --detective precinct --entry app/index.js", 19 | "test:lint": "prettier-standard 'app/**/*.js' 'tests/**/*.js' 'lib/**/*.js' 'unit-tests/*.js' && standard", 20 | "test:unit": "cross-env NODE_ENV=test babel-tape-runner -r ./unit-tests/_helpers/*.js unit-tests/*.js", 21 | "test:integration": "npm run build:prod && node ./tests/index.js", 22 | "clean": "npm run clean:git && npm run clean:dirs", 23 | "clean:git": "git clean -dfX", 24 | "clean:dirs": "rm -rf ~/.electron && rm -f package-lock.json", 25 | "update-rdt": "rm -rf dev/react-dev-tools && ced fmkadmapgofadopljbjfkapdkoienihi dev/react-dev-tools", 26 | "build:dev": "webpack --mode=development --progress --profile --colors", 27 | "build:prod": "webpack --mode=production --profile --colors", 28 | "pack": "npm run build:prod && npm run pack:all", 29 | "pack:os": "electron-builder --x64 --ia32 --dir", 30 | "pack:all": "npm run pack:os -- --linux --mac --win", 31 | "dist": "npm run build:prod && npm run dist:os", 32 | "dist:os": "electron-builder --x64 --ia32", 33 | "dist:all": "npm run dist:os -- --linux --mac --win", 34 | "dist:publish": "npm run dist -- --publish onTagOrDraft", 35 | "release": "standard-version" 36 | }, 37 | "standard": { 38 | "ignore": [ 39 | "dev/**" 40 | ] 41 | }, 42 | "devDependencies": { 43 | "@babel/core": "^7.0.0", 44 | "@babel/plugin-proposal-object-rest-spread": "^7.0.0", 45 | "@babel/preset-env": "^7.0.0", 46 | "@babel/preset-react": "^7.0.0", 47 | "babel-loader": "^8.0.0", 48 | "babel-tape-runner": "^3.0.0", 49 | "chrome-ext-downloader": "^1.0.4", 50 | "clipboardy": "^1.2.3", 51 | "cross-env": "^5.1.6", 52 | "del": "^3.0.0", 53 | "dependency-check": "^3.1.0", 54 | "electron": "^6.1.7", 55 | "electron-builder": "^21.2.0", 56 | "enzyme": "^3.3.0", 57 | "enzyme-adapter-react-16": "^1.1.1", 58 | "enzyme-adapter-react-helper": "^1.2.3", 59 | "gaze": "^1.1.3", 60 | "npm-run-all": "^4.1.3", 61 | "precinct": "^5.1.0", 62 | "prettier-standard": "^8.0.1", 63 | "spectron": "^5.0.0", 64 | "standard": "^12.0.1", 65 | "tape": "^4.9.0", 66 | "webpack": "^4.20.2", 67 | "webpack-cli": "^3.1.1", 68 | "webpack-node-externals": "^1.7.2" 69 | }, 70 | "dependencies": { 71 | "dat-colors": "^3.5.1", 72 | "dat-doctor": "^1.4.0", 73 | "dat-encoding": "^5.0.1", 74 | "dat-icons": "^2.5.2", 75 | "dat-node": "^3.5.8", 76 | "electron-default-menu": "^1.0.1", 77 | "minimist": "^1.2.0", 78 | "mirror-folder": "^3.0.0", 79 | "mkdirp-promise": "^5.0.1", 80 | "ms": "^2.1.1", 81 | "polished": "^2.3.0", 82 | "prettier-bytes": "^1.0.4", 83 | "react": "^16.3.2", 84 | "react-dom": "^16.3.2", 85 | "react-file-drop": "^0.2.7", 86 | "react-redux": "^5.0.7", 87 | "react-swap": "^2.0.2", 88 | "redux": "^4.0.0", 89 | "redux-logger": "^3.0.6", 90 | "redux-thunk": "^2.2.0", 91 | "standard-version": "^6.0.1", 92 | "styled-components": "^4.1.1", 93 | "tachyons": "^4.9.1", 94 | "util-promisify": "^2.1.0" 95 | }, 96 | "build": { 97 | "appId": "land.dat.dat-desktop", 98 | "asar": true, 99 | "directories": { 100 | "output": "dist" 101 | }, 102 | "mac": { 103 | "category": "public.app-category.utilities", 104 | "icon": "build/icon.icns" 105 | }, 106 | "dmg": { 107 | "icon": "build/icon.icns", 108 | "contents": [ 109 | { 110 | "x": 220, 111 | "y": 200 112 | }, 113 | { 114 | "x": 448, 115 | "y": 200, 116 | "type": "link", 117 | "path": "/Applications" 118 | } 119 | ] 120 | }, 121 | "linux": { 122 | "category": "Utility", 123 | "packageCategory": "Utility", 124 | "target": [ 125 | "AppImage", 126 | "deb" 127 | ] 128 | }, 129 | "deb": { 130 | "synopsis": "Dat Desktop App" 131 | }, 132 | "win": { 133 | "target": "NSIS", 134 | "icon": "build/icon.png" 135 | }, 136 | "protocols": [ 137 | { 138 | "name": "Dat Link", 139 | "schemes": [ 140 | "dat" 141 | ] 142 | } 143 | ] 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /assets/intro-6.svg: -------------------------------------------------------------------------------- 1 | intro-6 -------------------------------------------------------------------------------- /app/components/table-row.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import React from 'react' 4 | import styled from 'styled-components' 5 | import * as Button from './button' 6 | import Icon from './icon' 7 | import Status from './status' 8 | import bytes from 'prettier-bytes' 9 | import FinderButton from './finder-button' 10 | import HexContent from './hex-content' 11 | import TitleField from './title-field' 12 | 13 | export const Tr = styled.tr` 14 | transition: background-color 0.025s ease-out; 15 | &.selectable { 16 | &:hover, 17 | &:focus { 18 | background-color: var(--color-neutral-04); 19 | cursor: pointer; 20 | } 21 | } 22 | .cell-1 { 23 | width: 4rem; 24 | } 25 | .cell-2 { 26 | width: 14rem; 27 | max-width: 12rem; 28 | @media (min-width: 768px) { 29 | max-width: 20rem; 30 | } 31 | @media (min-width: 1280px) { 32 | max-width: 24rem; 33 | } 34 | } 35 | .cell-3 { 36 | width: 15rem; 37 | } 38 | .cell-4 { 39 | width: 4.5rem; 40 | white-space: nowrap; 41 | } 42 | .cell-5 { 43 | width: 4.5rem; 44 | white-space: nowrap; 45 | } 46 | .cell-6 { 47 | width: 10.25rem; 48 | } 49 | .cell-truncate { 50 | width: 100%; 51 | } 52 | ` 53 | 54 | const IconContainer = styled.div` 55 | .row-action { 56 | height: 1.5rem; 57 | display: inline-block; 58 | color: var(--color-neutral-20); 59 | svg { 60 | vertical-align: middle; 61 | width: 0.75em; 62 | max-height: 1.6em; 63 | margin-top: -0.05em; 64 | margin-right: 5px; 65 | @media (min-width: 960px) { 66 | width: 1.4em; 67 | } 68 | } 69 | &:hover, 70 | &:focus { 71 | outline: none; 72 | color: var(--color-neutral-50); 73 | } 74 | &:first-child { 75 | padding-left: 0; 76 | } 77 | &:last-child { 78 | padding-right: 0; 79 | } 80 | } 81 | .icon-network { 82 | display: inline-block; 83 | color: var(--color-neutral-20); 84 | vertical-align: sub; 85 | width: 1em; 86 | svg polygon { 87 | fill: inherit; 88 | } 89 | } 90 | ` 91 | 92 | const NetworkContainer = styled.td` 93 | vertical-align: top; 94 | svg { 95 | height: 1.5rem; 96 | display: inline-block; 97 | color: var(--color-neutral-20); 98 | vertical-align: top; 99 | width: 1.1em; 100 | max-height: 1.6em; 101 | } 102 | .network-peers-many { 103 | --polygon-1-color: var(--color-green); 104 | --polygon-2-color: var(--color-green); 105 | --polygon-3-color: var(--color-green); 106 | } 107 | .network-peers-1 { 108 | --polygon-1-color: var(--color-yellow); 109 | --polygon-2-color: var(--color-yellow); 110 | } 111 | .network-peers-0 { 112 | --polygon-1-color: var(--color-red); 113 | } 114 | ` 115 | 116 | const NetworkIcon = ({ dat }) => { 117 | const iconClass = `network-peers ${ 118 | dat.peers === 0 119 | ? 'network-peers-0' 120 | : dat.peers === 1 ? 'network-peers-1' : 'network-peers-many' 121 | }` 122 | return 123 | } 124 | 125 | const LinkButton = ({ ...props }) => ( 126 | } 128 | className='row-action btn-link' 129 | title='Copy Link' 130 | {...props} 131 | /> 132 | ) 133 | 134 | const DeleteButton = ({ ...props }) => ( 135 | } 137 | className='row-action mr2 btn-delete' 138 | title='Delete' 139 | {...props} 140 | /> 141 | ) 142 | 143 | const Row = ({ 144 | dat, 145 | shareDat, 146 | onDeleteDat, 147 | inspectDat, 148 | onTogglePause, 149 | updateTitle 150 | }) => { 151 | const { writable, metadata, key } = dat 152 | if (!metadata) return null 153 | const { title } = metadata 154 | const placeholderTitle = `#${key}` 155 | // TODO: inspectDat needs more work! 156 | // onClick={() => inspectDat(dat.key)} className='selectable'> 157 | return ( 158 |
159 | 170 | 187 | 190 | 193 | 194 | 195 | {dat.peers} 196 | 197 | 204 | 205 | ) 206 | } 207 | 208 | export default Row 209 | -------------------------------------------------------------------------------- /assets/intro-5.svg: -------------------------------------------------------------------------------- 1 | intro-5 -------------------------------------------------------------------------------- /dev/react-dev-tools/_metadata/verified_contents.json: -------------------------------------------------------------------------------- 1 | [{"description":"treehash per file","signed_content":{"payload":"eyJjb250ZW50X2hhc2hlcyI6W3siYmxvY2tfc2l6ZSI6NDA5NiwiZGlnZXN0Ijoic2hhMjU2IiwiZmlsZXMiOlt7InBhdGgiOiJidWlsZC9iYWNrZW5kLmpzIiwicm9vdF9oYXNoIjoiVDF0d1pGbnItRTd4NlgtekxmYXllWWl0OEwzbmE1bnBqVEFxcnJVNzNQWSJ9LHsicGF0aCI6ImJ1aWxkL2JhY2tncm91bmQuanMiLCJyb290X2hhc2giOiJwd2o2TFdiZGpDRTV4U3czRmxKT0lXSUhZdFJFWE1jNjIxeG8yZE9EajVRIn0seyJwYXRoIjoiYnVpbGQvY29udGVudFNjcmlwdC5qcyIsInJvb3RfaGFzaCI6Im1WUnMtdEp2eEFRR0pOY2l5ZjFkZjBCeVAyM1hVYlRHNjhXYVZQNE1oalUifSx7InBhdGgiOiJidWlsZC9pbmplY3QuanMiLCJyb290X2hhc2giOiJjVS13V3Q1ckFYdGpzdnNSZUQ3QWxZUVJFam1xOE8wTGk0bU1CSk95U204In0seyJwYXRoIjoiYnVpbGQvbWFpbi5qcyIsInJvb3RfaGFzaCI6InJ4OVdHcG9MSlNGWjh4Zjd0bXBkMXVSYjBzRFBoNkVVS1dOTlo5dkh6WDAifSx7InBhdGgiOiJidWlsZC9wYW5lbC5qcyIsInJvb3RfaGFzaCI6IjdubkM3azBocURUSkVHbFJQWVFFb2JSN1VBcEw5R0pfZFd4dy1XQnBkdEUifSx7InBhdGgiOiJpY29ucy8xMjgtZGVhZGNvZGUucG5nIiwicm9vdF9oYXNoIjoiMUF4MUl2eXRyeUtYQTFzQkZxU1FIWmtjWnJMbVJaNjBvcEVQcWIzSDZiNCJ9LHsicGF0aCI6Imljb25zLzEyOC1kZXZlbG9wbWVudC5wbmciLCJyb290X2hhc2giOiIxQXgxSXZ5dHJ5S1hBMXNCRnFTUUhaa2NackxtUlo2MG9wRVBxYjNINmI0In0seyJwYXRoIjoiaWNvbnMvMTI4LWRpc2FibGVkLnBuZyIsInJvb3RfaGFzaCI6InIta3JhcWpva3FJbHozTTFwQmxiSWk4dGdDXzFHU3hrT2VFMlFwU1FDblUifSx7InBhdGgiOiJpY29ucy8xMjgtb3V0ZGF0ZWQucG5nIiwicm9vdF9oYXNoIjoidVNIdEo2cUFsTHVETWRGYkxnSkpwTHBVSEVEVjRHckRSeVhHcGdSUVNfZyJ9LHsicGF0aCI6Imljb25zLzEyOC1wcm9kdWN0aW9uLnBuZyIsInJvb3RfaGFzaCI6ImF3VDBvNEVpVGowQm1lMFRBUmJaeS1ZSklEcDAzdlBpOG5XU1JPSlFMd2sifSx7InBhdGgiOiJpY29ucy8xMjgtdW5taW5pZmllZC5wbmciLCJyb290X2hhc2giOiIxQXgxSXZ5dHJ5S1hBMXNCRnFTUUhaa2NackxtUlo2MG9wRVBxYjNINmI0In0seyJwYXRoIjoiaWNvbnMvMTYtZGVhZGNvZGUucG5nIiwicm9vdF9oYXNoIjoiX2J4aVZ4bmlCcGUxRG5CSDF0azRDalhkYWp3eGtQM2hSbnBIZDVEQUxFSSJ9LHsicGF0aCI6Imljb25zLzE2LWRldmVsb3BtZW50LnBuZyIsInJvb3RfaGFzaCI6Il9ieGlWeG5pQnBlMURuQkgxdGs0Q2pYZGFqd3hrUDNoUm5wSGQ1REFMRUkifSx7InBhdGgiOiJpY29ucy8xNi1kaXNhYmxlZC5wbmciLCJyb290X2hhc2giOiJ2QWZQVW4xd3UtbU1MclFSTlNrM2pSVUN1V2ZZZWRIUHFKR1p0RUFRV1BZIn0seyJwYXRoIjoiaWNvbnMvMTYtb3V0ZGF0ZWQucG5nIiwicm9vdF9oYXNoIjoiX0VXZVpIeVprMDY0bFAyWF9jWTJHcHF1R0JIanpDcU55M1Z0c3YxXzVhRSJ9LHsicGF0aCI6Imljb25zLzE2LXByb2R1Y3Rpb24ucG5nIiwicm9vdF9oYXNoIjoiR19FcWRrQmtTQ09Tb3M5NHN6bldwUWNhZUhWcFJwN3NRbUNrd3cySUVlRSJ9LHsicGF0aCI6Imljb25zLzE2LXVubWluaWZpZWQucG5nIiwicm9vdF9oYXNoIjoiX2J4aVZ4bmlCcGUxRG5CSDF0azRDalhkYWp3eGtQM2hSbnBIZDVEQUxFSSJ9LHsicGF0aCI6Imljb25zLzMyLWRlYWRjb2RlLnBuZyIsInJvb3RfaGFzaCI6Ijk5WjZZZEtfbEw5aVFZeU1VSElwT1ZSU1dKNm5ZQUQ2Mmx4UkNrMF9kU0kifSx7InBhdGgiOiJpY29ucy8zMi1kZXZlbG9wbWVudC5wbmciLCJyb290X2hhc2giOiI5OVo2WWRLX2xMOWlRWXlNVUhJcE9WUlNXSjZuWUFENjJseFJDazBfZFNJIn0seyJwYXRoIjoiaWNvbnMvMzItZGlzYWJsZWQucG5nIiwicm9vdF9oYXNoIjoiUzN3T25MeFBzd013UGotQXRuNUF1Ny1iTklTREtWbWtlUl9hVDNlSnNXVSJ9LHsicGF0aCI6Imljb25zLzMyLW91dGRhdGVkLnBuZyIsInJvb3RfaGFzaCI6IkJWS0gtbnEyMlB5UDdLZl9lQXFYeDdVM1M5ZzMyRi1wLWxxVVE1NnBGbDAifSx7InBhdGgiOiJpY29ucy8zMi1wcm9kdWN0aW9uLnBuZyIsInJvb3RfaGFzaCI6Il9Fdzc5ODc4bGRYWlVSUGlaemw5NjhPelA5NURURmpNT3l1TlhqejdpUWsifSx7InBhdGgiOiJpY29ucy8zMi11bm1pbmlmaWVkLnBuZyIsInJvb3RfaGFzaCI6Ijk5WjZZZEtfbEw5aVFZeU1VSElwT1ZSU1dKNm5ZQUQ2Mmx4UkNrMF9kU0kifSx7InBhdGgiOiJpY29ucy80OC1kZWFkY29kZS5wbmciLCJyb290X2hhc2giOiJnR3JxVnJhTkl4N2J6M0labTN0MUJxb3BYSFRUQU9td0ZTeHpxdFhoZkI4In0seyJwYXRoIjoiaWNvbnMvNDgtZGV2ZWxvcG1lbnQucG5nIiwicm9vdF9oYXNoIjoiZ0dycVZyYU5JeDdiejNJWm0zdDFCcW9wWEhUVEFPbXdGU3h6cXRYaGZCOCJ9LHsicGF0aCI6Imljb25zLzQ4LWRpc2FibGVkLnBuZyIsInJvb3RfaGFzaCI6InZXMkZBNXlWRzhDRGtNcjBRekVsS1lfQmxTdWxURnB5YkpDdmlIOFplU00ifSx7InBhdGgiOiJpY29ucy80OC1vdXRkYXRlZC5wbmciLCJyb290X2hhc2giOiJ4dFY4U1RBVm42VzUyWXA5aDJxdHdHckdyVzl4LTcxR0U0UUZnMjZ6Rm9vIn0seyJwYXRoIjoiaWNvbnMvNDgtcHJvZHVjdGlvbi5wbmciLCJyb290X2hhc2giOiJHUzczNkQ5TXp3UUhzRW5JOTU4UGxNVnp4RkdGaF92TEt2MFN2VGVTdkdVIn0seyJwYXRoIjoiaWNvbnMvNDgtdW5taW5pZmllZC5wbmciLCJyb290X2hhc2giOiJnR3JxVnJhTkl4N2J6M0labTN0MUJxb3BYSFRUQU9td0ZTeHpxdFhoZkI4In0seyJwYXRoIjoiaWNvbnMvZGVhZGNvZGUuc3ZnIiwicm9vdF9oYXNoIjoick1abk15VkR6VWtRbDdLQl9UVzlTYjlEVjJqVXl5cTQ5WGwwQUhKMVl4TSJ9LHsicGF0aCI6Imljb25zL2RldmVsb3BtZW50LnN2ZyIsInJvb3RfaGFzaCI6InJNWm5NeVZEelVrUWw3S0JfVFc5U2I5RFYyalV5eXE0OVhsMEFISjFZeE0ifSx7InBhdGgiOiJpY29ucy9kaXNhYmxlZC5zdmciLCJyb290X2hhc2giOiJKNU1Rd3Rfb2NmUDJxSFh2UFZ6N2lTTFRmNjdybkJKLXd4S3U2LUhGSE5NIn0seyJwYXRoIjoiaWNvbnMvb3V0ZGF0ZWQuc3ZnIiwicm9vdF9oYXNoIjoiMTkzazMzdmpTTE5CMU5nTXFINGktUjc2YXhHeXktVW1VVFNJYmhadlNKSSJ9LHsicGF0aCI6Imljb25zL3Byb2R1Y3Rpb24uc3ZnIiwicm9vdF9oYXNoIjoiRVB4cHFIQU9IRzlwUUtHMXJYelRJZmN2cV9qTUYxU0xRR3g1b1RkYmFQOCJ9LHsicGF0aCI6Im1haW4uaHRtbCIsInJvb3RfaGFzaCI6InM3OURqWERtdng3bWdySFZNcmpod3JnNFk3SUYyd2JuUUxEcnVDS2hUWU0ifSx7InBhdGgiOiJtYW5pZmVzdC5qc29uIiwicm9vdF9oYXNoIjoicDJEV0RZNVRvcHR0Q0pMcnRJX3BfcjQzTlVYQXRhZ0NtNUgtcVpuY0pZNCJ9LHsicGF0aCI6InBhbmVsLmh0bWwiLCJyb290X2hhc2giOiJJQk9KT3JEeGtBMkFGdUlIT0IyQTJlLThoMUVoV1ZHNUZPVVdwSzNUSUg0In0seyJwYXRoIjoicG9wdXBzL2RlYWRjb2RlLmh0bWwiLCJyb290X2hhc2giOiJzSHBuSUVZeDh5SFoycDk4ejdkclc4dkRTQm1SUWFpd3hqOHJhN3Q0OUpvIn0seyJwYXRoIjoicG9wdXBzL2RldmVsb3BtZW50Lmh0bWwiLCJyb290X2hhc2giOiJMOTQ5M0ptdjVyclRPYmZDWUlObDJiWHJJNEdxandOcXFEbEx5YWpsX3lzIn0seyJwYXRoIjoicG9wdXBzL2Rpc2FibGVkLmh0bWwiLCJyb290X2hhc2giOiJka3FZLXNTZC1Gem85cURpX203WjRsbGdDT3lLRUdQZlRKejBGdjdwRENJIn0seyJwYXRoIjoicG9wdXBzL291dGRhdGVkLmh0bWwiLCJyb290X2hhc2giOiJEY1VNcXRVb0gyYzR5Rjl5QXl2VERwcXc4TVhLdFk0QUo4VVRiSXdkRDNnIn0seyJwYXRoIjoicG9wdXBzL3Byb2R1Y3Rpb24uaHRtbCIsInJvb3RfaGFzaCI6Ikt1UXdjbVQ5dnA4R0ozS3RtQkRjdHlrdDg5ZFMtZW5pMV9SS0ZBdmx3TDgifSx7InBhdGgiOiJwb3B1cHMvc2hhcmVkLmpzIiwicm9vdF9oYXNoIjoiaXIwWkhXM0toUWktRVU3NG03Z3JvdzE5bmYxWk8xZmczeFRjbmZLUnp1SSJ9LHsicGF0aCI6InBvcHVwcy91bm1pbmlmaWVkLmh0bWwiLCJyb290X2hhc2giOiJjb0U5c3ZkeDJZcDgwMFl3a012YVpWRmxvMVV1M2dhcGQ4ZnRZOUhuVWpjIn1dLCJmb3JtYXQiOiJ0cmVlaGFzaCIsImhhc2hfYmxvY2tfc2l6ZSI6NDA5Nn1dLCJpdGVtX2lkIjoiZm1rYWRtYXBnb2ZhZG9wbGpiamZrYXBka29pZW5paGkiLCJpdGVtX3ZlcnNpb24iOiIzLjQuMiIsInByb3RvY29sX3ZlcnNpb24iOjF9","signatures":[{"header":{"kid":"publisher"},"protected":"eyJhbGciOiJSUzI1NiJ9","signature":"l9HNvm0-gZjyrebNPghVRPgTrdN5ipEVo2PhVwMv06YdMpbqHpIetSoWMYj_X4RwZrGJ5xmRF2ZRDtICXOzYifjFeXrxE2Pqzqb8o8LaQYAAkaw_Ts9QppBcUDgW0lfdSbUvDqzdjQxxVHwMuKy4D9t30eZsTH9l4GFVWX1NQNUBqtoV_b71daiwPcf1rTr59XrBUEx8SXeuDMY-CSKI2wt9III8vdcXm7FQv9HsPA0bkmaJXPMpFBU-iL6pTreUgtS3m-ZxB6xDPU_ctTsRkoKtPby7ljyc0ARAcyYEkoIY12hlb0hyqC0OR3EMO_K_rd_4LFlsU6NZAiEjRsGxVA"},{"header":{"kid":"webstore"},"protected":"eyJhbGciOiJSUzI1NiJ9","signature":"My_ktGjIASNiBZ4CZVTMsbGSr61XDMzUxfpJMCdXvR3Wa0kkFBGi6k80BzvcKS8fR45xIp1q6VVEpN_yqkuAHzsno4NGJI9ZOKNYh5Hf0Tga4bGwwisw5Y5SyqgRb9pJ01j6Y-fhzmWrcqPflAtcz-DQLCYUrbQQIbzUgF9AwyvxlYHATH_lr__2Q67JEuTHhY4macOKLESdhRM5lwbfyBrlGph6oDL6KtaHqAqtEYsqDMi6jcnoUAQBe4HzER4xZb4QimBagLBVCIH-HRKrh2ZYb_LOgv4paEz0e8nXd2Hg2b1C5kv5I6eyU2i3aZAYpVcra4pk3DV9p3OqcDprPA"}]}}] -------------------------------------------------------------------------------- /app/components/dialog.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import React from 'react' 4 | import styled from 'styled-components' 5 | import Icon from './icon' 6 | import * as Button from './button' 7 | 8 | const Overlay = styled.div` 9 | background: rgba(0, 0, 0, 0.25); 10 | ` 11 | 12 | const Inner = styled.div` 13 | min-width: 25rem; 14 | max-width: 32rem; 15 | padding: 2rem 2.5rem 2rem; 16 | background-color: var(--color-white); 17 | box-shadow: 0 1.2rem 2.4rem rgba(0, 0, 0, 0.5); 18 | .exit { 19 | border: none; 20 | color: var(--color-neutral-40); 21 | } 22 | .exit:hover, 23 | .exit:focus { 24 | color: var(--color-neutral); 25 | } 26 | .icon-cross { 27 | vertical-align: middle; 28 | width: 1.6em; 29 | max-height: 1.6em; 30 | transition: color 0.025s ease-out; 31 | margin-right: auto; 32 | margin-left: auto; 33 | } 34 | ` 35 | 36 | const LabelInput = styled.label` 37 | --input-height: 3rem; 38 | --icon-height: 1.2rem; 39 | --button-width: 3rem; 40 | height: var(--input-height); 41 | border: 0; 42 | .btn-copy-to-clipboard { 43 | width: var(--button-width); 44 | height: calc(var(--input-height) - 2px); 45 | top: 1px; 46 | right: 1px; 47 | bottom: 1px; 48 | background-color: var(--color-neutral-10); 49 | border: none; 50 | color: var(--color-neutral-30); 51 | &:hover, 52 | &:focus { 53 | outline: none; 54 | color: var(--color-green-hover); 55 | } 56 | } 57 | .icon-link, 58 | .icon-clipboard { 59 | position: absolute; 60 | top: 0; 61 | bottom: 0; 62 | padding-top: calc(var(--icon-height) - 0.35rem); 63 | padding-left: 0.75rem; 64 | pointer-events: none; 65 | display: block; 66 | width: var(--icon-height); 67 | height: var(--icon-height); 68 | transition: color 0.025s ease-out; 69 | } 70 | .icon-link { 71 | left: 0; 72 | color: var(--color-neutral-30); 73 | } 74 | .icon-clipboard { 75 | right: 0.8rem; 76 | } 77 | .dat-input-input { 78 | width: 100%; 79 | height: var(--input-height); 80 | padding-right: var(--button-width); 81 | padding-left: 2.5rem; 82 | font-size: 1rem; 83 | font-weight: 600; 84 | border: 1px solid var(--color-neutral-20); 85 | background-color: var(--color-white); 86 | color: var(--color-green-hover); 87 | overflow: hidden; 88 | white-space: nowrap; 89 | text-overflow: ellipsis; 90 | &:hover, 91 | &:focus { 92 | outline: none; 93 | } 94 | } 95 | .dat-input-check { 96 | color: var(--color-blue); 97 | top: 2rem; 98 | } 99 | .icon-check { 100 | width: var(--icon-height); 101 | height: 0.875rem; 102 | vertical-align: -0.15rem; 103 | display: inline-block; 104 | } 105 | .confirmation { 106 | right: 0; 107 | opacity: 0; 108 | top: -0.5rem; 109 | color: var(--color-blue); 110 | } 111 | .show-confirmation { 112 | top: -1.2rem; 113 | opacity: 1; 114 | transition: all 0.15s ease-out; 115 | } 116 | ` 117 | 118 | function CloseButton ({ onExit }) { 119 | return ( 120 | 128 | ) 129 | } 130 | 131 | export const Link = ({ link, copied, onCopy, onExit }) => ( 132 | 136 | 137 |

Copy Dat Link

138 | 139 |

144 | 145 | Link copied to clipboard 146 |

147 | 153 | 154 | 162 |
163 |

164 | Anyone with this link can view your Dat. 165 |

166 | 167 |
168 |
169 | ) 170 | 171 | export const Confirm = ({ dat, onConfirm, onExit }) => ( 172 | 176 | 177 |

Remove Dat

178 |

179 | Are you sure you want to remove this dat? 180 |
181 | This can’t be undone. 182 |

183 |

184 | onConfirm(dat)} 187 | > 188 | Yes, Remove Dat 189 | 190 | 191 | No, Cancel 192 | 193 |

194 | 195 |
196 |
197 | ) 198 | 199 | export const Alert = ({ alert, onExit }) => ( 200 | 204 | 205 |

{alert}

206 |

207 | 208 | CLOSE 209 | 210 |

211 | 212 |
213 |
214 | ) 215 | -------------------------------------------------------------------------------- /tests/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var clipboard = require('clipboardy') 3 | var spectron = require('spectron') 4 | var path = require('path') 5 | var test = require('tape').test 6 | var del = require('del') 7 | var { exec } = require('child_process') 8 | var props = require('bluebird').props 9 | var waitForAndClick = require('./utils/waitForAndClick') 10 | var waitForMatch = require('./utils/waitForMatch') 11 | var wait = require('./utils/wait') 12 | 13 | var TEST_DATA = path.join(__dirname, 'test_data') 14 | var TEST_DATA_DB = path.join(TEST_DATA, 'multidat.json') 15 | var FIXTURES = path.join(__dirname, 'fixtures') 16 | 17 | test('init', function (t) { 18 | t.test('should be able to boot up the app', function (t) { 19 | const app = createApp() 20 | return init(app, t) 21 | .then(() => 22 | props({ 23 | getWindowCount: app.client.getWindowCount(), 24 | isMinimized: app.browserWindow.isMinimized(), 25 | isDevToolsOpened: app.browserWindow.isDevToolsOpened(), 26 | isFocused: app.browserWindow.isFocused(), 27 | bounds: app.browserWindow 28 | .getBounds() 29 | .then(bounds => ({ width: bounds.width, height: bounds.height })) 30 | }) 31 | ) 32 | .then(props => 33 | t.deepEquals( 34 | props, 35 | { 36 | getWindowCount: 1, 37 | isMinimized: false, 38 | isDevToolsOpened: false, 39 | isFocused: true, 40 | bounds: { 41 | width: 800, 42 | height: 600 43 | } 44 | }, 45 | 'All settings match' 46 | ) 47 | ) 48 | .catch(e => t.fail(e)) 49 | .then(() => endTest(app, t)) 50 | }) 51 | t.end() 52 | }) 53 | 54 | test('onboarding', function (t) { 55 | t.test( 56 | 'intro should show every time you open the app as long as you have no dats', 57 | function (t) { 58 | var app = createApp() 59 | return initAndSkipIntro(app, t) 60 | .then(() => app.stop()) 61 | .then(() => Promise.resolve((app = createApp()))) 62 | .then(() => waitForLoad(app, t)) 63 | .then(() => 64 | app.browserWindow 65 | .isVisible() 66 | .then(isVisible => t.ok(isVisible, 'App is visible.')) 67 | ) 68 | .then(() => waitForAndClick(t, app, '.btn-get-started')) 69 | .then(() => app.client.waitForExist('.btn-skip')) 70 | .catch(e => t.fail(e)) 71 | .then(() => endTest(app, t)) 72 | } 73 | ) 74 | t.end() 75 | }) 76 | 77 | function initAndSkipIntro (app, t) { 78 | return init(app, t) 79 | .then(() => 80 | app.browserWindow 81 | .getTitle() 82 | .then(title => 83 | t.equals( 84 | title, 85 | 'Dat Desktop | Welcome', 86 | 'intro title shown in the beginning' 87 | ) 88 | ) 89 | ) 90 | .then(() => waitForAndClick(t, app, '.btn-get-started')) 91 | .then(() => waitForAndClick(t, app, '.btn-skip')) 92 | .then(() => 93 | app.browserWindow 94 | .getTitle() 95 | .then(title => 96 | t.equals(title, 'Dat Desktop', 'dat title shown after the intro') 97 | ) 98 | ) 99 | } 100 | 101 | test('working with dats', function (t) { 102 | var app = createApp() 103 | return initAndSkipIntro(app, t) 104 | .then(() => waitForAndClick(t, app, '.btn-share-folder')) 105 | .then(() => 106 | Promise.all([ 107 | waitForMatch(t, app, '.network', /0/), 108 | waitForMatch(t, app, '.size', /([1-9]\d*) B/) 109 | ]) 110 | ) 111 | .then(() => 112 | Promise.all([ 113 | clipboard.write('').then(() => t.ok(true, 'Cleared clipboard')), 114 | waitForAndClick(t, app, '.btn-link') 115 | ]) 116 | ) 117 | .then(() => waitForAndClick(t, app, '.btn-copy-to-clipboard')) 118 | .then(() => wait(200)) 119 | .then(() => 120 | clipboard 121 | .read() 122 | .then(text => 123 | t.ok( 124 | text.match(/^dat:\/\/[0-9a-f]{32}/), 125 | 'link copied to clipboard: ' + text 126 | ) 127 | ) 128 | ) 129 | .then(() => app.stop()) 130 | .then(() => Promise.resolve((app = createApp()))) 131 | .then(() => waitForLoad(app, t)) 132 | .then(() => 133 | app.browserWindow 134 | .isVisible() 135 | .then(isVisible => t.equal(isVisible, true, 'reloaded and is visible')) 136 | ) 137 | .then(() => 138 | waitForMatch(t, app, '.size', /([1-9]\d*) B/) 139 | ) 140 | .then(() => waitForAndClick(t, app, '.btn-delete')) 141 | .then(() => waitForAndClick(t, app, '.btn-cancel')) 142 | .then(() => waitForAndClick(t, app, '.btn-delete')) 143 | .then(() => waitForAndClick(t, app, '.btn-confirm')) 144 | .then(() => 145 | waitForMatch( 146 | t, 147 | app, 148 | '.tutorial', 149 | /share/i, 150 | 'now the dat is gone and welcome screen is back' 151 | ) 152 | ) // now the dat is gone and welcome screen is back 153 | .catch(e => t.fail(e)) 154 | .then(() => endTest(app, t)) 155 | }) 156 | 157 | // Create a new app instance 158 | function createApp (t) { 159 | let electronPath = path.resolve(__dirname, '..', 'node_modules', '.bin', 'electron') 160 | if (process.platform === 'win32') { 161 | electronPath += '.cmd' 162 | } 163 | const index = path.join(__dirname, '..', 'index.js') 164 | var app = new spectron.Application({ 165 | path: electronPath, 166 | args: [index, '--data', TEST_DATA, '--db', TEST_DATA_DB], 167 | env: { 168 | NODE_ENV: 'test', 169 | RUNNING_IN_SPECTRON: true, 170 | OPEN_RESULT: FIXTURES 171 | } 172 | }) 173 | process.on('SIGTERM', () => endTest(app)) 174 | return app 175 | } 176 | 177 | function clear () { 178 | return Promise.all([ 179 | del(FIXTURES).then( 180 | () => 181 | new Promise((resolve, reject) => 182 | exec( 183 | `git checkout -- "${FIXTURES}"`, 184 | error => (error ? reject(error) : resolve()) 185 | ) 186 | ) 187 | ), 188 | del(TEST_DATA) 189 | ]) 190 | } 191 | 192 | function init (app, t) { 193 | return clear().then(() => waitForLoad(app, t)) 194 | } 195 | 196 | // Starts the app, waits for it to load, returns a promise 197 | function waitForLoad (app, t) { 198 | return app 199 | .start() 200 | .then(() => { 201 | app.client.waitUntilWindowLoaded() 202 | }) 203 | .then(function () { 204 | // Switch to the main window 205 | return app.client.windowByIndex(0) 206 | }) 207 | .then(() => { 208 | app.client.waitUntilWindowLoaded() 209 | }) 210 | .then(() => { 211 | app.browserWindow.focus() 212 | app.browserWindow 213 | .isVisible() 214 | .then(isVisible => { 215 | t.ok(isVisible, 'isVisible') 216 | }) 217 | }) 218 | .then(() => app) 219 | } 220 | 221 | // Quit the app, end the test, either in success (!err) or failure (err) 222 | function endTest (app, t) { 223 | return app 224 | .stop() 225 | .then(() => clear()) 226 | .then(() => t && t.end()) 227 | } 228 | -------------------------------------------------------------------------------- /app/reducers/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import SCREEN from '../consts/screen' 4 | import { generateDefaultState } from '../consts/state' 5 | 6 | const redatApp = (state = generateDefaultState(), action) => { 7 | switch (action.type) { 8 | case 'NEXT_INTRO': 9 | return { 10 | ...state, 11 | intro: { 12 | screen: action.screen + 1 13 | } 14 | } 15 | case 'HIDE_INTRO': 16 | document.title = 'Dat Desktop' 17 | return { 18 | ...state, 19 | screen: SCREEN.DATS 20 | } 21 | case 'SHOW_DOWNLOAD_SCREEN': 22 | return { 23 | ...state, 24 | screen: SCREEN.DOWNLOAD, 25 | downloadDatKey: action.key 26 | } 27 | case 'HIDE_DOWNLOAD_SCREEN': 28 | return { 29 | ...state, 30 | screen: SCREEN.DATS, 31 | downloadDatKey: null 32 | } 33 | case 'CHANGE_DOWNLOAD_PATH': 34 | return { 35 | ...state, 36 | dats: { 37 | ...state.dats, 38 | [action.key]: { 39 | ...state.dats[action.key], 40 | path: action.path 41 | } 42 | } 43 | } 44 | case 'ADD_DAT': 45 | return { 46 | ...state, 47 | dats: { 48 | ...state.dats, 49 | [action.key]: { 50 | key: action.key, 51 | path: action.path, 52 | loading: true, 53 | paused: action.paused, 54 | metadata: {}, 55 | stats: { 56 | network: { 57 | up: 0, 58 | down: 0 59 | } 60 | } 61 | } 62 | }, 63 | screen: SCREEN.DATS 64 | } 65 | case 'ADD_DAT_ERROR:EXISTED': 66 | return { 67 | ...state, 68 | dialogs: { 69 | ...state.dialogs, 70 | alert: 'The DAT is already in the list.' 71 | } 72 | } 73 | case 'ADD_DAT_ERROR': 74 | case 'WRITE_METADATA_ERROR': 75 | return { 76 | ...state, 77 | dats: { 78 | ...state.dats, 79 | [action.key]: { 80 | ...state.dats[action.key], 81 | error: action.error, 82 | loading: false 83 | } 84 | } 85 | } 86 | case 'ADD_DAT_SUCCESS': 87 | return { 88 | ...state, 89 | dats: { 90 | ...state.dats, 91 | [action.key]: { 92 | ...state.dats[action.key], 93 | loading: false 94 | } 95 | } 96 | } 97 | case 'REMOVE_DAT': 98 | if (state.dats[action.key]) { 99 | const { [action.key]: del, ...dats } = state.dats 100 | return { ...state, dats } 101 | } 102 | return state 103 | case 'INSPECT_DAT': 104 | return { 105 | ...state, 106 | screen: SCREEN.INSPECT, 107 | inspect: { 108 | key: action.key 109 | } 110 | } 111 | case 'INSPECT_DAT_CLOSE': 112 | return { 113 | ...state, 114 | screen: SCREEN.DATS 115 | } 116 | case 'DAT_FILES': 117 | return { 118 | ...state, 119 | dats: { 120 | ...state.dats, 121 | [action.key]: { 122 | ...state.dats[action.key], 123 | files: action.files 124 | } 125 | } 126 | } 127 | case 'DAT_METADATA': 128 | return { 129 | ...state, 130 | dats: { 131 | ...state.dats, 132 | [action.key]: { 133 | ...state.dats[action.key], 134 | metadata: action.metadata 135 | } 136 | } 137 | } 138 | case 'DAT_WRITABLE': 139 | return { 140 | ...state, 141 | dats: { 142 | ...state.dats, 143 | [action.key]: { 144 | ...state.dats[action.key], 145 | writable: action.writable 146 | } 147 | } 148 | } 149 | case 'DAT_STATS': 150 | return { 151 | ...state, 152 | dats: { 153 | ...state.dats, 154 | [action.key]: { 155 | ...state.dats[action.key], 156 | stats: { ...state.dats[action.key].stats, ...action.stats } 157 | } 158 | } 159 | } 160 | case 'DAT_PROGRESS': 161 | return { 162 | ...state, 163 | dats: { 164 | ...state.dats, 165 | [action.key]: { 166 | ...state.dats[action.key], 167 | progress: action.progress 168 | } 169 | } 170 | } 171 | case 'DAT_STATE': 172 | return { 173 | ...state, 174 | dats: { 175 | ...state.dats, 176 | [action.key]: { 177 | ...state.dats[action.key], 178 | state: action.state 179 | } 180 | } 181 | } 182 | case 'DAT_NETWORK_STATS': 183 | return { 184 | ...state, 185 | dats: { 186 | ...state.dats, 187 | [action.key]: { 188 | ...state.dats[action.key], 189 | stats: { ...state.dats[action.key].stats, network: action.stats } 190 | } 191 | }, 192 | speed: { 193 | up: 194 | state.speed.up - 195 | state.dats[action.key].stats.network.up + 196 | action.stats.up, 197 | down: 198 | state.speed.down - 199 | state.dats[action.key].stats.network.down + 200 | action.stats.down 201 | } 202 | } 203 | case 'DAT_PEERS': 204 | return { 205 | ...state, 206 | dats: { 207 | ...state.dats, 208 | [action.key]: { 209 | ...state.dats[action.key], 210 | peers: action.peers 211 | } 212 | } 213 | } 214 | case 'UPDATE_TITLE': 215 | return { 216 | ...state, 217 | dats: { 218 | ...state.dats, 219 | [action.key]: { 220 | ...state.dats[action.key], 221 | metadata: { 222 | ...state.dats[action.key].metadata, 223 | title: action.title 224 | } 225 | } 226 | } 227 | } 228 | case 'DIALOGS_LINK_OPEN': 229 | return { 230 | ...state, 231 | dialogs: { 232 | ...state.dialogs, 233 | link: { 234 | link: action.key, 235 | copied: false 236 | } 237 | } 238 | } 239 | case 'DIALOGS_LINK_COPY': 240 | return { 241 | ...state, 242 | dialogs: { 243 | ...state.dialogs, 244 | link: { 245 | ...state.dialogs.link, 246 | copied: true 247 | } 248 | } 249 | } 250 | case 'DIALOGS_LINK_CLOSE': 251 | return { 252 | ...state, 253 | dialogs: { 254 | ...state.dialogs, 255 | link: { 256 | link: null, 257 | copied: false 258 | } 259 | } 260 | } 261 | case 'DIALOGS_DELETE_OPEN': 262 | return { 263 | ...state, 264 | dialogs: { 265 | ...state.dialogs, 266 | delete: { 267 | dat: action.key 268 | } 269 | } 270 | } 271 | case 'DIALOGS_DELETE_CLOSE': 272 | return { 273 | ...state, 274 | screen: SCREEN.DATS, 275 | dialogs: { 276 | ...state.dialogs, 277 | delete: { 278 | dat: null 279 | } 280 | } 281 | } 282 | case 'DIALOGS_ALERT_CLOSE': 283 | return { 284 | ...state, 285 | dialogs: { 286 | ...state.dialogs, 287 | alert: null 288 | } 289 | } 290 | case 'TOGGLE_PAUSE': 291 | const dat = state.dats[action.key] 292 | return { 293 | ...state, 294 | dats: { 295 | ...state.dats, 296 | [action.key]: { 297 | ...dat, 298 | paused: !action.paused, 299 | peers: !action.paused ? 0 : dat.peers 300 | } 301 | } 302 | } 303 | case 'TOGGLE_MENU': 304 | return { 305 | ...state, 306 | menu: { 307 | ...state.menu, 308 | visible: action.visible 309 | } 310 | } 311 | default: 312 | return state 313 | } 314 | } 315 | 316 | export default redatApp 317 | -------------------------------------------------------------------------------- /dev/react-dev-tools/build/inject.js: -------------------------------------------------------------------------------- 1 | !function(modules) { 2 | function __webpack_require__(moduleId) { 3 | if (installedModules[moduleId]) return installedModules[moduleId].exports; 4 | var module = installedModules[moduleId] = { 5 | exports: {}, 6 | id: moduleId, 7 | loaded: !1 8 | }; 9 | return modules[moduleId].call(module.exports, module, module.exports, __webpack_require__), 10 | module.loaded = !0, module.exports; 11 | } 12 | var installedModules = {}; 13 | return __webpack_require__.m = modules, __webpack_require__.c = installedModules, 14 | __webpack_require__.p = "", __webpack_require__(0); 15 | }([ function(module, exports, __webpack_require__) { 16 | "use strict"; 17 | var lastDetectionResult, installGlobalHook = __webpack_require__(1), installRelayHook = __webpack_require__(2), nullthrows = __webpack_require__(3)["default"]; 18 | window.addEventListener("message", function(evt) { 19 | evt.source === window && evt.data && "react-devtools-detector" === evt.data.source && (lastDetectionResult = { 20 | hasDetectedReact: !0, 21 | reactBuildType: evt.data.reactBuildType 22 | }, chrome.runtime.sendMessage(lastDetectionResult)); 23 | }), window.addEventListener("pageshow", function(evt) { 24 | lastDetectionResult && evt.target === window.document && chrome.runtime.sendMessage(lastDetectionResult); 25 | }); 26 | var detectReact = "\nwindow.__REACT_DEVTOOLS_GLOBAL_HOOK__.on('renderer', function(evt) {\n window.postMessage({\n source: 'react-devtools-detector',\n reactBuildType: evt.reactBuildType,\n }, '*');\n});\n", saveNativeValues = "\nwindow.__REACT_DEVTOOLS_GLOBAL_HOOK__.nativeObjectCreate = Object.create;\nwindow.__REACT_DEVTOOLS_GLOBAL_HOOK__.nativeMap = Map;\nwindow.__REACT_DEVTOOLS_GLOBAL_HOOK__.nativeWeakMap = WeakMap;\nwindow.__REACT_DEVTOOLS_GLOBAL_HOOK__.nativeSet = Set;\n", js = ";(" + installGlobalHook.toString() + "(window));(" + installRelayHook.toString() + "(window))" + saveNativeValues + detectReact, script = document.createElement("script"); 27 | script.textContent = js, nullthrows(document.documentElement).appendChild(script), 28 | nullthrows(script.parentNode).removeChild(script); 29 | }, function(module, exports) { 30 | "use strict"; 31 | function installGlobalHook(window) { 32 | function detectReactBuildType(renderer) { 33 | try { 34 | if ("string" == typeof renderer.version) return renderer.bundleType > 0 ? "development" : "production"; 35 | var toString = Function.prototype.toString; 36 | if (renderer.Mount && renderer.Mount._renderNewRootComponent) { 37 | var renderRootCode = toString.call(renderer.Mount._renderNewRootComponent); 38 | return 0 !== renderRootCode.indexOf("function") ? "production" : renderRootCode.indexOf("storedMeasure") !== -1 ? "development" : renderRootCode.indexOf("should be a pure function") !== -1 ? renderRootCode.indexOf("NODE_ENV") !== -1 ? "development" : renderRootCode.indexOf("development") !== -1 ? "development" : renderRootCode.indexOf("true") !== -1 ? "development" : renderRootCode.indexOf("nextElement") !== -1 || renderRootCode.indexOf("nextComponent") !== -1 ? "unminified" : "development" : renderRootCode.indexOf("nextElement") !== -1 || renderRootCode.indexOf("nextComponent") !== -1 ? "unminified" : "outdated"; 39 | } 40 | } catch (err) {} 41 | return "production"; 42 | } 43 | if (!window.__REACT_DEVTOOLS_GLOBAL_HOOK__) { 44 | var hasDetectedBadDCE = !1, hook = { 45 | _renderers: {}, 46 | helpers: {}, 47 | checkDCE: function(fn) { 48 | try { 49 | var toString = Function.prototype.toString, code = toString.call(fn); 50 | code.indexOf("^_^") > -1 && (hasDetectedBadDCE = !0, setTimeout(function() { 51 | throw new Error("React is running in production mode, but dead code elimination has not been applied. Read how to correctly configure React for production: https://fb.me/react-perf-use-the-production-build"); 52 | })); 53 | } catch (err) {} 54 | }, 55 | inject: function(renderer) { 56 | var id = Math.random().toString(16).slice(2); 57 | hook._renderers[id] = renderer; 58 | var reactBuildType = hasDetectedBadDCE ? "deadcode" : detectReactBuildType(renderer); 59 | return hook.emit("renderer", { 60 | id: id, 61 | renderer: renderer, 62 | reactBuildType: reactBuildType 63 | }), id; 64 | }, 65 | _listeners: {}, 66 | sub: function(evt, fn) { 67 | return hook.on(evt, fn), function() { 68 | return hook.off(evt, fn); 69 | }; 70 | }, 71 | on: function(evt, fn) { 72 | hook._listeners[evt] || (hook._listeners[evt] = []), hook._listeners[evt].push(fn); 73 | }, 74 | off: function(evt, fn) { 75 | if (hook._listeners[evt]) { 76 | var ix = hook._listeners[evt].indexOf(fn); 77 | ix !== -1 && hook._listeners[evt].splice(ix, 1), hook._listeners[evt].length || (hook._listeners[evt] = null); 78 | } 79 | }, 80 | emit: function(evt, data) { 81 | hook._listeners[evt] && hook._listeners[evt].map(function(fn) { 82 | return fn(data); 83 | }); 84 | }, 85 | supportsFiber: !0, 86 | _fiberRoots: {}, 87 | getFiberRoots: function(rendererID) { 88 | var roots = hook._fiberRoots; 89 | return roots[rendererID] || (roots[rendererID] = new Set()), roots[rendererID]; 90 | }, 91 | onCommitFiberUnmount: function(rendererID, fiber) { 92 | hook.helpers[rendererID] && hook.helpers[rendererID].handleCommitFiberUnmount(fiber); 93 | }, 94 | onCommitFiberRoot: function(rendererID, root) { 95 | var mountedRoots = hook.getFiberRoots(rendererID), current = root.current, isKnownRoot = mountedRoots.has(root), isUnmounting = null == current.memoizedState || null == current.memoizedState.element; 96 | isKnownRoot || isUnmounting ? isKnownRoot && isUnmounting && mountedRoots["delete"](root) : mountedRoots.add(root), 97 | hook.helpers[rendererID] && hook.helpers[rendererID].handleCommitFiberRoot(root); 98 | } 99 | }; 100 | Object.defineProperty(window, "__REACT_DEVTOOLS_GLOBAL_HOOK__", { 101 | value: hook 102 | }); 103 | } 104 | } 105 | module.exports = installGlobalHook; 106 | }, function(module, exports) { 107 | "use strict"; 108 | function installRelayHook(window) { 109 | function decorate(obj, attr, fn) { 110 | var old = obj[attr]; 111 | obj[attr] = function() { 112 | var res = old.apply(this, arguments); 113 | return fn.apply(this, arguments), res; 114 | }; 115 | } 116 | function emit(name, data) { 117 | _eventQueue.push({ 118 | name: name, 119 | data: data 120 | }), _listener && _listener(name, data); 121 | } 122 | function setRequestListener(listener) { 123 | if (_listener) throw new Error("Relay Devtools: Called only call setRequestListener once."); 124 | return _listener = listener, _eventQueue.forEach(function(_ref) { 125 | var name = _ref.name, data = _ref.data; 126 | listener(name, data); 127 | }), function() { 128 | _listener = null; 129 | }; 130 | } 131 | function recordRequest(type, start, request, requestNumber) { 132 | var id = Math.random().toString(16).substr(2); 133 | request.getPromise().then(function(response) { 134 | emit("relay:success", { 135 | id: id, 136 | end: performanceNow(), 137 | response: response.response 138 | }); 139 | }, function(error) { 140 | emit("relay:failure", { 141 | id: id, 142 | end: performanceNow(), 143 | error: error 144 | }); 145 | }); 146 | for (var textChunks = [], text = request.getQueryString(); text.length > 0; ) textChunks.push(text.substr(0, TEXT_CHUNK_LENGTH)), 147 | text = text.substr(TEXT_CHUNK_LENGTH); 148 | return { 149 | id: id, 150 | name: request.getDebugName(), 151 | requestNumber: requestNumber, 152 | start: start, 153 | text: textChunks, 154 | type: type, 155 | variables: request.getVariables() 156 | }; 157 | } 158 | function instrumentRelayRequests(relayInternals) { 159 | var NetworkLayer = relayInternals.NetworkLayer; 160 | decorate(NetworkLayer, "sendMutation", function(mutation) { 161 | requestNumber++, emit("relay:pending", [ recordRequest("mutation", performanceNow(), mutation, requestNumber) ]); 162 | }), decorate(NetworkLayer, "sendQueries", function(queries) { 163 | requestNumber++; 164 | var start = performanceNow(); 165 | emit("relay:pending", queries.map(function(query) { 166 | return recordRequest("query", start, query, requestNumber); 167 | })); 168 | }); 169 | var instrumented = {}; 170 | for (var key in relayInternals) relayInternals.hasOwnProperty(key) && (instrumented[key] = relayInternals[key]); 171 | return instrumented.setRequestListener = setRequestListener, instrumented; 172 | } 173 | var performanceNow, performance = window.performance; 174 | performanceNow = performance && "function" == typeof performance.now ? function() { 175 | return performance.now(); 176 | } : function() { 177 | return Date.now(); 178 | }; 179 | var TEXT_CHUNK_LENGTH = 500, hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__; 180 | if (hook) { 181 | var _eventQueue = [], _listener = null, requestNumber = 0, _relayInternals = null; 182 | Object.defineProperty(hook, "_relayInternals", { 183 | configurable: !0, 184 | set: function(relayInternals) { 185 | _relayInternals = instrumentRelayRequests(relayInternals); 186 | }, 187 | get: function() { 188 | return _relayInternals; 189 | } 190 | }); 191 | } 192 | } 193 | module.exports = installRelayHook; 194 | }, function(module, exports) { 195 | "use strict"; 196 | Object.defineProperty(exports, "__esModule", { 197 | value: !0 198 | }), exports["default"] = function(x) { 199 | if (null != x) return x; 200 | throw new Error("Got unexpected null or undefined"); 201 | }; 202 | } ]); -------------------------------------------------------------------------------- /app/actions/dat-middleware.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import Dat from 'dat-node' 4 | import { encode } from 'dat-encoding' 5 | import fs from 'fs' 6 | import { basename, join as joinPath } from 'path' 7 | import { ipcRenderer, shell } from 'electron' 8 | import mkdirp from 'mkdirp-promise' 9 | import mirror from 'mirror-folder' 10 | import promisify from 'util-promisify' 11 | 12 | const readFile = promisify(fs.readFile) 13 | const writeFile = promisify(fs.writeFile) 14 | const { Notification } = window 15 | 16 | async function readJSON (file) { 17 | try { 18 | const blob = await readFile(file, 'utf8') 19 | if (!blob) { 20 | return {} 21 | } 22 | return JSON.parse(blob) 23 | } catch (_) {} 24 | return {} 25 | } 26 | 27 | export default class DatMiddleware { 28 | constructor ({ downloadsDir, dataDir }) { 29 | this.downloadsDir = downloadsDir 30 | this.dataDir = dataDir 31 | this.dats = {} 32 | this.listeners = [] 33 | this.execByType = { 34 | UPDATE_TITLE: action => this.updateTitle(action), 35 | REMOVE_DAT: action => this.removeDat(action), 36 | TRY_ADD_DAT: action => this.tryAddDat(action), 37 | REQUEST_DOWNLOAD: action => this.validateDownloadRequest(action), 38 | TOGGLE_PAUSE: action => this.togglePause(action), 39 | DOWNLOAD_SPARSE_DAT: action => this.downloadSparseDat(action), 40 | CANCEL_DOWNLOAD_DAT: action => this.cancelDownloadDat(action) 41 | } 42 | } 43 | 44 | async validateDownloadRequest ({ key }) { 45 | if (key) { 46 | key = encode(key) 47 | if (this.dats[key]) { 48 | return this.dispatch({ 49 | type: 'ADD_DAT_ERROR', 50 | key, 51 | error: new Error('Dat with same key already exists.') 52 | }) 53 | } 54 | } 55 | this.dispatch({ type: 'SHOW_DOWNLOAD_SCREEN', key }) 56 | } 57 | 58 | execAction (action) { 59 | const exec = this.execByType[action.type] 60 | if (!exec) return false 61 | // Telling it to all the middlewares. 62 | this.dispatch(action) 63 | exec(action) 64 | .then(data => 65 | this.dispatch({ ...action, type: `${action.type}_SUCCESS`, ...data }) 66 | ) 67 | .catch(error => 68 | this.dispatch({ ...action, type: `${action.type}_ERROR`, error }) 69 | ) 70 | return true 71 | } 72 | 73 | middleware (store) { 74 | return dispatch => { 75 | this.listeners.push(dispatch) 76 | return action => { 77 | const triggersEffect = this.execAction(action) 78 | if (!triggersEffect) { 79 | // This action was not ment for this middleware. 80 | // Pass on to the next. 81 | dispatch(action) 82 | } 83 | } 84 | } 85 | } 86 | 87 | dispatch (action) { 88 | this.listeners.forEach(listener => listener(action)) 89 | } 90 | 91 | async updateTitle ({ key, title }) { 92 | const dat = this.dats[key] 93 | const filePath = joinPath(dat.path, 'dat.json') 94 | const metadata = { ...dat.dat.metadata, title: title } 95 | 96 | try { 97 | await writeFile(filePath, JSON.stringify(metadata)) 98 | } catch (error) { 99 | return this.dispatch({ type: 'WRITE_METADATA_ERROR', key, error }) 100 | } 101 | } 102 | 103 | async removeDat ({ key }) { 104 | this.removeDatInternally(key) 105 | this.storeOnDisk() 106 | } 107 | 108 | async tryAddDat (action) { 109 | let { key, path } = action 110 | if (key) { 111 | key = encode(key) 112 | if (this.dats[key]) { 113 | this.dispatch({ type: 'ADD_DAT_ERROR:EXISTED' }) 114 | throw this.dispatch({ 115 | type: 'ADD_DAT_ERROR', 116 | key, 117 | error: new Error('Dat with same key already added.') 118 | }) 119 | } 120 | } 121 | if (!path) path = joinPath(this.downloadsDir, key) 122 | 123 | for (let key in this.dats) { 124 | const dat = this.dats[key] 125 | if (dat.path === path) { 126 | this.dispatch({ type: 'ADD_DAT_ERROR:EXISTED' }) 127 | throw this.dispatch({ 128 | type: 'ADD_DAT_ERROR', 129 | key, 130 | error: new Error('Dat with same path already added.') 131 | }) 132 | } 133 | } 134 | 135 | await this.internalAddDat(action) 136 | } 137 | 138 | async internalAddDat ({ key, path, paused, ...opts }) { 139 | if (key) { 140 | this.dispatch({ type: 'ADD_DAT', key, path, paused }) 141 | } 142 | opts = { 143 | watch: true, 144 | resume: true, 145 | ignoreHidden: true, 146 | compareFileContent: true, 147 | ...opts 148 | } 149 | 150 | Dat(path, { key }, (error, dat) => { 151 | if (error) return this.dispatch({ type: 'ADD_DAT_ERROR', key, error }) 152 | if (!key) { 153 | key = encode(dat.key) 154 | this.dispatch({ type: 'ADD_DAT', key, path, paused }) 155 | } 156 | 157 | dat.trackStats() 158 | if (dat.writable) dat.importFiles(opts) 159 | 160 | this.dispatch({ 161 | type: 'DAT_METADATA', 162 | key, 163 | metadata: { 164 | title: basename(path), 165 | author: 'Anonymous' 166 | } 167 | }) 168 | 169 | this.dispatch({ type: 'ADD_DAT_SUCCESS', key }) 170 | this.dispatch({ type: 'DAT_WRITABLE', key, writable: dat.writable }) 171 | 172 | dat.archive.readFile('dat.json', (err, blob) => { 173 | if (err) return 174 | 175 | let metadata = {} 176 | try { 177 | metadata = JSON.parse(blob) 178 | } catch (_) {} 179 | 180 | this.dispatch({ type: 'DAT_METADATA', key, metadata }) 181 | }) 182 | 183 | dat.stats.on('update', stats => { 184 | if (!stats) stats = dat.stats.get() 185 | this.updateProgress(dat, key, stats) 186 | this.dispatch({ type: 'DAT_STATS', key, stats: { ...stats } }) 187 | }) 188 | 189 | this.updateState(dat) 190 | this.updateProgress(dat, key) 191 | 192 | if (!paused) { 193 | this.joinNetwork(dat) 194 | this.updateConnections(dat) 195 | } 196 | 197 | let prevNetworkStats 198 | dat.updateInterval = setInterval(() => { 199 | const stats = JSON.stringify(dat.stats.network) 200 | if (stats === prevNetworkStats) return 201 | prevNetworkStats = stats 202 | this.dispatch({ 203 | type: 'DAT_NETWORK_STATS', 204 | key, 205 | stats: { 206 | up: dat.stats.network.uploadSpeed, 207 | down: dat.stats.network.downloadSpeed 208 | } 209 | }) 210 | }, 1000) 211 | 212 | this.appendDatInternally(key, dat, path, opts) 213 | this.storeOnDisk() 214 | }) 215 | } 216 | 217 | updateProgress (dat, key, stats) { 218 | if (!stats) stats = dat.stats.get() 219 | const prevProgress = dat.progress 220 | const progress = !dat.stats 221 | ? 0 222 | : dat.writable ? 1 : Math.min(1, stats.downloaded / stats.length) 223 | dat.progress = progress 224 | 225 | this.dispatch({ type: 'DAT_PROGRESS', key, progress }) 226 | this.updateState(dat) 227 | 228 | const unfinishedBefore = prevProgress < 1 && prevProgress > 0 229 | if (dat.progress === 1 && unfinishedBefore) { 230 | const notification = new Notification('Download finished', { 231 | body: key 232 | }) 233 | notification.onclick = () => 234 | shell.openExternal(`file://${dat.path}`, () => {}) 235 | } 236 | 237 | const incomplete = [] 238 | for (const d of Object.values(this.dats)) { 239 | if (d.network && d.progress < 1) incomplete.push(d) 240 | } 241 | let totalProgress = incomplete.length 242 | ? incomplete.reduce((acc, dat) => { 243 | return acc + dat.progress 244 | }, 0) / incomplete.length 245 | : 1 246 | if (totalProgress === 1) totalProgress = -1 // deactivate 247 | if (ipcRenderer) ipcRenderer.send('progress', totalProgress) 248 | } 249 | 250 | async togglePause ({ key, paused }) { 251 | const { dat } = this.dats[key] 252 | if (paused) { 253 | this.joinNetwork(dat) 254 | } else { 255 | dat.leaveNetwork() 256 | } 257 | this.storeOnDisk() 258 | } 259 | 260 | async downloadSparseDat ({ key }) { 261 | key = encode(key) 262 | if (this.dats[key]) { 263 | this.dispatch({ type: 'ADD_DAT_ERROR:EXISTED' }) 264 | return 265 | } 266 | const path = joinPath(this.downloadsDir, key) 267 | 268 | this.dispatch({ type: 'ADD_DAT', key, path }) 269 | 270 | Dat(path, { key, sparse: true }, (error, dat) => { 271 | if (error) return this.dispatch({ type: 'ADD_DAT_ERROR', key, error }) 272 | 273 | dat.trackStats() 274 | 275 | this.dispatch({ 276 | type: 'DAT_METADATA', 277 | key, 278 | metadata: { 279 | title: basename(path), 280 | author: 'Anonymous' 281 | } 282 | }) 283 | 284 | this.dispatch({ type: 'ADD_DAT_SUCCESS', key }) 285 | 286 | dat.archive.readFile('/dat.json', (err, blob) => { 287 | if (err) return 288 | 289 | let metadata = {} 290 | try { 291 | metadata = JSON.parse(blob) 292 | } catch (_) {} 293 | 294 | this.dispatch({ type: 'DAT_METADATA', key, metadata }) 295 | }) 296 | 297 | dat.stats.on('update', stats => { 298 | if (!stats) stats = dat.stats.get() 299 | this.dispatch({ type: 'DAT_STATS', key, stats: { ...stats } }) 300 | }) 301 | 302 | this.updateState(dat) 303 | this.joinNetwork(dat) 304 | this.updateConnections(dat) 305 | 306 | this.appendDatInternally(key, dat, path) 307 | }) 308 | } 309 | 310 | async cancelDownloadDat ({ key }) { 311 | this.removeDatInternally(key) 312 | } 313 | 314 | appendDatInternally (key, dat, path, opts = {}) { 315 | this.dats[key] = { dat, path, opts } 316 | dat.stats.once('update', () => { 317 | this.walk(dat) 318 | }) 319 | } 320 | 321 | removeDatInternally (key) { 322 | this.dispatch({ type: 'REMOVE_DAT', key }) 323 | 324 | const { dat } = this.dats[key] || {} 325 | if (!dat) return // maybe was deleted 326 | delete this.dats[key] 327 | if (dat.mirrorProgress) { 328 | dat.mirrorProgress.destroy() 329 | } 330 | 331 | for (const con of dat.network.connections) { 332 | con.removeAllListeners() 333 | } 334 | dat.stats.removeAllListeners() 335 | clearInterval(dat.updateInterval) 336 | 337 | dat.close() 338 | } 339 | 340 | walk (dat) { 341 | const key = encode(dat.key) 342 | if (!this.dats[key]) return // maybe it was deleted? 343 | if (!dat.files) dat.files = [] 344 | var fs = { name: '/', fs: dat.archive } 345 | var progress = mirror(fs, '/', { dryRun: true }) 346 | progress.on('put', file => { 347 | file.name = file.name.slice(1) 348 | if (file.name === '') return 349 | dat.files.push({ 350 | path: file.name, 351 | size: file.stat.size, 352 | isFile: file.stat.isFile() 353 | }) 354 | dat.files.sort(function (a, b) { 355 | return a.path.localeCompare(b.path) 356 | }) 357 | 358 | const { files } = dat 359 | this.dispatch({ type: 'DAT_FILES', key, files }) 360 | }) 361 | dat.mirrorProgress = progress 362 | } 363 | 364 | updateState (dat) { 365 | const key = encode(dat.key) 366 | const state = !dat.network 367 | ? 'paused' 368 | : dat.writable || dat.progress === 1 369 | ? 'complete' 370 | : dat.network.connected ? 'loading' : 'stale' 371 | this.dispatch({ type: 'DAT_STATE', key, state }) 372 | } 373 | 374 | updateConnections (dat) { 375 | if (!dat.network) return 376 | const key = encode(dat.key) 377 | this.dispatch({ type: 'DAT_PEERS', key, peers: dat.network.connected }) 378 | } 379 | 380 | joinNetwork (dat) { 381 | dat.joinNetwork() 382 | dat.network.on('connection', con => { 383 | this.updateConnections(dat) 384 | this.updateState(dat) 385 | con.on('close', () => { 386 | this.updateConnections(dat) 387 | this.updateState(dat) 388 | }) 389 | }) 390 | } 391 | 392 | async loadFromDisk () { 393 | try { 394 | await mkdirp(this.downloadsDir) 395 | await mkdirp(this.dataDir) 396 | } catch (_) {} 397 | 398 | const [datOpts, paused] = await Promise.all([ 399 | readJSON(joinPath(this.dataDir, 'dats.json')), 400 | readJSON(joinPath(this.dataDir, 'paused.json')) 401 | ]) 402 | 403 | for (const key of Object.keys(datOpts)) { 404 | const opts = JSON.parse(datOpts[key]) 405 | this.internalAddDat({ 406 | key, 407 | path: opts.dir, 408 | paused: paused[key], 409 | ...opts 410 | }) 411 | } 412 | } 413 | 414 | async storeOnDisk () { 415 | try { 416 | await mkdirp(this.dataDir) 417 | } catch (_) {} 418 | 419 | const datsState = Object.keys(this.dats).reduce( 420 | (acc, key) => ({ 421 | ...acc, 422 | [key]: JSON.stringify({ 423 | dir: this.dats[key].path, 424 | opts: this.dats[key].opts 425 | }) 426 | }), 427 | {} 428 | ) 429 | const pausedState = Object.keys(this.dats).reduce( 430 | (acc, key) => ({ 431 | ...acc, 432 | [key]: !this.dats[key].dat.network 433 | }), 434 | {} 435 | ) 436 | 437 | await writeFile( 438 | joinPath(this.dataDir, 'dats.json'), 439 | JSON.stringify(datsState) 440 | ) 441 | await writeFile( 442 | joinPath(this.dataDir, 'paused.json'), 443 | JSON.stringify(pausedState) 444 | ) 445 | } 446 | } 447 | --------------------------------------------------------------------------------
{file.path}{size}
53 | LinkStatusSizePeers 58 |
160 |
{ 163 | event.stopPropagation() 164 | onTogglePause(dat) 165 | }} 166 | > 167 | 168 |
169 |
171 |
172 | updateTitle(key, title)} 176 | /> 177 |

178 | 179 | {dat.metadata.author || 'Anonymous'} •{' '} 180 | 181 | 182 | {dat.writable ? 'Read & Write' : 'Read-only'} 183 | 184 |

185 |
186 |
188 | 189 | 191 | {bytes(dat.stats.byteLength || 0)} 192 | 198 | 199 | 200 | shareDat(`dat://${dat.key}`)} /> 201 | onDeleteDat(dat.key)} /> 202 | 203 |