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 |
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 |
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 |
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(Unable to find React on the page.
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/dev/react-dev-tools/popups/deadcode.html:
--------------------------------------------------------------------------------
1 |
2 |
17 |
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 |
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 | "],
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 |
53 | Link
54 | Status
55 | Size
56 | Peers
57 |
58 |
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 |
68 |
75 |
76 |
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 | [](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 | [](https://travis-ci.org/dat-land/dat-desktop)
12 | [](https://standardjs.com)
13 |
14 | 
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 | development 780 780
--------------------------------------------------------------------------------
/dev/react-dev-tools/icons/development.svg:
--------------------------------------------------------------------------------
1 | development 780 780
--------------------------------------------------------------------------------
/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 |
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 && {label} }
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 |
160 | {
163 | event.stopPropagation()
164 | onTogglePause(dat)
165 | }}
166 | >
167 |
168 |
169 |
170 |
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 |
187 |
188 |
189 |
190 |
191 | {bytes(dat.stats.byteLength || 0)}
192 |
193 |
194 |
195 | {dat.peers}
196 |
197 |
198 |
199 |
200 | shareDat(`dat://${dat.key}`)} />
201 | onDeleteDat(dat.key)} />
202 |
203 |
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 |
126 |
127 |
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 | onCopy(link)}
159 | >
160 |
161 |
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 |
--------------------------------------------------------------------------------