├── .babelrc
├── .eslintignore
├── .eslintrc.json
├── .gitignore
├── .travis.yml
├── ATTRIBUTION.md
├── LICENSE.md
├── README.md
├── build_assets
├── background.tiff
├── icons
│ ├── partyshare.icns_128x128.png
│ ├── partyshare.icns_24x24.png
│ ├── partyshare.icns_256x256.png
│ ├── partyshare.icns_32x32.png
│ ├── partyshare.icns_48x48.png
│ └── partyshare.icns_64x64.png
└── partyshare.icns
├── package.json
├── src
├── electron
│ ├── classes
│ │ └── IPFSSync.js
│ ├── constants.js
│ ├── functions.js
│ └── main.js
├── index.html
├── shared
│ └── constants.js
├── static
│ ├── images
│ │ ├── icon-menubar-dark.png
│ │ ├── icon-menubar-dark@2x.png
│ │ ├── icon-menubar-light.png
│ │ └── icon-menubar-light@2x.png
│ └── svg
│ │ ├── icon_cog.svg
│ │ ├── icon_cog_filled.svg
│ │ ├── icon_file.svg
│ │ ├── icon_file_filled.svg
│ │ ├── icon_folder.svg
│ │ └── icon_folder_filled.svg
└── ui
│ ├── components
│ ├── Button
│ │ ├── Button.css
│ │ └── index.jsx
│ ├── Center
│ │ ├── Center.css
│ │ └── index.jsx
│ ├── FileList
│ │ ├── FileList.css
│ │ └── index.jsx
│ ├── FileListItem
│ │ ├── FileListItem.css
│ │ └── index.jsx
│ ├── Footer
│ │ ├── Footer.css
│ │ └── index.jsx
│ ├── Header
│ │ ├── Header.css
│ │ └── index.jsx
│ ├── Notification
│ │ ├── Notification.css
│ │ └── index.jsx
│ └── Title
│ │ ├── Title.css
│ │ └── index.jsx
│ ├── functions.js
│ ├── icons
│ ├── IconCog
│ │ ├── IconCog.css
│ │ └── index.jsx
│ ├── IconFile
│ │ ├── IconFile.css
│ │ └── index.jsx
│ └── IconFolder
│ │ ├── IconFolder.css
│ │ └── index.jsx
│ ├── index.css
│ └── index.js
├── webpack.config.js
└── yarn.lock
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [["env", {
3 | "targets": {
4 | "electron": "1.6.2"
5 | },
6 | "modules": false,
7 | "useBuiltIns": true
8 | }], "react"],
9 | "plugins": [
10 | "transform-object-rest-spread",
11 | ["transform-react-jsx", { "pragma": "h" }]
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | src/static
2 | node_modules
3 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "node": true
5 | },
6 | "extends": ["@vimeo/eslint-config-player/es6", "standard-preact"],
7 | "parser": "babel-eslint",
8 | "parserOptions": {
9 | "ecmaFeatures": {
10 | "jsx": true
11 | }
12 | },
13 | "rules": {
14 | "comma-dangle": [
15 | "error",
16 | "always-multiline"
17 | ],
18 | "react/jsx-indent": ["warn", 4],
19 | "jsx-quotes": ["error", "prefer-double"],
20 | "multiline-ternary": [0],
21 | "react/jsx-indent-props": ["true", 2]
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | **/*bundle.js
3 | **/*main.css
4 | .vscode
5 | dist
6 | **/*.DS_Store
7 | *.log
8 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js: '8'
3 | matrix:
4 | include:
5 | - os: osx
6 | osx_image: xcode9.0
7 | env:
8 | - ELECTRON_CACHE=$HOME/.cache/electron
9 | - ELECTRON_BUILDER_CACHE=$HOME/.cache/electron-builder
10 | - os: linux
11 | sudo: required
12 | addons:
13 | apt:
14 | sources:
15 | - sourceline: deb https://dl.yarnpkg.com/debian/ stable main
16 | key_url: https://dl.yarnpkg.com/debian/pubkey.gpg
17 | packages:
18 | - yarn
19 | install: yarn
20 | cache: apt
21 | env:
22 | - ELECTRON_CACHE=$HOME/.cache/electron
23 | - ELECTRON_BUILDER_CACHE=$HOME/.cache/electron-builder
24 | cache: yarn
25 | script:
26 | - yarn lint
27 | before_deploy:
28 | - yarn build_ui
29 | - yarn build_electron
30 | deploy:
31 | provider: releases
32 | api_key:
33 | secure: GnPsJYCTnMeP0SmM3sOjakTNoykQEmgRKwVSJjR11ggmRWCBujqZ3mgPO1PlZZ7zexA1UT47cxEVgP6NBBCAI+ef572RAPbedhKJ6MemNV5FHdelh517uFEwMh6o5PdQ5tlIGJqirFHSBFXw9D+igbPrLKLThThfnov3kj092sdD8OyzaT14NWX1bY2CAJ4O+9AJCo91mw5vXtHrrs0rZQzem67c3VSXPBf4ZJvK1VtyGO1qAYnRaRxzDl1YFwBzV9D1pIRYNWorjOf39ExnsJs8n2PIhrwGp7jkwfXgWINZCSxmGdgEDEyEn4KbOpnmVHXId5hXdWIkJr26A94hqwRbFliyWETLr4RbsnE59iN7S3lrXbxcvSJsgn0w2QPn5iqyzTrt4yN34yIp73RBbyc0WXo/kg+AZStF5s0FzkOaekZAjqzpNxai9SnTAu3nRz/+bAFfZgTpD0gseJNVHobqfIWRU0zFmvWuc1vG9/kX57WE/Mkn5dsLa18i08WHuCJ4R2TjRNpGnfu17CSv+qELg8NuHmh3Yw89KO8NFAcvMwSpWeeta1GGjwmqCsziQgTRoyNNngDjEhpCEZfWjz9282WgAjAwithVUMzLF94tDgxmpa2zsvUpdcNnmLRrJ6F05krKNgmiLKHP6JqiMNLfQoAU4PSC2DcLXrPKTbs=
34 | skip_cleanup: true
35 | file_glob: true
36 | file:
37 | - dist/github/latest-{mac,linux}.json
38 | - dist/latest-{mac,linux}.yml
39 | - dist/partyshare.busterlabs.xyz-*-mac.{dmg,zip}
40 | - dist/partyshare.busterlabs.xyz-*-linux.{deb,tar.gz,AppImage}
41 | on:
42 | repo: BusterLabs/Partyshare
43 | branch: master
44 | tags: true
45 |
--------------------------------------------------------------------------------
/ATTRIBUTION.md:
--------------------------------------------------------------------------------
1 | # Attribution
2 |
3 | ### Creative Commons 3.0
4 | - [icon_cog.svg](src/static/svg/icon_cog.svg) created by [arjuazka](https://thenounproject.com/arjuazka/)
5 | - [icon_cog_filled.svg](src/static/svg/icon_cog_filled.svg) created by [arjuazka](https://thenounproject.com/arjuazka/)
6 | - [icon_file.svg](src/static/svg/icon_file.svg) created by [arjuazka](https://thenounproject.com/arjuazka/)
7 | - [icon_file_filled.svg](src/static/svg/icon_file_filled.svg) created by [arjuazka](https://thenounproject.com/arjuazka/)
8 | - [icon_folder.svg](src/static/svg/icon_folder.svg) created by [arjuazka](https://thenounproject.com/arjuazka/)
9 | - [icon_folder_filled.svg](src/static/svg/icon_folder_filled.svg) created by [arjuazka](https://thenounproject.com/arjuazka/)
10 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Buster Labs
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Partyshare
2 |
3 | A free, open source file sharing application, built on the peer-to-peer hypermedia protocol [IPFS](https://ipfs.io/).
4 |
5 |
6 |
7 | It's a simple way to share files via IPFS.
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | ## Download
16 |
17 | You can download the latest version at [busterlabs.github.io/Partyshare](https://busterlabs.github.io/Partyshare). Please, [report any issues](https://github.com/BusterLabs/Partyshare/issues/new?title=&body=%23%23%23%23%20Steps%20to%20Reproduce%0D%0A-%0D%0A%0D%0A%23%23%23%23%20Expected%20Result%0D%0A-%20%0D%0A%0D%0A%23%23%23%23%20Actual%20Result%0D%0A-&labels%5B%5D=bug) you come across.
18 |
19 | ## Contribute
20 |
21 | ### Development
22 | 1. Take a look at the [open issues](https://github.com/BusterLabs/Partyshare/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22)
23 | 2. Assign any to yourself that you'd like to work on
24 | 3. Start coding:
25 |
26 | ```
27 | $ curl -o- -L https://yarnpkg.com/install.sh | bash
28 | $ yarn install
29 | $ yarn start
30 | ```
31 |
32 | ### Releases
33 |
34 | [](https://travis-ci.org/BusterLabs/Partyshare)
35 |
36 | Releases are built by [Travis CI](https://travis-ci.org/BusterLabs/Partyshare), and automatically released [on Github](https://github.com/BusterLabs/Partyshare/releases) for every tagged commit pushed to master. To release a new version:
37 |
38 | ```sh
39 | $ npm version
40 | $ git push origin master --tags
41 | ```
42 |
--------------------------------------------------------------------------------
/build_assets/background.tiff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BusterLabs/Partyshare/931726e9dc718aa0d8db915994296d1458af623e/build_assets/background.tiff
--------------------------------------------------------------------------------
/build_assets/icons/partyshare.icns_128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BusterLabs/Partyshare/931726e9dc718aa0d8db915994296d1458af623e/build_assets/icons/partyshare.icns_128x128.png
--------------------------------------------------------------------------------
/build_assets/icons/partyshare.icns_24x24.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BusterLabs/Partyshare/931726e9dc718aa0d8db915994296d1458af623e/build_assets/icons/partyshare.icns_24x24.png
--------------------------------------------------------------------------------
/build_assets/icons/partyshare.icns_256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BusterLabs/Partyshare/931726e9dc718aa0d8db915994296d1458af623e/build_assets/icons/partyshare.icns_256x256.png
--------------------------------------------------------------------------------
/build_assets/icons/partyshare.icns_32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BusterLabs/Partyshare/931726e9dc718aa0d8db915994296d1458af623e/build_assets/icons/partyshare.icns_32x32.png
--------------------------------------------------------------------------------
/build_assets/icons/partyshare.icns_48x48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BusterLabs/Partyshare/931726e9dc718aa0d8db915994296d1458af623e/build_assets/icons/partyshare.icns_48x48.png
--------------------------------------------------------------------------------
/build_assets/icons/partyshare.icns_64x64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BusterLabs/Partyshare/931726e9dc718aa0d8db915994296d1458af623e/build_assets/icons/partyshare.icns_64x64.png
--------------------------------------------------------------------------------
/build_assets/partyshare.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BusterLabs/Partyshare/931726e9dc718aa0d8db915994296d1458af623e/build_assets/partyshare.icns
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "partyshare.busterlabs.xyz",
3 | "version": "1.3.1",
4 | "description": "A file sharing app using IPFS (InterPlanetary File System)",
5 | "main": "src/electron/main.js",
6 | "scripts": {
7 | "build_electron": "build --publish=never",
8 | "build_ui": "webpack",
9 | "lint": "eslint --ext .js --ext .jsx --format=node_modules/eslint-formatter-pretty src/",
10 | "run_electron": "ENV=dev electron src/electron/main.js",
11 | "start": "yarn watch & yarn run_electron",
12 | "watch": "webpack --progress --colors --watch"
13 | },
14 | "repository": "https://github.com/busterlabs/partyshare",
15 | "keywords": [],
16 | "author": "Ben Stahl ",
17 | "license": "CC0-1.0",
18 | "dependencies": {
19 | "babel-plugin-transform-object-rest-spread": "^6.26.0",
20 | "core-js": "^2.5.1",
21 | "css-loader": "^0.28.7",
22 | "electron-log": "^2.2.11",
23 | "electron-updater": "^2.16.1",
24 | "eslint-plugin-react": "^7.5.1",
25 | "extract-text-webpack-plugin": "^3.0.2",
26 | "file-size": "^1.0.0",
27 | "fs-extra": "^4.0.2",
28 | "ipfs-api": "^17.1.2",
29 | "ipfsd-ctl": "^0.25.1",
30 | "klaw": "^2.1.1",
31 | "menubar": "^5.2.3",
32 | "mime": "^2.0.3",
33 | "preact": "^8.2.6",
34 | "preact-compat": "^3.17.0",
35 | "style-loader": "^0.19.0"
36 | },
37 | "devDependencies": {
38 | "@vimeo/eslint-config-player": "^5.0.0",
39 | "babel-core": "^6.26.0",
40 | "babel-eslint": "^8.0.2",
41 | "babel-loader": "^7.1.2",
42 | "babel-preset-env": "^1.6.1",
43 | "babel-preset-react": "^6.23.0",
44 | "electron": "^1.7.9",
45 | "electron-builder": "^19.45.5",
46 | "eslint": "^4.11.0",
47 | "eslint-config-standard-preact": "^1.1.6",
48 | "eslint-formatter-pretty": "^1.3.0",
49 | "webpack": "^3.8.1"
50 | },
51 | "build": {
52 | "appId": "partyshare.busterlabs.xyz",
53 | "artifactName": "${name}-${version}-${os}.${ext}",
54 | "asarUnpack": "node_modules/go-ipfs-dep",
55 | "productName": "Partyshare",
56 | "icon": "build_assets/partyshare.icns",
57 | "forceCodeSigning": true,
58 | "directories": {
59 | "buildResources": "build_assets"
60 | },
61 | "dmg": {
62 | "background": "build_assets/background.tiff",
63 | "title": "Install ${productName}"
64 | },
65 | "mac": {
66 | "target": [
67 | "dmg",
68 | "zip"
69 | ]
70 | },
71 | "linux": {
72 | "target": [
73 | "AppImage",
74 | "deb",
75 | "tar.gz"
76 | ]
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/electron/classes/IPFSSync.js:
--------------------------------------------------------------------------------
1 | const EventEmitter = require('events');
2 | const fs = require('fs');
3 | const fse = require('fs-extra');
4 | const logger = require('electron-log');
5 | const ipfsCtl = require('ipfsd-ctl');
6 | const ipfsAPI = require('ipfs-api');
7 | const {
8 | getFiles,
9 | statWithPromise,
10 | } = require('../functions.js');
11 | const {
12 | basename,
13 | extname,
14 | join,
15 | } = require('path');
16 | const {
17 | IPFS_FOLDER,
18 | IPFS_REPO,
19 | } = require('../constants.js');
20 | const {
21 | IPC_EVENT_FILES_ADDED,
22 | IPC_EVENT_STATE_CHANGE,
23 | } = require('../../shared/constants');
24 |
25 | const DEFAULTS = {
26 | folderPath: IPFS_FOLDER,
27 | autoStart: true,
28 | };
29 |
30 | const MAX_RECONNECTS = 5;
31 |
32 | /**
33 | * Keep a folder in sync with your IPFS repo. Any files added
34 | * to the folder will be automatically added to IPFS.
35 | * @extends EventEmitter
36 | */
37 | class IPFSSync extends EventEmitter {
38 | constructor(options) {
39 | super();
40 |
41 | const {
42 | folderPath,
43 | autoStart,
44 | } = Object.assign(DEFAULTS, options);
45 |
46 | this._bindMethods();
47 | this._state = {
48 | files: [],
49 | folder: {
50 | path: folderPath,
51 | basename: basename(folderPath),
52 | },
53 | ipfs: null,
54 | connectRetries: 0,
55 | connected: false,
56 | synced: false,
57 | };
58 |
59 | if (autoStart) {
60 | this.start();
61 | }
62 | }
63 |
64 | // Privates _______________________________________________________________
65 |
66 | /**
67 | * Bind any methods used in this class.
68 | * @return {IPFSSync}
69 | */
70 | _bindMethods() {
71 | logger.info('[IPFSSync] _bindMethods');
72 | this._addDirectory = this._addDirectory.bind(this);
73 | this._addFile = this._addFile.bind(this);
74 | this._connectToExistingDaemon = this._connectToExistingDaemon.bind(this);
75 | this._connectToIPFS = this._connectToIPFS.bind(this);
76 | this._getConfig = this._getConfig.bind(this);
77 | this._getNode = this._getNode.bind(this);
78 | this._initNode = this._initNode.bind(this);
79 | this._readAndSyncFiles = this._readAndSyncFiles.bind(this);
80 | this._retryConnection = this._retryConnection.bind(this);
81 | this._syncFiles = this._syncFiles.bind(this);
82 | this.setState = this.setState.bind(this);
83 | this.start = this.start.bind(this);
84 | this.quit = this.quit.bind(this);
85 | this.watch = this.watch.bind(this);
86 | return this;
87 | }
88 |
89 | /**
90 | * Wrap a file in a fake folder, then add it to IPFS to allow for file names
91 | * in the URL.
92 | *
93 | * @param {String} path
94 | * @return {Promise}
95 | */
96 | _addFile(path) {
97 | logger.info('[IPFSSync] _addFile');
98 | const fileName = basename(path);
99 | const fakeDirectory = basename(basename(fileName), extname(fileName)).trim();
100 | const file = {
101 | path: join(fakeDirectory, fileName),
102 | content: fs.createReadStream(path),
103 | };
104 |
105 | return this.state.ipfs.add([file], { wrap: true })
106 | .then((result) => {
107 | const [fileObject, dirObject] = result;
108 | fileObject.urlPath = encodeURI(join(dirObject.hash, fileName));
109 | fileObject.name = fileName;
110 | fileObject.path = path;
111 | return Promise.resolve([fileObject]);
112 | });
113 | }
114 |
115 | /**
116 | * Add an entire directory to IPFS, using the directory as the root
117 | * hash.
118 | *
119 | * @param {String} path
120 | * @return {Promise}
121 | */
122 | _addDirectory(path) {
123 | logger.info('[IPFSSync] _addDirectory');
124 | return this.state.ipfs.util.addFromFs(path, { recursive: true })
125 | .then((objects) => {
126 | return new Promise((resolve) => {
127 | const dirName = basename(path);
128 | const folder = objects.find((item) => dirName.indexOf(item.path) > -1);
129 | const files = objects.filter((item) => item !== folder).map((file) => {
130 | file.name = basename(file.path);
131 | file.urlPath = encodeURI(file.path.replace(folder.path, folder.hash));
132 | file.path = join(this.state.folder.path, file.path);
133 | return file;
134 | });
135 | return resolve(files);
136 | });
137 | });
138 | }
139 |
140 | /**
141 | * Add the whole folder to IPFS, resolving with the new folder
142 | * hash and a list of files.
143 | * @param {Array} fileNames
144 | * @return {Promise}
145 | */
146 | _syncFiles(fileNames) {
147 | logger.info('[IPFSSync] _syncFiles');
148 |
149 | if (fileNames.length < 1) {
150 | // No files to sync
151 | return this.setState({ files: [], synced: true });
152 | }
153 |
154 | const promises = fileNames.map((fileName) => {
155 | return new Promise((resolve, reject) => {
156 | const fullPath = join(this.state.folder.path, fileName);
157 |
158 | fs.stat(fullPath, (err, stats) => {
159 | if (err) {
160 | return reject(err);
161 | }
162 |
163 | if (stats.isDirectory()) {
164 | return this._addDirectory(fullPath)
165 | .then(resolve)
166 | .catch(reject);
167 | }
168 |
169 | return this._addFile(fullPath)
170 | .then(resolve)
171 | .catch(reject);
172 | });
173 | });
174 | });
175 |
176 | return Promise.all(promises)
177 | .then((groups) => [].concat(...groups))
178 | .then((files) => Promise.all(files.map((file) => statWithPromise(file))))
179 | .then((files) => this.setState({ files, synced: true }));
180 | }
181 |
182 | /**
183 | * Retry connecting to the daemon.
184 | */
185 | _retryConnection() {
186 | logger.info('[IPFSSync] _retryConnection');
187 |
188 | if (this.state.connectRetries > MAX_RECONNECTS) {
189 | logger.error('[IPFSSync] _retryConnection: Exceeded max connection retries', this.state.connectRetries);
190 | return;
191 | }
192 |
193 | this.setState({
194 | connectRetries: this.state.connectRetries + 1,
195 | synced: false,
196 | });
197 |
198 | this.start()
199 | .then(() => this.setState({ connectRetries: 0 }))
200 | .catch((e) => logger.error('[IPFSSync] _retryConnection: ', e));
201 | }
202 |
203 | /**
204 | * Get the contents of a folder, and add them to the IPFS repo.
205 | * @return {Promise}
206 | */
207 | _readAndSyncFiles() {
208 | logger.info('[IPFSSync] _readAndSyncFiles');
209 | this.setState({ synced: false });
210 |
211 | return getFiles(this.state.folder.path)
212 | .then(this._syncFiles)
213 | .catch((e) => {
214 | logger.error('[IPFSSync] _readAndSyncFiles: ', e);
215 | this._retryConnection();
216 | });
217 | }
218 |
219 | /**
220 | * Get a local node.
221 | * @return {Promise}
222 | */
223 | _getNode() {
224 | logger.info('[IPFSSync] _getNode');
225 | return new Promise((resolve, reject) => {
226 | ipfsCtl.local(IPFS_REPO, {}, (err, node) => {
227 | if (err) {
228 | logger.error('[IPFSSync] _getNode', err);
229 | reject(err);
230 | return;
231 | }
232 |
233 | resolve(node);
234 | });
235 | });
236 | }
237 |
238 | /**
239 | * Initialize the ipfs node if it isn't already.
240 | * @param {Node} node
241 | * @return {Promise}
242 | */
243 | _initNode(node) {
244 | logger.info('[IPFSSync] _initNode');
245 | return new Promise((resolve, reject) => {
246 | if (node.initialized) {
247 | resolve(node);
248 | return;
249 | }
250 |
251 | node.init({ directory: IPFS_REPO }, (err, res) => {
252 | if (err) {
253 | logger.error('[IPFSSync] _initNode', err);
254 | return reject(err);
255 | }
256 |
257 | return resolve(node);
258 | });
259 | });
260 | }
261 |
262 | /**
263 | * Attempt to read an existing ipfs config
264 | * @param {Node} node
265 | * @return {Promise}
266 | */
267 | _getConfig(node) {
268 | logger.info('[IPFSSync] _getConfig');
269 | return new Promise((resolve, reject) => {
270 | node.getConfig('show', (err, configString) => {
271 | if (err) {
272 | logger.error('[IPFSSync] _getConfig', err);
273 | return reject(err);
274 | }
275 |
276 | try {
277 | const config = JSON.parse(configString);
278 | return resolve(config);
279 | }
280 | catch (e) {
281 | return reject(e);
282 | }
283 | });
284 | });
285 | }
286 |
287 | /**
288 | * Attempt to connet to a running daemon.
289 | * @param {Node} node
290 | * @return {Promise}
291 | */
292 | _connectToExistingDaemon(node) {
293 | logger.info('[IPFSSync] _connectToExistingDaemon');
294 | return new Promise((resolve, reject) => {
295 | this._getConfig(node)
296 | .then((config) => {
297 | const api = ipfsAPI(config.Addresses.API);
298 | return resolve(api);
299 | })
300 | .catch(reject);
301 | });
302 | }
303 |
304 | /**
305 | * Attempt to start an ipfs daemon, connecting to a possible running daemon
306 | * if we fail.
307 | * @param {Node} node
308 | * @return {Promise}
309 | */
310 | _connectToIPFS(node) {
311 | logger.info('[IPFSSync] _connectToIPFS');
312 | return new Promise((resolve, reject) => {
313 | node.startDaemon((err, daemon) => {
314 | if (err) {
315 | logger.error('[IPFSSync] _connectToIPFS', err);
316 |
317 | // Connect to an exising daemon if possible
318 | return this._connectToExistingDaemon(node)
319 | .then(resolve)
320 | .catch(reject);
321 | }
322 |
323 | return resolve(daemon);
324 | });
325 | });
326 | }
327 |
328 |
329 | /**
330 | * Attempt to stop the runninn daemon
331 | * @param {Node} node
332 | */
333 | _stopIPFS(node) {
334 | logger.info('[IPFSSync] _stopIPFS');
335 | node.stopDaemon((err, daemon) => {
336 | if (err) {
337 | logger.error('[IPFSSync] stopIPFSDaemon', err);
338 | }
339 | });
340 | }
341 |
342 | // Public API ____________________________________________________________
343 | get state() {
344 | logger.info('[IPFSSync] get state');
345 | return this._state;
346 | }
347 |
348 | /**
349 | * Update the current state of the sync and dispatch an event.
350 | * @param {Object} newState
351 | * @return {Promise}
352 | */
353 | setState(newState) {
354 | logger.info('[IPFSSync] setState', newState);
355 |
356 | if (this._state &&
357 | this._state.files &&
358 | newState.files &&
359 | (newState.files.length - this._state.files.length) > 0) {
360 | this.emit(IPC_EVENT_FILES_ADDED);
361 | }
362 |
363 | this._state = Object.assign({}, this._state, newState);
364 | this.emit(IPC_EVENT_STATE_CHANGE, this._state);
365 | return Promise.resolve(this._state);
366 | }
367 |
368 | /**
369 | * Start the sync
370 | * @return {Promise}
371 | */
372 | start() {
373 | logger.info('[IPFSSync] start');
374 | this.watch();
375 |
376 | return this._getNode()
377 | .then(this._initNode)
378 | .then((node) => {
379 | this.setState({ node });
380 | return node;
381 | })
382 | .then(this._connectToIPFS)
383 | .then((ipfs) => this.setState({ ipfs, connected: true }))
384 | .then(() => this._readAndSyncFiles())
385 | .catch((e) => logger.error('[IPFSSync] startIPFS: ', e));
386 | }
387 |
388 | /**
389 | * Quit IPFS
390 | */
391 | quit() {
392 | logger.info('[IPFSSync] quit');
393 |
394 | if (this._state.connected) {
395 | this._stopIPFS(this._state.node);
396 | }
397 | }
398 |
399 | /**
400 | * Manually trigger the folder watch, called automatically in start().
401 | */
402 | watch() {
403 | logger.info('[IPFSSync] watch');
404 | fse.ensureDir(this.state.folder.path, (err) => {
405 | if (err) {
406 | logger.error('[IPFSSync] watch', err);
407 | return;
408 | }
409 | fs.watch(this.state.folder.path, { recursive: true }, this._readAndSyncFiles);
410 | });
411 | }
412 | }
413 |
414 | module.exports = IPFSSync;
415 |
--------------------------------------------------------------------------------
/src/electron/constants.js:
--------------------------------------------------------------------------------
1 |
2 | const { join, resolve } = require('path');
3 | const { homedir } = require('os');
4 | const { format } = require('url');
5 |
6 | const __DEV__ = process.env.ENV === 'dev';
7 | const GATEWAY_URL = __DEV__ ? 'http://localhost:8080/ipfs' : 'https://gateway.ipfs.io/ipfs';
8 | const LIGHT_MENUBAR_ICON_PATH = resolve(__dirname, '..', 'static', 'images', 'icon-menubar-light@2x.png');
9 | const DARK_MENUBAR_ICON_PATH = resolve(__dirname, '..', 'static', 'images', 'icon-menubar-dark@2x.png');
10 |
11 | const INDEX_PATH = format({
12 | pathname: resolve(__dirname, '..', 'index.html'),
13 | protocol: 'file:',
14 | slashes: true,
15 | });
16 | const IPFS_FOLDER = join(homedir(), 'Partyshare');
17 | const IPFS_REPO = join(homedir(), '.partyshare-repo');
18 |
19 | module.exports = {
20 | __DEV__,
21 | GATEWAY_URL,
22 | LIGHT_MENUBAR_ICON_PATH,
23 | DARK_MENUBAR_ICON_PATH,
24 | INDEX_PATH,
25 | IPFS_FOLDER,
26 | IPFS_REPO,
27 | };
28 |
--------------------------------------------------------------------------------
/src/electron/functions.js:
--------------------------------------------------------------------------------
1 | const { basename } = require('path');
2 | const fse = require('fs-extra');
3 | const fs = require('fs');
4 |
5 | /**
6 | * Filter function for ignoring hidden files.
7 | * @param {String} fileName
8 | * @return {boolean}
9 | */
10 | const filterHiddenFiles = (fileName) => {
11 | const base = basename(fileName);
12 | return !base.startsWith('.');
13 | };
14 |
15 | /**
16 | * Ensure a directory exists, and list the contents.
17 | * @param {String} dirPath
18 | * @return {Promise}
19 | */
20 | const getFiles = (dirPath) => {
21 | return new Promise((resolve, reject) => {
22 | fse.ensureDir(dirPath, (ensureErr) => {
23 | if (ensureErr) {
24 | return reject(ensureErr);
25 | }
26 |
27 | return fs.readdir(dirPath, (readErr, files) => {
28 | if (readErr) {
29 | return reject(readErr);
30 | }
31 |
32 | files = files.filter(filterHiddenFiles);
33 | return resolve(files);
34 | });
35 | });
36 | });
37 | };
38 |
39 | /**
40 | * Stat a file added to IPFS, wrapping the callback style in a promise.
41 | * @param {Object} file
42 | * @return {Promise}
43 | */
44 | const statWithPromise = (file) => {
45 | return new Promise((resolve, reject) => {
46 | fs.stat(file.path, (err, stats) => {
47 | if (err) {
48 | return reject(err);
49 | }
50 |
51 | file.stats = stats;
52 | return resolve(file);
53 | });
54 | });
55 | };
56 |
57 |
58 | module.exports = {
59 | getFiles,
60 | statWithPromise,
61 | };
62 |
63 |
--------------------------------------------------------------------------------
/src/electron/main.js:
--------------------------------------------------------------------------------
1 | const { autoUpdater } = require('electron-updater');
2 | const {
3 | dialog,
4 | ipcMain,
5 | } = require('electron');
6 | const IPFSSync = require('./classes/IPFSSync.js');
7 | const logger = require('electron-log');
8 | const menubar = require('menubar');
9 | const fs = require('fs-extra');
10 | const {
11 | basename,
12 | join,
13 | } = require('path');
14 | const {
15 | __DEV__,
16 | LIGHT_MENUBAR_ICON_PATH,
17 | DARK_MENUBAR_ICON_PATH,
18 | INDEX_PATH,
19 | } = require('./constants');
20 | const {
21 | IPC_EVENT_REQUEST_STATE,
22 | IPC_EVENT_SEND_STATE,
23 | IPC_EVENT_STATE_CHANGE,
24 | IPC_EVENT_FILES_ADDED,
25 | IPC_EVENT_NOTIFICATION,
26 | IPC_EVENT_HIDE_MENU,
27 | IPC_EVENT_QUIT_APP,
28 | } = require('../shared/constants');
29 |
30 | autoUpdater.logger = logger;
31 | autoUpdater.logger.transports.file.level = 'info';
32 |
33 | const ipfsSync = new IPFSSync();
34 |
35 | const mb = menubar({
36 | icon: DARK_MENUBAR_ICON_PATH,
37 | index: INDEX_PATH,
38 | preloadWindow: true,
39 | alwaysOnTop: __DEV__,
40 | width: 350,
41 | height: 400,
42 | });
43 |
44 | mb.on('ready', () => {
45 |
46 | // Menubar flashes, the closes on start without a slight timeout
47 | setTimeout(() => {
48 | mb.showWindow();
49 | }, 1000);
50 |
51 | // Setup highlighted icon
52 | mb.tray.setPressedImage(LIGHT_MENUBAR_ICON_PATH);
53 |
54 | // Copy files dragged to menu bar into partyshare
55 | mb.tray.on('drop-files', (event, files) => {
56 |
57 | if (!ipfsSync.state.connected) {
58 | return;
59 | }
60 |
61 | files.forEach((filePath) => {
62 | const fileName = basename(filePath);
63 | const newPath = join(ipfsSync.state.folder, fileName);
64 | fs.copy(filePath, newPath);
65 | });
66 | });
67 |
68 |
69 | if (__DEV__) {
70 | return;
71 | }
72 |
73 | autoUpdater.checkForUpdates();
74 | });
75 |
76 | mb.on('after-create-window', () => {
77 | mb.window.webContents.send(IPC_EVENT_SEND_STATE, ipfsSync.state);
78 | mb.window.setMovable(false);
79 |
80 | if (__DEV__) {
81 | mb.window.openDevTools();
82 | }
83 |
84 | });
85 |
86 | mb.on('show', () => {
87 | mb.window.webContents.send(IPC_EVENT_SEND_STATE, ipfsSync.state);
88 | });
89 |
90 | ipfsSync.on(IPC_EVENT_STATE_CHANGE, () => {
91 | if (!mb.window) {
92 | logger.error('[ipcMain] No window to send event on');
93 | return;
94 | }
95 |
96 | mb.window.webContents.send(IPC_EVENT_SEND_STATE, ipfsSync.state);
97 | });
98 |
99 | ipfsSync.on(IPC_EVENT_FILES_ADDED, () => {
100 | logger.info('[ipfsSync] files-added');
101 | mb.showWindow();
102 | mb.window.webContents.send(IPC_EVENT_FILES_ADDED);
103 | });
104 |
105 |
106 | // Inter Process (Main <-> Render) Communication
107 | // ___________________________________________________________________________
108 |
109 | ipcMain.on(IPC_EVENT_REQUEST_STATE, (event) => {
110 | event.sender.send(IPC_EVENT_SEND_STATE, ipfsSync.state);
111 | });
112 |
113 | ipcMain.on(IPC_EVENT_HIDE_MENU, () => {
114 | mb.hideWindow();
115 | });
116 |
117 | ipcMain.on(IPC_EVENT_QUIT_APP, () => {
118 | mb.app.quit();
119 | ipfsSync.quit();
120 | });
121 |
122 | ipcMain.on(IPC_EVENT_NOTIFICATION, (event, text) => {
123 | const state = Object.assign(ipfsSync.state, {
124 | notification: text,
125 | });
126 | mb.window.webContents.send(IPC_EVENT_SEND_STATE, state);
127 | setTimeout(() => {
128 | const clearedState = Object.assign(ipfsSync.state, {
129 | notification: false,
130 | });
131 | mb.window.webContents.send(IPC_EVENT_SEND_STATE, clearedState);
132 | }, 3000);
133 | });
134 |
135 | // ---------------------------------------------------------------------------
136 | // Auto updates
137 | //
138 | // For details about these events, see their Wiki:
139 | // https://github.com/electron-userland/electron-builder/wiki/Auto-Update#events
140 | // ___________________________________________________________________________
141 |
142 | autoUpdater.on('update-downloaded', (ev, info) => {
143 | logger.info('[autoUpdate] Notifying user of update');
144 | dialog.showMessageBox({
145 | type: 'info',
146 | title: 'Update available',
147 | message: 'An update for Partyshare is available, ok to restart?',
148 | buttons: ['Restart', 'Ignore'],
149 | }, (buttonIndex) => {
150 | if (buttonIndex === 0) {
151 | logger.info('[autoUpdate] Quitting and installing....');
152 | autoUpdater.quitAndInstall();
153 | }
154 | });
155 | });
156 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Partyshare
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/shared/constants.js:
--------------------------------------------------------------------------------
1 | const IPC_EVENT_REQUEST_STATE = 'IPC_EVENT_REQUEST_STATE';
2 | const IPC_EVENT_SEND_STATE = 'IPC_EVENT_SEND_STATE';
3 | const IPC_EVENT_STATE_CHANGE = 'IPC_EVENT_STATE_CHANGE';
4 | const IPC_EVENT_FILES_ADDED = 'IPC_EVENT_FILES_ADDED';
5 | const IPC_EVENT_NOTIFICATION = 'IPC_EVENT_NOTIFICATION';
6 | const IPC_EVENT_HIDE_MENU = 'IPC_EVENT_HIDE_MENU';
7 | const IPC_EVENT_QUIT_APP = 'IPC_EVENT_QUIT_APP';
8 | const IPC_EVENTS = {
9 | IPC_EVENT_REQUEST_STATE,
10 | IPC_EVENT_SEND_STATE,
11 | IPC_EVENT_STATE_CHANGE,
12 | IPC_EVENT_FILES_ADDED,
13 | IPC_EVENT_NOTIFICATION,
14 | IPC_EVENT_HIDE_MENU,
15 | IPC_EVENT_QUIT_APP,
16 | };
17 |
18 | const URL_ATTRIBUTION = 'https://github.com/BusterLabs/Partyshare/blob/master/ATTRIBUTION.md';
19 | const URL_BUG = 'https://github.com/BusterLabs/Partyshare/issues/new?title=&body=%23%23%23%23%20Steps%20to%20Reproduce%0D%0A-%0D%0A%0D%0A%23%23%23%23%20Expected%20Result%0D%0A-%20%0D%0A%0D%0A%23%23%23%23%20Actual%20Result%0D%0A-&labels%5B%5D=bug';
20 | const URL_CONTRIBUTE = 'https://github.com/BusterLabs/Partyshare#contribute';
21 | const URL_TWEET = 'https://twitter.com/intent/tweet?text=Check%20out%20Partyshare%2C%20a%20simple%20and%20free%20way%20to%20share%20files%20on%20IPFS.&url=https%3A%2F%2Fpartysha.re%2F&via=BusterLabs&hashtags=IPFS';
22 |
23 | module.exports = {
24 | IPC_EVENT_REQUEST_STATE,
25 | IPC_EVENT_SEND_STATE,
26 | IPC_EVENT_STATE_CHANGE,
27 | IPC_EVENT_FILES_ADDED,
28 | IPC_EVENT_NOTIFICATION,
29 | IPC_EVENT_HIDE_MENU,
30 | IPC_EVENT_QUIT_APP,
31 | IPC_EVENTS,
32 | URL_ATTRIBUTION,
33 | URL_BUG,
34 | URL_CONTRIBUTE,
35 | URL_TWEET,
36 | };
37 |
--------------------------------------------------------------------------------
/src/static/images/icon-menubar-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BusterLabs/Partyshare/931726e9dc718aa0d8db915994296d1458af623e/src/static/images/icon-menubar-dark.png
--------------------------------------------------------------------------------
/src/static/images/icon-menubar-dark@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BusterLabs/Partyshare/931726e9dc718aa0d8db915994296d1458af623e/src/static/images/icon-menubar-dark@2x.png
--------------------------------------------------------------------------------
/src/static/images/icon-menubar-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BusterLabs/Partyshare/931726e9dc718aa0d8db915994296d1458af623e/src/static/images/icon-menubar-light.png
--------------------------------------------------------------------------------
/src/static/images/icon-menubar-light@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BusterLabs/Partyshare/931726e9dc718aa0d8db915994296d1458af623e/src/static/images/icon-menubar-light@2x.png
--------------------------------------------------------------------------------
/src/static/svg/icon_cog.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/static/svg/icon_cog_filled.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/static/svg/icon_file.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/static/svg/icon_file_filled.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/static/svg/icon_folder.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/static/svg/icon_folder_filled.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/src/ui/components/Button/Button.css:
--------------------------------------------------------------------------------
1 | .this {
2 | display: inline-block;
3 | padding: 3px 8px;
4 | margin-bottom: 0;
5 | font-size: 0.9rem;
6 | line-height: 1.4;
7 | text-align: center;
8 | white-space: nowrap;
9 | vertical-align: middle;
10 | cursor: default;
11 | background-image: none;
12 | border: 1px solid transparent;
13 | border-radius: var(--radius-size);
14 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.06);
15 | -webkit-app-region: no-drag;
16 | cursor: pointer;
17 | }
18 |
19 | .this:focus {
20 | outline: none;
21 | box-shadow: none;
22 | }
23 |
24 | .default {
25 | color: #333;
26 | border-top-color: #c2c0c2;
27 | border-right-color: #c2c0c2;
28 | border-bottom-color: #a19fa1;
29 | border-left-color: #c2c0c2;
30 | background-color: #fcfcfc;
31 | background-image: linear-gradient(to bottom, #fcfcfc 0%, #f1f1f1 100%);
32 | }
33 |
34 | .default:active {
35 | background-color: #ddd;
36 | background-image: none;
37 | }
38 |
--------------------------------------------------------------------------------
/src/ui/components/Button/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import styles from './Button.css';
3 |
4 | const Button = ({
5 | className,
6 | children,
7 | type = 'default',
8 | ...props
9 | }) => (
10 |
16 | );
17 |
18 | export default Button;
19 |
--------------------------------------------------------------------------------
/src/ui/components/Center/Center.css:
--------------------------------------------------------------------------------
1 | .this {
2 | display: flex;
3 | flex-direction: column;
4 | align-items: center;
5 | justify-content: center;
6 | font-size: 1rem;
7 | color: var(--dark-gray);
8 | height: 100%;
9 | text-align: center;
10 | }
11 |
--------------------------------------------------------------------------------
/src/ui/components/Center/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import styles from './Center.css';
3 |
4 | const Center = ({
5 | children,
6 | ...props
7 | }) => (
8 |
9 | {children}
10 |
11 | );
12 |
13 | export default Center;
14 |
--------------------------------------------------------------------------------
/src/ui/components/FileList/FileList.css:
--------------------------------------------------------------------------------
1 | .this {
2 | list-style: none;
3 | margin: 0;
4 | padding: 0;
5 | overflow: scroll;
6 | }
7 |
--------------------------------------------------------------------------------
/src/ui/components/FileList/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import Button from 'components/Button';
3 | import Center from 'components/Center';
4 | import FileListItem from 'components/FileListItem';
5 | import { GATEWAY_URL } from '../../../electron/constants';
6 | import { shell, ipcRenderer } from 'electron';
7 | import {
8 | IPC_EVENT_HIDE_MENU,
9 | } from '../../../shared/constants';
10 |
11 | import styles from './FileList.css';
12 |
13 | const FileList = ({
14 | files,
15 | synced,
16 | folder,
17 | ...props
18 | }) => {
19 | if (files.length < 1 && !synced) {
20 | return (
21 | Syncing…
22 | );
23 | }
24 |
25 | if (files.length < 1) {
26 | return (
27 |
28 | Drag a file into your Partyshare folder to begin
29 |
37 |
38 | );
39 | }
40 |
41 | files = files.sort((a, b) => new Date(b.stats.ctime) - new Date(a.stats.ctime));
42 |
43 | return (
44 |
45 | {files.map((file) =>
49 | )}
50 |
51 | );
52 | };
53 |
54 | export default FileList;
55 |
--------------------------------------------------------------------------------
/src/ui/components/FileListItem/FileListItem.css:
--------------------------------------------------------------------------------
1 | .this {
2 | display: flex;
3 | flex-direction: row;
4 | align-items: center;
5 | padding: 0 15px;
6 | height: 58px;
7 | border-bottom: 1px solid var(--medium-gray);
8 | transition: background-color 100ms ease-out;
9 | }
10 |
11 | .this:hover {
12 | background-color: var(--lightest-gray);
13 | }
14 |
15 | .icon,
16 | .image,
17 | .copy_hit_area {
18 | display: flex;
19 | flex-grow: 0;
20 | flex-shrink: 0;
21 | }
22 |
23 | .icon {
24 | margin-right: 20px;
25 | width: 40px;
26 | justify-content: center;
27 | align-items: center;
28 | }
29 |
30 | .file {
31 | width: 25px;
32 | height: 30px;
33 | fill: var(--dark-gray);
34 | }
35 |
36 | .image {
37 | max-width: 40px;
38 | max-height: 40px;
39 | border-radius: 2px;
40 | }
41 |
42 | .name {
43 | flex: 1;
44 | padding: 0;
45 | margin: 0;
46 | font-size: 1rem;
47 | color: #4A4A4A;
48 | white-space: nowrap;
49 | overflow: hidden;
50 | text-overflow: ellipsis;
51 | }
52 |
53 | .copy_hit_area {
54 | cursor: pointer;
55 | padding: 0 0 0 10px;
56 | height: 100%;
57 | display: flex;
58 | align-items: center;
59 | }
60 |
61 | .copy_hit_area:hover .copy_button {
62 | color: var(--blue);
63 | border: 1px solid var(--blue);
64 | }
65 |
66 | .copy_button {
67 | padding: 4px 5px;
68 | font-size: 0.95rem;
69 | color: var(--dark-gray);
70 | font-weight: 500;
71 | border-radius: var(--radius-size);
72 | transition: all 100ms ease-out;
73 | border: 1px solid transparent;
74 | }
75 |
76 | .copy_button:active {
77 | background-color: var(--light-blue);
78 | }
79 |
80 |
--------------------------------------------------------------------------------
/src/ui/components/FileListItem/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import { clipboard, ipcRenderer } from 'electron';
3 | import { isImage } from 'functions';
4 | import {
5 | IPC_EVENT_NOTIFICATION,
6 | } from '../../../shared/constants';
7 | import IconFile from 'icons/IconFile';
8 | import styles from './FileListItem.css';
9 |
10 | const FileListItem = ({
11 | name,
12 | path,
13 | url,
14 | ...props
15 | }) => (
16 |
17 |
18 | { isImage(path) ?
19 |
20 | :
21 |
22 | }
23 |
24 |
25 | { name }
26 |
27 | { url &&
28 | {
33 | clipboard.writeText(url);
34 | ipcRenderer.send(IPC_EVENT_NOTIFICATION, 'Link copied to your clipboard');
35 | }}
36 | >
37 |
38 | Copy Link
39 |
40 |
41 | }
42 |
43 | );
44 |
45 | export default FileListItem;
46 |
--------------------------------------------------------------------------------
/src/ui/components/Footer/Footer.css:
--------------------------------------------------------------------------------
1 | .this {
2 | position: absolute;
3 | bottom: 0;
4 | padding: 10px;
5 | text-align: center;
6 | width: 100%;
7 | color: var(--dark-gray);
8 | font-size: 0.9rem;
9 | font-weight: 400;
10 | }
11 |
--------------------------------------------------------------------------------
/src/ui/components/Footer/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import styles from './Footer.css';
3 |
4 | const Footer = ({
5 | children,
6 | ...props
7 | }) => (
8 |
11 | );
12 |
13 | export default Footer;
14 |
--------------------------------------------------------------------------------
/src/ui/components/Header/Header.css:
--------------------------------------------------------------------------------
1 | .this {
2 | display: flex;
3 | flex-direction: row;
4 | height: 29px;
5 | padding: 10px 15px;
6 | box-shadow: inset 0 1px 0 #f5f4f5;
7 | background-color: var(--light-gray);
8 | background-image: linear-gradient(to bottom, var(--light-gray) 0%, var(--medium-gray) 100%);
9 | border-bottom: 1px solid #c2c0c2;
10 | }
11 |
--------------------------------------------------------------------------------
/src/ui/components/Header/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import styles from './Header.css';
3 |
4 | const Header = ({
5 | children,
6 | ...props
7 | }) => (
8 |
11 | );
12 |
13 | export default Header;
14 |
--------------------------------------------------------------------------------
/src/ui/components/Notification/Notification.css:
--------------------------------------------------------------------------------
1 | @keyframes fadeIn {
2 | from {
3 | opacity: 0;
4 | transform: translateY(-4px);
5 | }
6 | to {
7 | opacity: 1;
8 | transform: translateY(0);
9 | }
10 | }
11 |
12 | .this {
13 | width: 80vw;
14 | border-radius: var(--radius-size);
15 | position: absolute;
16 | margin: 10px 10%;
17 | text-align: center;
18 | padding: 8px 5px;
19 | z-index: 100;
20 | color: var(--dark-green);
21 | background-color: var(--green);
22 | font-weight: 400;
23 | font-size: 1.1rem;
24 | animation: fadeIn 180ms ease-out;
25 | animation-fill-mode: both;
26 | }
27 |
--------------------------------------------------------------------------------
/src/ui/components/Notification/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import styles from './Notification.css';
3 |
4 | const Notification = ({
5 | children,
6 | ...props
7 | }) => (
8 |
9 | {children}
10 |
11 | );
12 |
13 | export default Notification;
14 |
--------------------------------------------------------------------------------
/src/ui/components/Title/Title.css:
--------------------------------------------------------------------------------
1 | .this {
2 | display: flex;
3 | flex-grow: 1;
4 | flex-shrink: 0;
5 | align-items: center;
6 | justify-content: center;
7 | margin: 0;
8 | font-size: 1rem;
9 | font-weight: 400;
10 | text-align: center;
11 | color: #555;
12 | cursor: default;
13 | -webkit-app-region: drag;
14 | }
15 |
--------------------------------------------------------------------------------
/src/ui/components/Title/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import styles from './Title.css';
3 |
4 | const Title = ({
5 | children,
6 | ...props
7 | }) => (
8 |
9 | {children}
10 |
11 | );
12 |
13 | export default Title;
14 |
--------------------------------------------------------------------------------
/src/ui/functions.js:
--------------------------------------------------------------------------------
1 | import mime from 'mime';
2 |
3 | /**
4 | * Check the mime and match any images.
5 | *
6 | * @param {String} path
7 | * @return {Boolean}
8 | */
9 | export const isImage = (path) => {
10 | const mimeType = mime.getType(path);
11 | if (!mimeType) {
12 | return false;
13 | }
14 |
15 | return mimeType.match(/^image\/.*/) !== null;
16 | };
17 |
18 |
--------------------------------------------------------------------------------
/src/ui/icons/IconCog/IconCog.css:
--------------------------------------------------------------------------------
1 | .this {
2 | fill: var(--darkest-gray);
3 | width: 20px;
4 | height: 25px;
5 | }
6 |
--------------------------------------------------------------------------------
/src/ui/icons/IconCog/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import styles from './IconCog.css';
3 |
4 | const IconCog = ({
5 | className,
6 | children,
7 | filled = false,
8 | ...props
9 | }) => (
10 |
22 | );
23 |
24 | export default IconCog;
25 |
--------------------------------------------------------------------------------
/src/ui/icons/IconFile/IconFile.css:
--------------------------------------------------------------------------------
1 | .this {
2 | fill: var(--dark-gray);
3 | width: 20px;
4 | height: 25px;
5 | }
6 |
--------------------------------------------------------------------------------
/src/ui/icons/IconFile/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import styles from './IconFile.css';
3 |
4 | const IconFile = ({
5 | className,
6 | children,
7 | filled = false,
8 | ...props
9 | }) => (
10 |
22 | );
23 |
24 | export default IconFile;
25 |
--------------------------------------------------------------------------------
/src/ui/icons/IconFolder/IconFolder.css:
--------------------------------------------------------------------------------
1 | .this {
2 | fill: var(--darkest-gray);
3 | width: 18px;
4 | height: 23px;
5 | }
6 |
--------------------------------------------------------------------------------
/src/ui/icons/IconFolder/index.jsx:
--------------------------------------------------------------------------------
1 | import { h } from 'preact';
2 | import styles from './IconFolder.css';
3 |
4 | const IconFolder = ({
5 | className,
6 | children,
7 | filled = false,
8 | ...props
9 | }) => (
10 |
22 | );
23 |
24 | export default IconFolder;
25 |
--------------------------------------------------------------------------------
/src/ui/index.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --white: #fff;
3 | --lightest-gray: #F7F7F7;
4 | --light-gray: #E8E6E8;
5 | --medium-gray: #D1CfD1;
6 | --dark-gray: #777777;
7 | --darkest-gray: #4A4A4A;
8 | --green: #41F5C0;
9 | --dark-green: #238568;
10 | --purple: #D78BFF;
11 | --pink: #FF8FE5;
12 | --orange: #FF7744;
13 | --yellow: #FBFF64;
14 | --light-blue: #99dbfa;
15 | --blue: #76D3FF;
16 | --dark-blue: #3A677C;
17 | font-family: system, -apple-system, ".SFNSDisplay-Regular", "Helvetica Neue", Helvetica, "Segoe UI", sans-serif;
18 | font-size: 80%;
19 | --radius-size: 4px;
20 | }
21 |
22 | .this {
23 | position: absolute;
24 | top: 0;
25 | right: 0;
26 | bottom: 0;
27 | left: 0;
28 | display: flex;
29 | flex-direction: column;
30 | background-color: var(--white);
31 | overflow: scroll;
32 | }
33 |
34 | .content {
35 | display: flex;
36 | flex-direction: column;
37 | flex-basis: 100%;
38 | }
39 |
40 | .header_button_icon {
41 | fill: var(--dark-gray);
42 | transition: fill 100ms ease-out;
43 | }
44 |
45 | .header_button:hover .header_button_icon {
46 | fill: var(--darkest-gray);
47 | }
48 |
--------------------------------------------------------------------------------
/src/ui/index.js:
--------------------------------------------------------------------------------
1 | import 'babel-polyfill';
2 | import { h, Component, render } from 'preact';
3 | import Button from 'components/Button';
4 | import Center from 'components/Center';
5 | import FileList from 'components/FileList';
6 | import Footer from 'components/Footer';
7 | import Header from 'components/Header';
8 | import Notification from 'components/Notification';
9 | import Title from 'components/Title';
10 | import IconCog from 'icons/IconCog';
11 | import IconFolder from 'icons/IconFolder';
12 | import { basename, join } from 'path';
13 | import { ipcRenderer, shell, remote } from 'electron';
14 | const { Menu, MenuItem } = remote;
15 | import fs from 'fs-extra';
16 | import filesize from 'file-size';
17 | import {
18 | IPC_EVENT_REQUEST_STATE,
19 | IPC_EVENT_SEND_STATE,
20 | IPC_EVENT_HIDE_MENU,
21 | IPC_EVENT_QUIT_APP,
22 | URL_ATTRIBUTION,
23 | URL_BUG,
24 | URL_CONTRIBUTE,
25 | URL_TWEET,
26 | } from '../shared/constants';
27 | import { version } from '../../package.json';
28 |
29 | import styles from './index.css';
30 |
31 | class Application extends Component {
32 |
33 | constructor(props) {
34 | super(props);
35 |
36 | this.state = {
37 | files: [],
38 | connected: false,
39 | synced: false,
40 | daemon: null,
41 | folder: null,
42 | };
43 |
44 | this.onIpcChange = this.onIpcChange.bind(this);
45 | this.onDrop = this.onDrop.bind(this);
46 | this.openSettingsMenu = this.openSettingsMenu.bind(this);
47 | this.openFolder = this.openFolder.bind(this);
48 | }
49 |
50 | componentDidMount() {
51 | ipcRenderer.on(IPC_EVENT_SEND_STATE, this.onIpcChange);
52 | ipcRenderer.send(IPC_EVENT_REQUEST_STATE);
53 | }
54 |
55 | componentWillUnmount() {
56 | ipcRenderer.removeListener(IPC_EVENT_SEND_STATE, this.onIpcChange);
57 | }
58 |
59 | onIpcChange(event, newState) {
60 | this.setState(newState);
61 | }
62 |
63 | onDrop(e) {
64 | e.preventDefault();
65 | const {
66 | folder,
67 | } = this.state;
68 |
69 | Array.from(e.dataTransfer.files).forEach((file) => {
70 | const fileName = basename(file.path);
71 | const newPath = join(folder, fileName);
72 | fs.copy(file.path, newPath, () => {});
73 | });
74 | }
75 |
76 | openSettingsMenu() {
77 | const menu = new Menu();
78 | menu.append(new MenuItem({ label: `Version ${version}`, enabled: false }));
79 | menu.append(new MenuItem({ type: 'separator' }));
80 | menu.append(new MenuItem({ label: 'Attribution', click: () => shell.openExternal(URL_ATTRIBUTION) }));
81 | menu.append(new MenuItem({ type: 'separator' }));
82 | menu.append(new MenuItem({ label: 'Spread the Word', click: () => shell.openExternal(URL_TWEET) }));
83 | menu.append(new MenuItem({ label: 'Contribute', click: () => shell.openExternal(URL_CONTRIBUTE) }));
84 | menu.append(new MenuItem({ label: 'Report a Bug', click: () => shell.openExternal(URL_BUG) }));
85 | menu.append(new MenuItem({ type: 'separator' }));
86 | menu.append(new MenuItem({ label: 'Quit Partyshare', click: () => ipcRenderer.send(IPC_EVENT_QUIT_APP) }));
87 | menu.popup(remote.getCurrentWindow());
88 | }
89 |
90 | openFolder() {
91 | ipcRenderer.send(IPC_EVENT_HIDE_MENU);
92 | shell.openItem(this.state.folder.path);
93 | }
94 |
95 | render(props, state) {
96 | const {
97 | folder,
98 | connected,
99 | files,
100 | notification,
101 | synced,
102 | } = this.state;
103 |
104 | const totalSize = filesize(files.reduce((total, file) => total + file.size, 0), { fixed: 1 }).human('si');
105 |
106 | return (
107 | e.preventDefault()}
109 | onDrop={this.onDrop}
110 | >
111 |
112 |
121 |
122 | {connected && synced && `Sharing ${files.length} files (${totalSize})`}
123 | {connected && !synced && 'Syncing…'}
124 |
125 |
134 |
135 |
136 |
137 | { notification ? {notification} : null }
138 | { connected ? :
Connecting…}
139 | { files && files.length < 1 && }
140 |
141 |
142 | );
143 | }
144 | }
145 |
146 | document.addEventListener('DOMContentLoaded', () => {
147 | render(, document.body);
148 | });
149 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const ExtractTextPlugin = require('extract-text-webpack-plugin');
3 |
4 | const extractCSSPlugin = new ExtractTextPlugin({
5 | filename: 'css/[name].css',
6 | allChunks: true,
7 | });
8 |
9 |
10 | module.exports = {
11 | entry: path.resolve(__dirname, 'src', 'ui', 'index.js'),
12 | output: {
13 | filename: 'js/bundle.js',
14 | path: path.resolve(__dirname, 'src', 'static'),
15 | },
16 | target: 'electron',
17 | module: {
18 | rules: [
19 | {
20 | test: /\.(js|jsx)$/,
21 | loader: 'babel-loader',
22 | include: [
23 | path.resolve(__dirname, 'src', 'ui'),
24 | ],
25 | },
26 | {
27 | test: /\.(css|scss)$/,
28 | loader: extractCSSPlugin.extract({
29 | use: [
30 | {
31 | loader: 'css-loader',
32 | options: {
33 | modules: true,
34 | importLoaders: 2,
35 | localIdentName: '[name]--[local]___[hash:base64:5]',
36 | },
37 | },
38 | ],
39 | }),
40 | },
41 | ],
42 | },
43 | resolve: {
44 | alias: {
45 | 'react': 'preact-compat',
46 | 'react-dom': 'preact-compat',
47 | },
48 | modules: [
49 | path.resolve(__dirname, 'node_modules'),
50 | path.resolve(__dirname, 'src', 'ui'),
51 | path.resolve(__dirname, 'src', 'electron'),
52 | ],
53 | extensions: ['.js', '.jsx', '.json'],
54 | },
55 | plugins: [extractCSSPlugin],
56 | };
57 |
--------------------------------------------------------------------------------