├── .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 | [![Build Status](https://travis-ci.org/BusterLabs/Partyshare.svg?branch=master)](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 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/static/svg/icon_cog_filled.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/static/svg/icon_file.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/static/svg/icon_file_filled.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/static/svg/icon_folder.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/static/svg/icon_folder_filled.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 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 | 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 |
    9 | {children} 10 |
    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 | 16 | { filled ? 17 | 18 | : 19 | 20 | } 21 | 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 | 16 | { filled ? 17 | 18 | : 19 | 20 | } 21 | 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 | 16 | { filled ? 17 | 18 | : 19 | 20 | } 21 | 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 &&
    * Heads up, files added to IPFS can‘t be deleted
    } 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 | --------------------------------------------------------------------------------