├── src ├── translations │ ├── es.json │ └── en.json ├── images │ ├── logo.png │ ├── compact.png │ ├── turtle.png │ ├── wrench.png │ ├── arrow-up.png │ ├── lock_icon.png │ ├── progress.png │ ├── settings.png │ ├── arrow-down.png │ ├── blue-turtle.png │ ├── filter_bar.png │ ├── filter_icon.png │ ├── toolbar-info.png │ ├── inspector-info.png │ ├── toolbar-close.png │ ├── toolbar-folder.png │ ├── toolbar-pause.png │ ├── toolbar-start.png │ ├── file-priority-low.png │ ├── inspector-files.png │ ├── inspector-peers.png │ ├── toolbar-pause-all.png │ ├── toolbar-start-all.png │ ├── file-priority-high.png │ ├── inspector-trackers.png │ ├── file-priority-normal.png │ └── buttons │ │ └── torrent_buttons.png ├── components │ ├── Torrent │ │ ├── services │ │ │ ├── index.js │ │ │ ├── progress-details.js │ │ │ └── peer-details.js │ │ ├── ProgressDetails │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ ├── Pending.spec.js.snap │ │ │ │ │ ├── Magnet.spec.js.snap │ │ │ │ │ ├── Done.spec.js.snap │ │ │ │ │ └── index.spec.js.snap │ │ │ │ ├── Pending.spec.js │ │ │ │ ├── Magnet.spec.js │ │ │ │ ├── Done.spec.js │ │ │ │ └── index.spec.js │ │ │ ├── index.js │ │ │ ├── Pending.js │ │ │ ├── Magnet.js │ │ │ └── Done.js │ │ ├── StatusDetails │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ ├── Checking.spec.js.snap │ │ │ │ │ ├── Seeding.spec.js.snap │ │ │ │ │ ├── Error.spec.js.snap │ │ │ │ │ ├── Downloading.spec.js.snap │ │ │ │ │ ├── index.spec.js.snap │ │ │ │ │ └── Status.spec.js.snap │ │ │ │ ├── Checking.spec.js │ │ │ │ ├── Seeding.spec.js │ │ │ │ ├── Error.spec.js │ │ │ │ ├── Downloading.spec.js │ │ │ │ ├── index.spec.js │ │ │ │ └── Status.spec.js │ │ │ ├── Checking.js │ │ │ ├── Seeding.js │ │ │ ├── index.js │ │ │ ├── Error.js │ │ │ ├── Status.js │ │ │ └── Downloading.js │ │ ├── StatusButton │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── index.spec.js.snap │ │ │ │ └── index.spec.js │ │ │ ├── index.js │ │ │ └── styles │ │ │ │ └── index.css │ │ ├── ProgressBar │ │ │ ├── __tests__ │ │ │ │ ├── __snapshots__ │ │ │ │ │ └── index.spec.js.snap │ │ │ │ └── index.spec.js │ │ │ ├── index.js │ │ │ └── styles │ │ │ │ └── index.css │ │ ├── Compact.js │ │ ├── styles │ │ │ └── index.css │ │ ├── Full.js │ │ └── index.js │ ├── menus │ │ ├── ContextMenu │ │ │ ├── styles │ │ │ │ └── index.css │ │ │ ├── Menu.js │ │ │ └── index.js │ │ ├── TorrentContextMenu │ │ │ ├── styles │ │ │ │ └── index.css │ │ │ └── index.js │ │ ├── RateContextMenu │ │ │ ├── styles │ │ │ │ └── index.css │ │ │ └── index.js │ │ ├── SortByContextMenu │ │ │ ├── styles │ │ │ │ └── index.css │ │ │ └── index.js │ │ └── SettingsContextMenu │ │ │ ├── styles │ │ │ └── index.css │ │ │ └── index.js │ ├── dialogs │ │ ├── PreferencesDialog │ │ │ ├── fields │ │ │ │ ├── TextRow │ │ │ │ │ ├── styles │ │ │ │ │ │ └── index.css │ │ │ │ │ └── index.js │ │ │ │ ├── PortTestRow │ │ │ │ │ ├── styles │ │ │ │ │ │ └── index.css │ │ │ │ │ └── index.js │ │ │ │ ├── CheckRow │ │ │ │ │ ├── styles │ │ │ │ │ │ └── index.css │ │ │ │ │ └── index.js │ │ │ │ ├── SelectRow │ │ │ │ │ ├── styles │ │ │ │ │ │ └── index.css │ │ │ │ │ └── index.js │ │ │ │ └── CheckValueRow │ │ │ │ │ ├── styles │ │ │ │ │ └── index.css │ │ │ │ │ └── index.js │ │ │ ├── styles │ │ │ │ └── index.css │ │ │ ├── NetworkTabPanel │ │ │ │ └── index.js │ │ │ ├── TorrentsTabPanel │ │ │ │ └── index.js │ │ │ ├── PeersTabPanel │ │ │ │ └── index.js │ │ │ ├── SpeedTabPanel │ │ │ │ └── index.js │ │ │ └── index.js │ │ ├── StatisticsDialog │ │ │ ├── styles │ │ │ │ └── index.css │ │ │ └── index.js │ │ ├── AboutDialog │ │ │ ├── styles │ │ │ │ └── index.css │ │ │ └── index.js │ │ ├── ConnectionDialog │ │ │ ├── styles │ │ │ │ └── index.css │ │ │ └── index.js │ │ ├── PromptDialog │ │ │ ├── styles │ │ │ │ └── index.css │ │ │ └── index.js │ │ ├── Dialog │ │ │ ├── Header │ │ │ │ ├── styles │ │ │ │ │ └── index.css │ │ │ │ └── index.js │ │ │ ├── styles │ │ │ │ └── index.css │ │ │ └── index.js │ │ └── OpenDialog │ │ │ ├── styles │ │ │ └── index.css │ │ │ └── index.js │ ├── SelectableList │ │ ├── Item │ │ │ └── index.js │ │ ├── styles │ │ │ └── index.css │ │ └── index.js │ ├── Inspector │ │ ├── Row.js │ │ ├── Peers │ │ │ ├── index.js │ │ │ ├── styles │ │ │ │ └── index.css │ │ │ └── PeerGroup.js │ │ ├── Trackers │ │ │ ├── index.js │ │ │ ├── styles │ │ │ │ └── index.css │ │ │ ├── services │ │ │ │ └── tracker-stats.js │ │ │ └── TrackerGroup.js │ │ ├── Files │ │ │ ├── FileList.js │ │ │ ├── styles │ │ │ │ └── index.css │ │ │ ├── index.js │ │ │ ├── WantedButton.js │ │ │ ├── FileRow.js │ │ │ ├── PriorityButton.js │ │ │ └── services │ │ │ │ └── generate-tree.js │ │ ├── Details.js │ │ ├── Activity.js │ │ ├── styles │ │ │ └── index.css │ │ └── index.js │ ├── DropzoneLayer │ │ ├── styles │ │ │ └── index.css │ │ └── index.js │ ├── App │ │ └── styles │ │ │ └── index.css │ └── toolbars │ │ ├── ActionToolbar │ │ ├── styles │ │ │ └── index.css │ │ └── index.js │ │ ├── FilterToolbar │ │ ├── styles │ │ │ └── index.css │ │ └── index.js │ │ └── StatusToolbar │ │ ├── styles │ │ └── index.css │ │ └── index.js ├── stores │ ├── tracker.js │ ├── __tests__ │ │ ├── torrent-upload.test.js │ │ └── torrent-store.spec.js │ ├── stats-store.js │ ├── index.js │ ├── torrent-upload.js │ ├── session-store.js │ ├── torrent.js │ └── view-store.js ├── util │ ├── converters.js │ ├── common.js │ ├── notifications.js │ ├── uri.js │ ├── comparators.js │ └── rpc.js ├── reactions │ └── notify.js └── index.js ├── .eslintignore ├── public ├── favicon.ico └── index.html ├── .gitignore ├── test └── util │ └── createComponentWithIntl.js ├── config ├── jest │ ├── fileTransform.js │ └── cssTransform.js ├── polyfills.js ├── env.js └── paths.js ├── .travis.yml ├── PULL_REQUEST_TEMPLATE.md ├── scripts ├── test.js └── translate.js ├── LICENSE ├── README.md └── package.json /src/translations/es.json: -------------------------------------------------------------------------------- 1 | { 2 | "activity.title": "Actividad" 3 | } 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | src/util/common.js 2 | src/util/formatters.js 3 | src/util/uri.js 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fcsonline/react-transmission/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fcsonline/react-transmission/HEAD/src/images/logo.png -------------------------------------------------------------------------------- /src/images/compact.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fcsonline/react-transmission/HEAD/src/images/compact.png -------------------------------------------------------------------------------- /src/images/turtle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fcsonline/react-transmission/HEAD/src/images/turtle.png -------------------------------------------------------------------------------- /src/images/wrench.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fcsonline/react-transmission/HEAD/src/images/wrench.png -------------------------------------------------------------------------------- /src/images/arrow-up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fcsonline/react-transmission/HEAD/src/images/arrow-up.png -------------------------------------------------------------------------------- /src/images/lock_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fcsonline/react-transmission/HEAD/src/images/lock_icon.png -------------------------------------------------------------------------------- /src/images/progress.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fcsonline/react-transmission/HEAD/src/images/progress.png -------------------------------------------------------------------------------- /src/images/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fcsonline/react-transmission/HEAD/src/images/settings.png -------------------------------------------------------------------------------- /src/images/arrow-down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fcsonline/react-transmission/HEAD/src/images/arrow-down.png -------------------------------------------------------------------------------- /src/images/blue-turtle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fcsonline/react-transmission/HEAD/src/images/blue-turtle.png -------------------------------------------------------------------------------- /src/images/filter_bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fcsonline/react-transmission/HEAD/src/images/filter_bar.png -------------------------------------------------------------------------------- /src/images/filter_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fcsonline/react-transmission/HEAD/src/images/filter_icon.png -------------------------------------------------------------------------------- /src/images/toolbar-info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fcsonline/react-transmission/HEAD/src/images/toolbar-info.png -------------------------------------------------------------------------------- /src/images/inspector-info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fcsonline/react-transmission/HEAD/src/images/inspector-info.png -------------------------------------------------------------------------------- /src/images/toolbar-close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fcsonline/react-transmission/HEAD/src/images/toolbar-close.png -------------------------------------------------------------------------------- /src/images/toolbar-folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fcsonline/react-transmission/HEAD/src/images/toolbar-folder.png -------------------------------------------------------------------------------- /src/images/toolbar-pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fcsonline/react-transmission/HEAD/src/images/toolbar-pause.png -------------------------------------------------------------------------------- /src/images/toolbar-start.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fcsonline/react-transmission/HEAD/src/images/toolbar-start.png -------------------------------------------------------------------------------- /src/images/file-priority-low.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fcsonline/react-transmission/HEAD/src/images/file-priority-low.png -------------------------------------------------------------------------------- /src/images/inspector-files.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fcsonline/react-transmission/HEAD/src/images/inspector-files.png -------------------------------------------------------------------------------- /src/images/inspector-peers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fcsonline/react-transmission/HEAD/src/images/inspector-peers.png -------------------------------------------------------------------------------- /src/images/toolbar-pause-all.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fcsonline/react-transmission/HEAD/src/images/toolbar-pause-all.png -------------------------------------------------------------------------------- /src/images/toolbar-start-all.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fcsonline/react-transmission/HEAD/src/images/toolbar-start-all.png -------------------------------------------------------------------------------- /src/images/file-priority-high.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fcsonline/react-transmission/HEAD/src/images/file-priority-high.png -------------------------------------------------------------------------------- /src/images/inspector-trackers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fcsonline/react-transmission/HEAD/src/images/inspector-trackers.png -------------------------------------------------------------------------------- /src/images/file-priority-normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fcsonline/react-transmission/HEAD/src/images/file-priority-normal.png -------------------------------------------------------------------------------- /src/images/buttons/torrent_buttons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fcsonline/react-transmission/HEAD/src/images/buttons/torrent_buttons.png -------------------------------------------------------------------------------- /src/components/Torrent/services/index.js: -------------------------------------------------------------------------------- 1 | export { getPeerDetails, getPeerDetailsShort } from './peer-details'; 2 | export getProgressDetails from './progress-details'; 3 | -------------------------------------------------------------------------------- /src/components/menus/ContextMenu/styles/index.css: -------------------------------------------------------------------------------- 1 | .contextMenuOuter { 2 | position: absolute; 3 | /*padding: 0 5px;*/ 4 | } 5 | 6 | .contextMenuInner { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /src/components/Torrent/ProgressDetails/__tests__/__snapshots__/Pending.spec.js.snap: -------------------------------------------------------------------------------- 1 | exports[`test Pending 1`] = ` 2 | 3 | 90.0 kB of 100.0 kB (90%) 4 | 5 | `; 6 | -------------------------------------------------------------------------------- /src/components/Torrent/StatusDetails/__tests__/__snapshots__/Checking.spec.js.snap: -------------------------------------------------------------------------------- 1 | exports[`test Checking 1`] = ` 2 | 3 | Verifying local data (50% tested) 4 | 5 | `; 6 | -------------------------------------------------------------------------------- /src/components/dialogs/PreferencesDialog/fields/TextRow/styles/index.css: -------------------------------------------------------------------------------- 1 | .row { 2 | display: flex; 3 | padding-bottom: 10px; 4 | } 5 | 6 | .key { 7 | width: 180px; 8 | } 9 | 10 | .value { 11 | flex: 1; 12 | } 13 | -------------------------------------------------------------------------------- /src/components/dialogs/PreferencesDialog/fields/PortTestRow/styles/index.css: -------------------------------------------------------------------------------- 1 | .row { 2 | display: flex; 3 | padding-bottom: 10px; 4 | } 5 | 6 | .key { 7 | width: 180px; 8 | } 9 | 10 | .value { 11 | flex: 1; 12 | } 13 | -------------------------------------------------------------------------------- /src/components/dialogs/PreferencesDialog/fields/CheckRow/styles/index.css: -------------------------------------------------------------------------------- 1 | .row { 2 | display: flex; 3 | padding-bottom: 10px; 4 | } 5 | 6 | .row input { 7 | margin-right: 5px; 8 | } 9 | 10 | .row label { 11 | flex: 1; 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | # testing 7 | coverage 8 | 9 | # production 10 | build 11 | 12 | # misc 13 | .DS_Store 14 | .env 15 | npm-debug.log 16 | -------------------------------------------------------------------------------- /src/components/dialogs/PreferencesDialog/styles/index.css: -------------------------------------------------------------------------------- 1 | .body { 2 | composes: body from "../../Dialog/styles/index.css"; 3 | } 4 | 5 | .content { 6 | padding: 10px 30px; 7 | display: flex; 8 | flex-direction: column; 9 | min-height: 350px; 10 | } 11 | -------------------------------------------------------------------------------- /src/components/dialogs/StatisticsDialog/styles/index.css: -------------------------------------------------------------------------------- 1 | .body { 2 | composes: body from "../../Dialog/styles/index.css"; 3 | padding: 10px 30px; 4 | } 5 | 6 | .row { 7 | display: flex; 8 | padding: 0px 10px 10px 20px; 9 | } 10 | 11 | .key { 12 | width: 120px; 13 | } 14 | 15 | .value { 16 | flex: 1; 17 | } 18 | -------------------------------------------------------------------------------- /src/components/Torrent/ProgressDetails/__tests__/__snapshots__/Magnet.spec.js.snap: -------------------------------------------------------------------------------- 1 | exports[`test Magnet 1`] = ` 2 | 3 | Magnetized transfer - retrieving metadata (75%) 4 | 5 | `; 6 | 7 | exports[`test Magnet stopped 1`] = ` 8 | 9 | Magnetized transfer - needs metadata (75%) 10 | 11 | `; 12 | -------------------------------------------------------------------------------- /src/components/Torrent/ProgressDetails/__tests__/__snapshots__/Done.spec.js.snap: -------------------------------------------------------------------------------- 1 | exports[`test Done 1`] = ` 2 | 3 | 100.0 kB, uploaded 100.0 kB (Ratio: 2.00) 4 | 5 | `; 6 | 7 | exports[`test Done partially 1`] = ` 8 | 9 | 10.0 kB of 100.0 kB (10%), uploaded 10.0 kB (Ratio: 2.00) 10 | 11 | `; 12 | -------------------------------------------------------------------------------- /src/components/Torrent/StatusButton/__tests__/__snapshots__/index.spec.js.snap: -------------------------------------------------------------------------------- 1 | exports[`test StatusButton pause 1`] = ` 2 | 16 | 17 | ); 18 | } 19 | } 20 | 21 | export default Header; 22 | -------------------------------------------------------------------------------- /src/components/Torrent/StatusDetails/Seeding.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FormattedMessage } from 'react-intl'; 3 | 4 | import { formatUL } from 'util/formatters'; 5 | 6 | export default ({ torrent: { peersGettingFromUs, peersConnected, rateUpload } }) => ( 7 | 16 | ); 17 | -------------------------------------------------------------------------------- /src/components/Torrent/StatusDetails/__tests__/__snapshots__/index.spec.js.snap: -------------------------------------------------------------------------------- 1 | exports[`test StatusDetails checking 1`] = ` 2 | 3 | Verifying local data (100% tested) 4 | 5 | `; 6 | 7 | exports[`test StatusDetails downloading 1`] = ` 8 | 9 | Downloading from 5 of 5 peers and 5 web seeds - ↓ 10.00 MB/s ↑ 100 kB/s 10 | 11 | `; 12 | 13 | exports[`test StatusDetails has errors 1`] = `null`; 14 | 15 | exports[`test StatusDetails seeding 1`] = ` 16 | 17 | Seeding to 5 of 5 connected peers - ↑ 100 kB/s 18 | 19 | `; 20 | 21 | exports[`test StatusDetails status 1`] = ` 22 | 23 | Unknown 24 | 25 | `; 26 | -------------------------------------------------------------------------------- /src/components/Torrent/ProgressDetails/__tests__/Magnet.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import createComponentWithIntl from 'test/util/createComponentWithIntl'; 3 | 4 | import Magnet from '../Magnet'; 5 | 6 | test('Magnet', () => { 7 | const component = createComponentWithIntl( 8 | 9 | ); 10 | 11 | expect(component.toJSON()).toMatchSnapshot(); 12 | }); 13 | 14 | test('Magnet stopped', () => { 15 | const component = createComponentWithIntl( 16 | 17 | ); 18 | 19 | expect(component.toJSON()).toMatchSnapshot(); 20 | }); 21 | -------------------------------------------------------------------------------- /config/polyfills.js: -------------------------------------------------------------------------------- 1 | if (typeof Promise === 'undefined') { 2 | // Rejection tracking prevents a common issue where React gets into an 3 | // inconsistent state due to an error, but it gets swallowed by a Promise, 4 | // and the user has no idea what causes React's erratic future behavior. 5 | require('promise/lib/rejection-tracking').enable(); 6 | window.Promise = require('promise/lib/es6-extensions.js'); 7 | } 8 | 9 | // fetch() polyfill for making API calls. 10 | require('whatwg-fetch'); 11 | 12 | // Object.assign() is commonly used with React. 13 | // It will use the native implementation if it's present and isn't buggy. 14 | Object.assign = require('object-assign'); 15 | -------------------------------------------------------------------------------- /src/components/Inspector/Peers/styles/index.css: -------------------------------------------------------------------------------- 1 | .peerList { 2 | width: 100%; 3 | border-collapse: collapse; 4 | } 5 | 6 | .peerRow:nth-child(even) { 7 | background-color: #EEEEEE; 8 | } 9 | 10 | .encryptedCol { 11 | width: 16px; 12 | } 13 | 14 | .upCol { 15 | width: 70px; 16 | } 17 | 18 | .downCol { 19 | width: 70px; 20 | } 21 | 22 | .percentCol { 23 | width: 30px; 24 | padding-right: 5px; 25 | text-align: right; 26 | } 27 | 28 | .statusCol { 29 | width: 40px; 30 | padding-right: 5px; 31 | } 32 | 33 | .addressCol { 34 | width: 180px; 35 | } 36 | 37 | .clientCol { 38 | white-space: nowrap; 39 | overflow: hidden; 40 | text-overflow: ellipsis; 41 | } 42 | -------------------------------------------------------------------------------- /src/stores/stats-store.js: -------------------------------------------------------------------------------- 1 | import {observable, action} from 'mobx'; 2 | 3 | class StatsStore { 4 | @observable stats = {}; 5 | @observable cumulativeStats = {}; 6 | @observable currentStats = {}; 7 | 8 | constructor(rpc) { 9 | this.rpc = rpc; 10 | } 11 | 12 | @action getStats() { 13 | return this.rpc.sendRequest('session-stats').then(action((response) => { 14 | response.json().then(action((result) => { 15 | this.stats = result.arguments; 16 | this.cumulativeStats = result.arguments['cumulative-stats']; 17 | this.currentStats = result.arguments['current-stats']; 18 | })); 19 | })); 20 | } 21 | } 22 | 23 | export default StatsStore; 24 | -------------------------------------------------------------------------------- /src/components/Inspector/Files/FileList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import CSSModules from 'react-css-modules'; 3 | 4 | import FileRow from './FileRow'; 5 | 6 | import styles from './styles/index.css'; 7 | 8 | function FileList({ entries, setPriority, setWanted }) { 9 | return ( 10 |
    11 | {Object.keys(entries).map((key, index) => ( 12 |
  • 13 | 19 |
  • 20 | ))} 21 |
22 | ); 23 | }; 24 | 25 | export default CSSModules(styles)(FileList); 26 | -------------------------------------------------------------------------------- /src/components/Torrent/ProgressDetails/__tests__/Done.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import createComponentWithIntl from 'test/util/createComponentWithIntl'; 3 | 4 | import Done from '../Done'; 5 | 6 | test('Done', () => { 7 | const component = createComponentWithIntl( 8 | 9 | ); 10 | 11 | expect(component.toJSON()).toMatchSnapshot(); 12 | }); 13 | 14 | test('Done partially', () => { 15 | const component = createComponentWithIntl( 16 | 17 | ); 18 | 19 | expect(component.toJSON()).toMatchSnapshot(); 20 | }); 21 | -------------------------------------------------------------------------------- /src/components/Torrent/ProgressBar/__tests__/__snapshots__/index.spec.js.snap: -------------------------------------------------------------------------------- 1 | exports[`test ProgressBar leeching 1`] = ` 2 |
4 | 7 |
8 | `; 9 | 10 | exports[`test ProgressBar magnet 1`] = ` 11 |
13 | 16 |
17 | `; 18 | 19 | exports[`test ProgressBar paused 1`] = ` 20 |
22 | 25 |
26 | `; 27 | 28 | exports[`test ProgressBar seeding 1`] = ` 29 |
31 | 34 |
35 | `; 36 | -------------------------------------------------------------------------------- /src/components/Torrent/ProgressDetails/Pending.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FormattedMessage, injectIntl } from 'react-intl'; 3 | 4 | import { size as formatSize } from 'util/formatters'; 5 | 6 | function ProgressPending({ torrent, intl }) { 7 | const { sizeWhenDone, leftUntilDone, percentDone } = torrent; 8 | 9 | return ( 10 | 19 | ); 20 | } 21 | 22 | export default injectIntl(ProgressPending); 23 | -------------------------------------------------------------------------------- /src/components/Torrent/StatusDetails/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Seeding from './Seeding'; 4 | import Checking from './Checking'; 5 | import Downloading from './Downloading'; 6 | import Error from './Error'; 7 | import Status from './Status'; 8 | 9 | function StatusDetails({ torrent }) { 10 | if (torrent.hasErrors) { 11 | return ; 12 | } 13 | 14 | if (torrent.isDownloading) { 15 | return ; 16 | } 17 | 18 | if (torrent.isSeeding) { 19 | return ; 20 | } 21 | 22 | if (torrent.isChecking) { 23 | return ; 24 | } 25 | 26 | return ; 27 | } 28 | 29 | export default StatusDetails; 30 | -------------------------------------------------------------------------------- /src/components/dialogs/PreferencesDialog/fields/TextRow/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component} from 'react'; 2 | import CSSModules from 'react-css-modules'; 3 | import { inject, observer } from 'mobx-react'; 4 | 5 | import styles from './styles/index.css'; 6 | 7 | @inject('session_store') 8 | @observer 9 | @CSSModules(styles) 10 | class TextRow extends Component { 11 | render() { 12 | const value = this.props.session_store.settings[this.props.id]; 13 | 14 | return ( 15 |
16 |
{this.props.label}:
17 |
18 | 19 |
20 |
21 | ); 22 | } 23 | } 24 | 25 | export default TextRow; 26 | -------------------------------------------------------------------------------- /src/reactions/notify.js: -------------------------------------------------------------------------------- 1 | import { reaction } from 'mobx'; 2 | 3 | import { 4 | buildTorrentAddedNotification, 5 | buildTorrentCompletedNotification, 6 | showNotification, 7 | } from 'util/notifications'; 8 | 9 | export default function({ view_store, torrents_store }) { 10 | reaction( 11 | () => ({ 12 | notificationsEnabled: view_store.notificationsEnabled, 13 | notifications: [ 14 | ...torrents_store.startedTorrents.map(buildTorrentAddedNotification), 15 | ...torrents_store.completedTorrents.map(buildTorrentCompletedNotification), 16 | ], 17 | }), 18 | ({ notificationsEnabled, notifications }) => { 19 | if (!notificationsEnabled) return; 20 | 21 | notifications.forEach(showNotification); 22 | } 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/components/Torrent/StatusButton/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import CSSModules from 'react-css-modules'; 3 | import autobind from 'autobind-decorator'; 4 | import { observer } from 'mobx-react'; 5 | 6 | import styles from './styles/index.css'; 7 | 8 | @observer 9 | @CSSModules(styles) 10 | class StatusButton extends Component { 11 | static defaultProps = { 12 | onToggle: () => {}, 13 | } 14 | 15 | @autobind onClick(event) { 16 | this.props.onToggle(this.props.torrent.id); 17 | } 18 | 19 | render() { 20 | return ( 21 | 20 | 27 | 34 | 35 | ); 36 | } 37 | 38 | export default CSSModules(styles)(PriorityButton); 39 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Provider } from 'mobx-react'; 4 | import { AppContainer } from 'react-hot-loader'; 5 | import { IntlProvider, addLocaleData } from 'react-intl'; 6 | 7 | import * as stores from 'stores'; 8 | import notify from 'reactions/notify'; 9 | 10 | import App from 'components/App'; 11 | 12 | import en from 'react-intl/locale-data/en'; 13 | import es from 'react-intl/locale-data/es'; 14 | import enTranslations from 'translations/en.json'; 15 | import esTranslations from 'translations/es.json'; 16 | 17 | addLocaleData([...en, ...es]); 18 | 19 | const messages = { 20 | 'en-US': enTranslations, 21 | en: enTranslations, 22 | es: esTranslations, 23 | }; 24 | 25 | // Start reactions 26 | notify(stores); 27 | 28 | const rootEl = document.getElementById('root'); 29 | 30 | function renderApp(app) { 31 | return ReactDOM.render( 32 | 33 | 34 | 35 | {app} 36 | 37 | 38 | , 39 | rootEl 40 | ); 41 | } 42 | 43 | renderApp(); 44 | 45 | if (module.hot) { 46 | module.hot.accept('components/App', () => { 47 | const NextApp = require('components/App').default; 48 | 49 | renderApp(); 50 | }); 51 | } 52 | -------------------------------------------------------------------------------- /src/components/Inspector/styles/index.css: -------------------------------------------------------------------------------- 1 | .inspector { 2 | text-align: left; 3 | padding: 15px; 4 | } 5 | 6 | .inspector :global .react-tabs [role=tablist] { 7 | border: none; 8 | margin: 0; 9 | padding: 0; 10 | text-align: center; 11 | } 12 | 13 | .inspector :global .react-tabs [role=tab] { 14 | border-radius: 0px; 15 | background-color: #dddddd; 16 | background-image: -webkit-gradient(linear, left top, left bottom, from(#ffffff), to(#bbbbbb)); 17 | background-image: -webkit-linear-gradient(top, #ffffff, #bbbbbb); 18 | background-image: -moz-linear-gradient(top, #ffffff, #bbbbbb); 19 | background-image: -ms-linear-gradient(top, #ffffff, #bbbbbb); 20 | background-image: -o-linear-gradient(top, #ffffff, #bbbbbb); 21 | background-image: linear-gradient(top, #ffffff, #bbbbbb); 22 | } 23 | 24 | .inspector :global .react-tabs [role=tab][aria-selected=true] { 25 | background-image: -webkit-linear-gradient(top, #cdcdff, white); 26 | } 27 | 28 | .inspector :global .react-tabs [role=tab]:first-child { 29 | border-radius: 5px 0px 0px 5px; 30 | } 31 | 32 | .inspector :global .react-tabs [role=tab]:last-child { 33 | border-radius: 0px 5px 5px 0px; 34 | } 35 | 36 | .inspector h1 { 37 | font-weight: bold; 38 | font-size: large; 39 | } 40 | 41 | /* Row */ 42 | .row { 43 | display: flex; 44 | padding-bottom: 10px; 45 | } 46 | 47 | .key { 48 | width: 120px; 49 | } 50 | 51 | .value { 52 | flex: 1; 53 | } 54 | -------------------------------------------------------------------------------- /src/util/comparators.js: -------------------------------------------------------------------------------- 1 | export function compareById(ta, tb) { 2 | return ta.id - tb.id; 3 | } 4 | 5 | export function compareByName(ta, tb) { 6 | return ta.publicName.localeCompare(tb.publicName) || compareById(ta, tb); 7 | } 8 | 9 | export function compareByQueue(ta, tb) { 10 | return ta.queuePosition - tb.queuePosition; 11 | } 12 | 13 | export function compareByAge(ta, tb) { 14 | const a = ta.addedDate; 15 | const b = tb.addedDate; 16 | 17 | return (b - a) || compareByQueue(ta, tb); 18 | } 19 | 20 | export function compareByState(ta, tb) { 21 | const a = ta.status; 22 | const b = tb.status; 23 | 24 | return (b - a) || compareByQueue(ta, tb); 25 | } 26 | 27 | export function compareByActivity(ta, tb) { 28 | const a = ta.activity; 29 | const b = tb.activity; 30 | 31 | return (b - a) || compareByState(ta, tb); 32 | } 33 | 34 | export function compareByRatio(ta, tb) { 35 | const a = ta.uploadRatio; 36 | const b = tb.uploadRatio; 37 | 38 | if (a < b) { 39 | return 1; 40 | } 41 | if (a > b) { 42 | return -1; 43 | } 44 | 45 | return compareByState(ta, tb); 46 | } 47 | 48 | export function compareByProgress(ta, tb) { 49 | const a = ta.percentDone; 50 | const b = tb.percentDone; 51 | 52 | return (a - b) || compareByRatio(ta, tb); 53 | } 54 | 55 | export function compareBySize(ta, tb) { 56 | const a = ta.totalSize; 57 | const b = tb.totalSize; 58 | 59 | return (a - b) || compareByName(ta, tb); 60 | } 61 | -------------------------------------------------------------------------------- /src/components/toolbars/FilterToolbar/styles/index.css: -------------------------------------------------------------------------------- 1 | .toolbar { 2 | background: #ccc; 3 | padding: 5px; 4 | 5 | display: -ms-flexbox; 6 | display: -webkit-flex; 7 | display: flex; 8 | 9 | -ms-flex-align: center; 10 | -webkit-align-items: center; 11 | -webkit-box-align: center; 12 | 13 | align-items: center; 14 | 15 | border-bottom: 1px solid #AAA; 16 | background-color: #cccccc; 17 | background-image: -webkit-gradient(linear, left top, left bottom, from(#dddddd), to(#bbbbbb)); 18 | background-image: -webkit-linear-gradient(top, #dddddd, #bbbbbb); 19 | background-image: -moz-linear-gradient(top, #dddddd, #bbbbbb); 20 | background-image: -ms-linear-gradient(top, #dddddd, #bbbbbb); 21 | background-image: -o-linear-gradient(top, #dddddd, #bbbbbb); 22 | background-image: linear-gradient(top, #dddddd, #bbbbbb); 23 | } 24 | 25 | .filters { 26 | flex: 1; 27 | } 28 | 29 | .filters select { 30 | width: 150px; 31 | margin-left: 5px; 32 | outline: none; 33 | border: 1px solid #999; 34 | border-radius: 5px; 35 | cursor: pointer; 36 | padding: 2px 5px; 37 | line-height: 16px; 38 | } 39 | 40 | .filter { 41 | border-radius: 5px; 42 | margin-left: 5px; 43 | outline: none; 44 | border: 1px solid #999; 45 | padding: 2px 5px; 46 | line-height: 16px; 47 | } 48 | 49 | .stats span { 50 | margin-left: 5px; 51 | } 52 | 53 | .stats img { 54 | margin-right: 2px; 55 | } 56 | 57 | .counter { 58 | margin-left: 5px; 59 | } 60 | -------------------------------------------------------------------------------- /src/util/rpc.js: -------------------------------------------------------------------------------- 1 | import 'whatwg-fetch'; 2 | import { filter, first } from 'lodash'; 3 | 4 | class RPC { 5 | static SESSION_ID_HEADER = 'X-Transmission-Session-Id'; 6 | 7 | constructor(onConnect = () => {}, onDisconnect = () => {}) { 8 | this._url = `/${first(filter(document.location.pathname.split('/')))}/rpc`; 9 | this._onConnect = onConnect; 10 | this._onDisconnect = onDisconnect; 11 | this._sessionId = null; 12 | } 13 | 14 | sendRequest(method, data) { 15 | const headers = { 16 | [RPC.SESSION_ID_HEADER]: this._sessionId, 17 | }; 18 | const body = JSON.stringify({ 19 | 'arguments': data, 20 | method, 21 | }); 22 | 23 | return fetch(this._url, { 24 | method: 'POST', 25 | headers, 26 | credentials: 'include', 27 | body, 28 | }).then((response) => { 29 | if (response.status === 502) { 30 | this._onDisconnect(response); 31 | } else if (response.status === 409 && response.headers.has(RPC.SESSION_ID_HEADER)) { 32 | this._onConnect(response); 33 | 34 | this._sessionId = response.headers.get(RPC.SESSION_ID_HEADER); 35 | 36 | return fetch(this._url, { 37 | method: 'POST', 38 | headers: {...headers, [RPC.SESSION_ID_HEADER]: this._sessionId}, 39 | body, 40 | }); 41 | } 42 | 43 | // TODO: Review fullfilment value 44 | return response; 45 | }); 46 | } 47 | } 48 | 49 | export default RPC; 50 | -------------------------------------------------------------------------------- /src/components/dialogs/PreferencesDialog/fields/PortTestRow/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component} from 'react'; 2 | import CSSModules from 'react-css-modules'; 3 | import { inject, observer } from 'mobx-react'; 4 | 5 | import styles from './styles/index.css'; 6 | 7 | @inject('session_store') 8 | @observer 9 | @CSSModules(styles) 10 | class PortTestRow extends Component { 11 | constructor(props) { 12 | super(props); 13 | 14 | this.state = { 15 | open: null, 16 | }; 17 | } 18 | 19 | componentDidMount() { 20 | const port = this.props.session_store.settings['peer-port']; 21 | 22 | this.props.session_store.testPort(port).then((open) => { 23 | this.setState({ 24 | open: open, 25 | }); 26 | }); 27 | } 28 | 29 | renderPortStatus() { 30 | if (this.state.open === null) { 31 | return ( 32 | Status: Unknown 33 | ); 34 | } else if (this.state.open) { 35 | return ( 36 | Port is Open 37 | ); 38 | } else { 39 | return ( 40 | Port is Closed 41 | ); 42 | } 43 | } 44 | 45 | render() { 46 | return ( 47 |
48 |
 
49 |
50 | {this.renderPortStatus()} 51 |
52 |
53 | ); 54 | } 55 | } 56 | 57 | export default PortTestRow; 58 | -------------------------------------------------------------------------------- /src/components/Inspector/Files/services/generate-tree.js: -------------------------------------------------------------------------------- 1 | import { isArray, isObject } from 'lodash'; 2 | 3 | function buildEntryTree(entry, getEntryPath = (entry) => entry) { 4 | return getEntryPath(entry).split('/').reduceRight((partialTree, part) => { 5 | return { 6 | [part]: { 7 | ...entry, 8 | entries: partialTree, 9 | }, 10 | }; 11 | }, {}); 12 | } 13 | 14 | function mergeEntryTrees(tree = {}, otherTree = {}) { 15 | // Merge arrays 16 | if (isArray(tree) && isArray(otherTree)) { 17 | return [...new Set([...tree, ...otherTree])]; 18 | } 19 | 20 | // Merge primitive types (number, string, ...) 21 | if (!isObject(tree) && !isObject(otherTree)) { 22 | return tree; 23 | } 24 | 25 | const allKeys = [...new Set([...Object.keys(tree), ...Object.keys(otherTree)])]; 26 | 27 | return allKeys.reduce(function(partialTree, key) { 28 | if (tree[key] && !otherTree[key]) { 29 | partialTree[key] = tree[key]; 30 | } 31 | 32 | if (!tree[key] && otherTree[key]) { 33 | partialTree[key] = otherTree[key]; 34 | } 35 | 36 | if (tree[key] && otherTree[key]) { 37 | partialTree[key] = mergeEntryTrees(tree[key], otherTree[key]); 38 | } 39 | 40 | return partialTree; 41 | }, {}); 42 | } 43 | 44 | export function generateTree(entries) { 45 | return entries 46 | .map((entry, entryIndex) => buildEntryTree({...entry, priority: [entry.priority], fileIds: [entryIndex]}, ({ name }) => name)) 47 | .reduce(mergeEntryTrees, {}); 48 | } 49 | -------------------------------------------------------------------------------- /src/components/dialogs/OpenDialog/styles/index.css: -------------------------------------------------------------------------------- 1 | .body { 2 | display: flex; 3 | padding: 20px; 4 | background-color: #eee; 5 | width: 380px; 6 | font-size: 1.1em; 7 | } 8 | 9 | .body h2 { 10 | font-size: 1.2em; 11 | color: black; 12 | } 13 | 14 | .body label, 15 | .body input { 16 | display: block; 17 | } 18 | 19 | .body input.inline { 20 | display: inline-block; 21 | } 22 | 23 | .logo { 24 | flex-basis: 80px; 25 | flex-shrink: 0; 26 | } 27 | 28 | .logo img { 29 | width: 64px; 30 | height: 64px; 31 | } 32 | 33 | .form { 34 | display: flex; 35 | flex-direction: column; 36 | justify-content: space-between; 37 | flex: 1; 38 | } 39 | 40 | .form h2 { 41 | font-size: 1.2em; 42 | margin: 0; 43 | flex-basis: 40px; 44 | } 45 | 46 | .body section { 47 | display: flex; 48 | flex-direction: column; 49 | } 50 | 51 | .body fieldset { 52 | border: none; 53 | margin: 0 0 12px 0; 54 | padding: 0; 55 | flex-grow: 1; 56 | } 57 | 58 | .body fieldset label { 59 | margin-bottom: 3px; 60 | } 61 | 62 | .body fieldset input { 63 | box-sizing: border-box; 64 | width: 100%; 65 | } 66 | 67 | .body fieldset label.inlineCheck { 68 | display: flex; 69 | } 70 | 71 | .body fieldset label.inlineCheck input { 72 | width: auto; 73 | } 74 | 75 | .body fieldset label.inlineCheck div { 76 | margin-left: 6px; 77 | } 78 | 79 | .body section.buttons { 80 | flex-direction: row; 81 | justify-content: flex-end; 82 | } 83 | 84 | .body section.buttons button { 85 | margin-left: 10px; 86 | } 87 | -------------------------------------------------------------------------------- /src/components/Torrent/ProgressBar/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import CSSModules from 'react-css-modules'; 3 | 4 | import styles from './styles/index.css'; 5 | 6 | function getPercentage(torrent) { 7 | // TODO: Extract from session store if configure globally or grab torrent one 8 | const seedRatioLimit = torrent.seedRatioLimit; 9 | 10 | if (torrent.needsMetaData) { 11 | return torrent.metadataPercentComplete * 100; 12 | } 13 | 14 | if (!torrent.isDone) { 15 | return Math.round(torrent.percentDone * 100); 16 | } 17 | 18 | if (seedRatioLimit > 0 && torrent.isSeeding) { 19 | return Math.round(torrent.uploadRatio * 100 / seedRatioLimit); 20 | } 21 | 22 | return 100; 23 | } 24 | 25 | function getProgressStyles(torrent) { 26 | let barStyle = ''; 27 | 28 | if (torrent.isStopped) { 29 | barStyle = styles.paused; 30 | } else if (torrent.isDownloadingQueued) { 31 | barStyle = styles.leechingQueued; 32 | } else if (torrent.needsMetaData) { 33 | barStyle = styles.magnet; 34 | } else if (torrent.isDownloading) { 35 | barStyle = styles.leeching; 36 | } else if (torrent.isSeedingQueued) { 37 | barStyle = styles.seedingQueued; 38 | } else if (torrent.isSeeding) { 39 | barStyle = styles.seeding; 40 | } 41 | 42 | return `${styles.progressBar} ${barStyle}`; 43 | } 44 | 45 | function ProgressBar({ torrent }) { 46 | return ( 47 |
48 | 49 |
50 | ); 51 | } 52 | 53 | export default CSSModules(ProgressBar, styles); 54 | -------------------------------------------------------------------------------- /src/components/Torrent/StatusDetails/__tests__/index.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import createComponentWithIntl from 'test/util/createComponentWithIntl'; 3 | 4 | import StatusDetails from '../'; 5 | 6 | const torrent = { 7 | recheckProgress: 1, 8 | peersGettingFromUs: 5, 9 | peersSendingToUs: 5, 10 | peersConnected: 5, 11 | webseedsSendingToUs: 5, 12 | rateUpload: 1E5, 13 | rateDownload: 1E7, 14 | }; 15 | 16 | test('StatusDetails has errors', () => { 17 | const component = createComponentWithIntl( 18 | 19 | ); 20 | 21 | expect(component.toJSON()).toMatchSnapshot(); 22 | }); 23 | 24 | test('StatusDetails downloading', () => { 25 | const component = createComponentWithIntl( 26 | 27 | ); 28 | 29 | expect(component.toJSON()).toMatchSnapshot(); 30 | }); 31 | 32 | test('StatusDetails seeding', () => { 33 | const component = createComponentWithIntl( 34 | 35 | ); 36 | 37 | expect(component.toJSON()).toMatchSnapshot(); 38 | }); 39 | 40 | test('StatusDetails checking', () => { 41 | const component = createComponentWithIntl( 42 | 43 | ); 44 | 45 | expect(component.toJSON()).toMatchSnapshot(); 46 | }); 47 | 48 | test('StatusDetails status', () => { 49 | const component = createComponentWithIntl( 50 | 51 | ); 52 | 53 | expect(component.toJSON()).toMatchSnapshot(); 54 | }); 55 | -------------------------------------------------------------------------------- /src/components/Torrent/StatusDetails/Error.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FormattedMessage } from 'react-intl'; 3 | 4 | import Torrent from 'stores/torrent'; 5 | 6 | const { 7 | ERR_TRACKER_WARNING, 8 | ERR_TRACKER_ERROR, 9 | ERR_LOCAL_ERROR, 10 | } = Torrent; 11 | 12 | function TrackerWarning({ torrent }) { 13 | const { errorDescription } = torrent; 14 | 15 | return ( 16 | 21 | ); 22 | } 23 | 24 | function TrackerError({ torrent }) { 25 | const { errorDescription } = torrent; 26 | 27 | return ( 28 | 33 | ); 34 | } 35 | 36 | function LocalError({ torrent }) { 37 | const { errorDescription } = torrent; 38 | 39 | return ( 40 | 45 | ); 46 | } 47 | 48 | export default function({ torrent }) { 49 | switch (torrent.error) { 50 | case ERR_TRACKER_WARNING: 51 | return ; 52 | case ERR_TRACKER_ERROR: 53 | return ; 54 | case ERR_LOCAL_ERROR: 55 | return ; 56 | default: 57 | return null; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/components/Torrent/Full.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import CSSModules from 'react-css-modules'; 3 | import { inject, observer } from 'mobx-react'; 4 | import autobind from 'autobind-decorator'; 5 | 6 | import ProgressBar from './ProgressBar'; 7 | import StatusButton from './StatusButton'; 8 | import StatusDetails from './StatusDetails'; 9 | import ProgressDetails from './ProgressDetails'; 10 | 11 | import styles from './styles/index.css'; 12 | 13 | function getPeerDetailsStyles(torrent) { 14 | if (torrent.hasErrors) { 15 | return `peerDetailsError`; 16 | } 17 | 18 | return 'peerDetails'; 19 | } 20 | 21 | @inject('torrents_store') 22 | @observer 23 | @CSSModules(styles) 24 | class Full extends Component { 25 | @autobind onToggleTorrent(torrentId) { 26 | if (this.props.torrent.isStopped) { 27 | this.props.torrents_store.start(torrentId); 28 | return; 29 | } 30 | 31 | this.props.torrents_store.stop(torrentId); 32 | } 33 | 34 | render() { 35 | const { torrent } = this.props; 36 | 37 | return ( 38 |
39 |
40 | {torrent.name} 41 |
42 |
43 | 44 |
45 |
46 | 47 | 48 |
49 |
50 | 51 |
52 |
53 | ); 54 | } 55 | } 56 | 57 | export default Full; 58 | -------------------------------------------------------------------------------- /src/stores/torrent-upload.js: -------------------------------------------------------------------------------- 1 | import { action, observable } from 'mobx'; 2 | 3 | import { fileToBase64 } from 'util/converters'; 4 | 5 | class TorrentUpload { 6 | // TODO: No need for observable 7 | @observable files = []; 8 | @observable url; 9 | @observable downloadDir; 10 | @observable paused; 11 | 12 | @action setDownloadDir(dir) { 13 | this.downloadDir = dir; 14 | } 15 | 16 | @action setPaused(paused) { 17 | this.paused = paused; 18 | } 19 | 20 | @action setTorrentFiles(files) { 21 | // FileList is not an Array, use destructuring to convert it 22 | this.files.replace([...files]); 23 | } 24 | 25 | @action setTorrentUrl(url) { 26 | // Accept also torrent hash as url 27 | if (url.match(/^[0-9a-f]{40}$/i)) { 28 | this.url = `magnet:?xt=urn:btih:${url}`; 29 | return; 30 | } 31 | 32 | this.url = url; 33 | } 34 | 35 | serialize() { 36 | return new Promise((resolve, reject) => { 37 | Promise.all(this.files.map((file) => fileToBase64(file))) 38 | .then((encodedTorrents) => { 39 | const fileTorrents = encodedTorrents.map((encodedTorrent) => { 40 | return { 41 | metainfo: encodedTorrent, 42 | paused: this.paused, 43 | 'download-dir': this.downloadDir, 44 | }; 45 | }); 46 | 47 | const urlTorrent = { 48 | filename: this.url, 49 | paused: this.paused, 50 | 'download-dir': this.downloadDir, 51 | }; 52 | 53 | resolve([...fileTorrents, urlTorrent]); 54 | }) 55 | .catch(() => reject(new Error("Can't parse files"))); 56 | }); 57 | } 58 | } 59 | 60 | export default TorrentUpload; 61 | -------------------------------------------------------------------------------- /src/components/Torrent/ProgressDetails/Done.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; 3 | 4 | import { size as formatSize, ratioString } from 'util/formatters'; 5 | 6 | const messages = defineMessages({ 7 | uploadStats: { 8 | id: 'torrent.progress.done.uploadstats', 9 | defaultMessage: `uploaded {uploaded} (Ratio: {ratio})`, 10 | }, 11 | }); 12 | 13 | const PartialDone = injectIntl(({ torrent, intl }) => 14 | 27 | ); 28 | 29 | const DoneDone = injectIntl(({ torrent, intl }) => 30 | 41 | ); 42 | 43 | function ProgressDone({ torrent }) { 44 | if (torrent.totalSize === torrent.sizeWhenDone) { 45 | return ; 46 | } 47 | 48 | return ; 49 | } 50 | 51 | export default ProgressDone; 52 | -------------------------------------------------------------------------------- /src/components/Torrent/StatusDetails/Status.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FormattedMessage } from 'react-intl'; 3 | 4 | import Torrent from 'stores/torrent'; 5 | 6 | const { 7 | STATUS_STOPPED, 8 | STATUS_CHECK_WAIT, 9 | STATUS_CHECK, 10 | STATUS_DOWNLOAD_WAIT, 11 | STATUS_DOWNLOAD, 12 | STATUS_SEED_WAIT, 13 | STATUS_SEED, 14 | } = Torrent; 15 | 16 | export default function({ torrent }) { 17 | switch (torrent.status) { 18 | case STATUS_CHECK_WAIT: 19 | return ; 20 | case STATUS_CHECK: 21 | return ; 22 | case STATUS_DOWNLOAD_WAIT: 23 | return ; 24 | case STATUS_DOWNLOAD: 25 | return ; 26 | case STATUS_SEED_WAIT: 27 | return ; 28 | case STATUS_SEED: 29 | return ; 30 | case STATUS_STOPPED: 31 | return ( 32 | torrent.isFinished 33 | ? 34 | : 35 | ); 36 | case null: 37 | case undefined: 38 | return ; 39 | default: 40 | return ; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/translations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "activity.title": "Activity", 3 | "torrent.full.checking": "Verifying local data ({recheckProgress} tested)", 4 | "torrent.full.downloading.frompeersandwebseeds": "Downloading from {peersSendingToUs, number} of {peersConnected, number} {peersConnected, plural, one {peer} other {peers}} and {webseedsSendingToUs, number} {webseedsSendingToUs, plural, one {web seed} other {web seeds}} - {formattedRateDownload} {formattedRateUpload}", 5 | "torrent.full.downloading.fromwebseeds": "Downloading from {webseedsSendingToUs, number} {webseedsSendingToUs, plural, one {web seed} other {web seeds}} - {formattedRateDownload} {formattedRateUpload}", 6 | "torrent.full.downloading.frompeers": "Downloading from {peersSendingToUs, number} of {peersConnected, number} {peersConnected, plural, one {peer} other {peers}} - {formattedRateDownload} {formattedRateUpload}", 7 | "torrent.error.trackerwarning": "Tracker returned a warning: {errorDescription}", 8 | "torrent.error.trackererror": "Tracker returned an error: {errorDescription}", 9 | "torrent.error.localerror": "Error: {errorDescription}", 10 | "torrent.full.seeding": "Seeding to {peersGettingFromUs, number} of {peersConnected, number} connected {peersConnected, plural, one {peer} other {peers}} - {formattedRateUpload}", 11 | "torrent.status.checkwait": "Queued for verification", 12 | "torrent.status.check": "Verifying local data", 13 | "torrent.status.downloadwait": "Queued for download", 14 | "torrent.status.download": "Downloading", 15 | "torrent.status.seedwait": "Queued for seeding", 16 | "torrent.status.seed": "Seeding", 17 | "torrent.status.finished": "Finished", 18 | "torrent.status.paused": "Paused", 19 | "torrent.status.unknown": "Unknown", 20 | "torrent.status.error": "Error" 21 | } 22 | -------------------------------------------------------------------------------- /src/components/DropzoneLayer/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component} from 'react'; 2 | import Dropzone from 'react-dropzone'; 3 | import CSSModules from 'react-css-modules'; 4 | import { inject, observer } from 'mobx-react'; 5 | import autobind from 'autobind-decorator'; 6 | 7 | import logoImage from 'images/logo.png'; 8 | 9 | import TorrentUpload from 'stores/torrent-upload'; 10 | 11 | import styles from './styles/index.css'; 12 | 13 | @inject('torrents_store', 'session_store') 14 | @observer 15 | @CSSModules(styles) 16 | class DropzoneLayer extends Component { 17 | constructor(props) { 18 | super(props); 19 | 20 | this.torrentUpload = new TorrentUpload(); 21 | this.torrentUpload.setDownloadDir(this.props.session_store.settings['download-dir']); 22 | } 23 | 24 | @autobind onDrop(acceptedFiles, rejectedFiles) { 25 | this.torrentUpload.setTorrentFiles(acceptedFiles); 26 | this.torrentUpload.serialize().then((torrents) => { 27 | torrents.forEach((torrentData) => this.props.torrents_store.add(torrentData)); 28 | }); 29 | } 30 | 31 | render() { 32 | return ( 33 | 40 |
41 |
42 | logo 43 |
Try dropping some torrent files here.
44 |
45 |
46 |
47 | {this.props.children} 48 |
49 |
50 | ); 51 | } 52 | }; 53 | 54 | export default DropzoneLayer; 55 | -------------------------------------------------------------------------------- /src/components/Torrent/ProgressBar/styles/index.css: -------------------------------------------------------------------------------- 1 | .progressBar {} 2 | 3 | .progressBar progress { 4 | -webkit-appearance: none; 5 | appearance: none; 6 | width: 100%; 7 | height: 12px; 8 | } 9 | 10 | .leeching progress::-webkit-progress-bar { 11 | box-shadow: inset 0px 0px 0px 1px #CFCFCF; 12 | background-image: linear-gradient(to bottom, #efefef 0%,#e7e7e7 50%,#d1d1d1 51%,#ebebeb 100%); 13 | } 14 | 15 | .leeching progress::-webkit-progress-value { 16 | border: 1px solid #3D9DEA; 17 | background-image: linear-gradient(to bottom, #42a9fb 0%,#40a3f3 50%,#3a93db 51%,#41a6f6 100%); 18 | } 19 | 20 | .paused progress::-webkit-progress-bar { 21 | box-shadow: inset 0px 0px 0px 1px #989898; 22 | background-image: linear-gradient(to bottom, #efefef 0%,#e7e7e7 50%,#d1d1d1 51%,#ebebeb 100%); 23 | } 24 | 25 | .paused progress::-webkit-progress-value { 26 | border: 1px solid #989898; 27 | background-image: linear-gradient(to bottom, #b0b0b0 0%,#aaaaaa 50%,#9a9a9a 51%,#adadad 100%); 28 | } 29 | 30 | .seeding progress::-webkit-progress-bar { 31 | box-shadow: inset 0px 0px 0px 1px #29AD35; 32 | background-image: linear-gradient(to bottom, #47e75f 0%,#44de5b 50%,#3ec953 51%,#45e25d 100%); 33 | } 34 | 35 | .seeding progress::-webkit-progress-value { 36 | border: 1px solid #29AD35; 37 | background-image: linear-gradient(to bottom, #3cc551 0%,#3abd4d 50%,#35ab47 51%,#3bc04f 100%); 38 | } 39 | 40 | .magnet progress::-webkit-progress-bar { 41 | box-shadow: inset 0px 0px 0px 1px #D47778; 42 | background-image: linear-gradient(to bottom, #ea8584 0%,#e38080 50%,#d37676 51%,#e78282 100%); 43 | } 44 | 45 | .magnet progress::-webkit-progress-value { 46 | border: 1px solid #D47778; 47 | background-image: linear-gradient(to bottom, #efefef 0%,#e7e7e7 50%,#d1d1d1 51%,#ebebeb 100%); 48 | } 49 | 50 | /* .seedingQueued */ 51 | /* .leechingQueued */ 52 | -------------------------------------------------------------------------------- /src/components/dialogs/PreferencesDialog/PeersTabPanel/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component} from 'react'; 2 | import { inject, observer } from 'mobx-react'; 3 | 4 | import TextRow from '../fields/TextRow'; 5 | import CheckRow from '../fields/CheckRow'; 6 | import CheckValueRow from '../fields/CheckValueRow'; 7 | import SelectRow from '../fields/SelectRow'; 8 | 9 | @inject('view_store') 10 | @observer 11 | class PeersTabPanel extends Component { 12 | render() { 13 | const encryption = { 14 | tolerated: 'Allow encryption', 15 | preferred: 'Prefer encryption', 16 | required: 'Require encryption', 17 | }; 18 | 19 | return ( 20 |
21 |

Connections

22 | 23 | 24 | 25 |

Options

26 | 27 | 28 | 29 | 30 | 31 | 32 |

Blocklist

33 | 34 | 35 |
36 |
Blocklist has ? rules
37 |
38 |
39 |
40 | ); 41 | } 42 | } 43 | 44 | export default PeersTabPanel; 45 | -------------------------------------------------------------------------------- /config/paths.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var fs = require('fs'); 3 | 4 | // Make sure any symlinks in the project folder are resolved: 5 | // https://github.com/facebookincubator/create-react-app/issues/637 6 | var appDirectory = fs.realpathSync(process.cwd()); 7 | function resolveApp(relativePath) { 8 | return path.resolve(appDirectory, relativePath); 9 | } 10 | 11 | // We support resolving modules according to `NODE_PATH`. 12 | // This lets you use absolute paths in imports inside large monorepos: 13 | // https://github.com/facebookincubator/create-react-app/issues/253. 14 | 15 | // It works similar to `NODE_PATH` in Node itself: 16 | // https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders 17 | 18 | // We will export `nodePaths` as an array of absolute paths. 19 | // It will then be used by Webpack configs. 20 | // Jest doesn’t need this because it already handles `NODE_PATH` out of the box. 21 | 22 | // Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored. 23 | // Otherwise, we risk importing Node.js core modules into an app instead of Webpack shims. 24 | // https://github.com/facebookincubator/create-react-app/issues/1023#issuecomment-265344421 25 | 26 | var nodePaths = (process.env.NODE_PATH || '') 27 | .split(process.platform === 'win32' ? ';' : ':') 28 | .filter(Boolean) 29 | .filter(folder => !path.isAbsolute(folder)) 30 | .map(resolveApp); 31 | 32 | // config after eject: we're in ./config/ 33 | module.exports = { 34 | appBuild: resolveApp('build'), 35 | appPublic: resolveApp('public'), 36 | appHtml: resolveApp('public/index.html'), 37 | appIndexJs: resolveApp('src/index.js'), 38 | appPackageJson: resolveApp('package.json'), 39 | appSrc: resolveApp('src'), 40 | yarnLockFile: resolveApp('yarn.lock'), 41 | testsSetup: resolveApp('src/setupTests.js'), 42 | appNodeModules: resolveApp('node_modules'), 43 | ownNodeModules: resolveApp('node_modules'), 44 | nodePaths: nodePaths 45 | }; 46 | -------------------------------------------------------------------------------- /src/components/dialogs/PromptDialog/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component} from 'react'; 2 | import CSSModules from 'react-css-modules'; 3 | import autobind from 'autobind-decorator'; 4 | 5 | import Dialog from '../Dialog'; 6 | import logoImage from 'images/logo.png'; 7 | 8 | import styles from './styles/index.css'; 9 | 10 | @CSSModules(styles) 11 | class PromptDialog extends Component { 12 | constructor(props) { 13 | super(props); 14 | 15 | this.state = {}; 16 | } 17 | 18 | componentWillReceiveProps(nextProps) { 19 | if (nextProps.toggle !== this.props.toggle) { 20 | this.setState({value: nextProps.placeholder}); 21 | } 22 | } 23 | 24 | @autobind onChange(event) { 25 | this.setState({ 26 | value: event.target.value, 27 | }); 28 | } 29 | 30 | @autobind onSubmit() { 31 | this.props.onSubmit(this.state.value); 32 | this.onHide(); 33 | } 34 | 35 | @autobind onDismiss(event) { 36 | event.preventDefault(); 37 | this.onHide(); 38 | } 39 | 40 | @autobind onHide() { 41 | this.props.onToggle(); 42 | } 43 | 44 | render() { 45 | return ( 46 | 51 |
52 |
53 | logo 54 |
55 |
56 | { this.props.question && 57 |

{this.props.question}:

58 | } 59 | 60 | 61 |
62 | 63 | 64 |
65 |
66 |
67 |
68 | ); 69 | } 70 | } 71 | 72 | export default PromptDialog; 73 | -------------------------------------------------------------------------------- /src/components/dialogs/PreferencesDialog/SpeedTabPanel/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component} from 'react'; 2 | import { inject, observer } from 'mobx-react'; 3 | import { range } from 'lodash'; 4 | 5 | import TextRow from '../fields/TextRow'; 6 | import CheckRow from '../fields/CheckRow'; 7 | import CheckValueRow from '../fields/CheckValueRow'; 8 | import SelectRow from '../fields/SelectRow'; 9 | 10 | @inject('view_store') 11 | @observer 12 | class SpeedTabPanel extends Component { 13 | render() { 14 | const times = range(0, 1440, 15).reduce((memo, item) => { 15 | const hours = Math.round(item / 60); 16 | const minutes = item % 60; 17 | 18 | memo[item] = `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}`; 19 | return memo; 20 | }, {}); 21 | 22 | const days = { 23 | '127': 'Everyday', 24 | '62': 'Weekdays', 25 | '65': 'Weekends', 26 | '1': 'Sunday', 27 | '2': 'Monday', 28 | '4': 'Tuesday', 29 | '8': 'Wednesday', 30 | '16': 'Thursday', 31 | '32': 'Friday', 32 | '64': 'Saturday', 33 | }; 34 | 35 | return ( 36 |
37 |

Speed Limits

38 | 39 | 40 | 41 |

Alternative Speed Limits

42 |

Override normal speed limits manually or at scheduled times

43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 |
52 | ); 53 | } 54 | } 55 | 56 | export default SpeedTabPanel; 57 | -------------------------------------------------------------------------------- /src/components/Torrent/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { findDOMNode } from 'react-dom'; 3 | import { inject, observer } from 'mobx-react'; 4 | import autobind from 'autobind-decorator'; 5 | 6 | import TorrentContextMenu from 'components/menus/TorrentContextMenu'; 7 | 8 | import Compact from './Compact'; 9 | import Full from './Full'; 10 | 11 | @inject('view_store') 12 | @observer 13 | class Torrent extends Component { 14 | constructor(props) { 15 | super(props); 16 | 17 | this.state = { 18 | position: { 19 | left: 0, 20 | top: 0, 21 | }, 22 | }; 23 | } 24 | 25 | @autobind toggleContextMenu(position) { 26 | if (!this.isContextMenuShown()) { 27 | this.props.view_store.toggleTorrentContextMenu(this.props.torrent.id); 28 | } 29 | 30 | this.setState({position}); 31 | } 32 | 33 | @autobind onContextMenu(event) { 34 | const { clientX, clientY } = event; 35 | 36 | event.preventDefault(); 37 | 38 | this.toggleContextMenu({left: clientX, top: clientY}); 39 | } 40 | 41 | @autobind isContextMenuShown() { 42 | return this.props.view_store.torrentContextMenuShown === this.props.torrent.id; 43 | } 44 | 45 | @autobind renderContextMenu() { 46 | const { top, left } = this.state.position; 47 | 48 | // TODO: Proper handling position depending on component bounds (left, top) 49 | 50 | return ( 51 |
52 | findDOMNode(this.refs.target)} 57 | onHide={() => this.props.view_store.toggleTorrentContextMenu()} 58 | /> 59 |
60 | ); 61 | } 62 | 63 | render() { 64 | const {torrent, view_store} = this.props; 65 | const View = view_store.compact ? Compact : Full; 66 | 67 | return ( 68 |
69 | 70 | {this.renderContextMenu()} 71 |
72 | ); 73 | } 74 | } 75 | 76 | export default Torrent; 77 | -------------------------------------------------------------------------------- /src/components/toolbars/StatusToolbar/styles/index.css: -------------------------------------------------------------------------------- 1 | .toolbar { 2 | background: #ccc; 3 | padding: 2px; 4 | 5 | display: -ms-flexbox; 6 | display: -webkit-flex; 7 | display: flex; 8 | 9 | -ms-flex-align: center; 10 | -webkit-align-items: center; 11 | -webkit-box-align: center; 12 | 13 | align-items: center; 14 | 15 | border-bottom: 1px solid #AAA; 16 | background-color: #cccccc; 17 | background-image: -webkit-gradient(linear, left top, left bottom, from(#dddddd), to(#bbbbbb)); 18 | background-image: -webkit-linear-gradient(top, #dddddd, #bbbbbb); 19 | background-image: -moz-linear-gradient(top, #dddddd, #bbbbbb); 20 | background-image: -ms-linear-gradient(top, #dddddd, #bbbbbb); 21 | background-image: -o-linear-gradient(top, #dddddd, #bbbbbb); 22 | background-image: linear-gradient(top, #dddddd, #bbbbbb); 23 | 24 | border-top: 1px solid #666; 25 | } 26 | 27 | .button { 28 | border: 1px solid #888; 29 | border-radius: 5px; 30 | background: transparent; 31 | padding: 2px 10px; 32 | outline: none; 33 | margin-right: 5px; 34 | height: 20px; 35 | width: 40px; 36 | 37 | background-color: #dddddd; 38 | background-image: -webkit-gradient(linear, left top, left bottom, from(#ffffff), to(#bbbbbb)); 39 | background-image: -webkit-linear-gradient(top, #ffffff, #bbbbbb); 40 | background-image: -moz-linear-gradient(top, #ffffff, #bbbbbb); 41 | background-image: -ms-linear-gradient(top, #ffffff, #bbbbbb); 42 | background-image: -o-linear-gradient(top, #ffffff, #bbbbbb); 43 | background-image: linear-gradient(top, #ffffff, #bbbbbb); 44 | } 45 | 46 | .button:hover { 47 | cursor: pointer; 48 | background: #ddd; 49 | } 50 | 51 | .buttonActive { 52 | background-image: -webkit-linear-gradient(top, #cdcdff, white); 53 | } 54 | 55 | .buttonTurtle { 56 | composes: button; 57 | 58 | background-image: url(images/turtle.png); 59 | background-position: center; 60 | background-repeat: no-repeat; 61 | } 62 | 63 | .turtleActive { 64 | background-image: url(images/blue-turtle.png), -webkit-linear-gradient(top, #cdcdff, white); 65 | background-position: center; 66 | background-repeat: no-repeat; 67 | } 68 | 69 | .filter { 70 | border-radius: 5px; 71 | } 72 | -------------------------------------------------------------------------------- /src/components/menus/SortByContextMenu/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import CSSModules from 'react-css-modules'; 3 | import { inject } from 'mobx-react'; 4 | import autobind from 'autobind-decorator'; 5 | 6 | import ContextMenu from 'components/menus/ContextMenu'; 7 | 8 | import styles from './styles/index.css'; 9 | 10 | @inject('view_store', 'torrents_store') 11 | @CSSModules(styles) 12 | class SortByContextMenu extends Component { 13 | @autobind onToggleSortByContextMenu() { 14 | this.props.view_store.toggleSortByContextMenu(); 15 | } 16 | 17 | @autobind onToggleContextMenu() { 18 | // TODO: Move it to ContextMenu component 19 | this.props.view_store.toggleContextMenus(); 20 | } 21 | 22 | @autobind onSetSortCriteria(sortCriteria) { 23 | this.props.torrents_store.setSortCriteria(sortCriteria); 24 | } 25 | 26 | render() { 27 | const { sortCriteria, sortDirection } = this.props.torrents_store; 28 | const criteriaList = { 29 | queue_order: 'Queue Order', 30 | activity: 'Activity', 31 | age: 'Age', 32 | name: 'Name', 33 | percent_completed: 'Progress', 34 | ratio: 'Ratio', 35 | size: 'Size', 36 | state: 'State', 37 | }; 38 | 39 | return ( 40 | 46 |
    52 | {Object.keys(criteriaList).map((key) => ( 53 |
  • this.onSetSortCriteria(key)}>{criteriaList[key]}
  • 54 | ))} 55 | 56 |
  • 57 |
  • Reverse Sort Order
  • 58 |
59 |
60 | ); 61 | } 62 | } 63 | 64 | export default SortByContextMenu; 65 | -------------------------------------------------------------------------------- /src/components/Inspector/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component} from 'react'; 2 | import { Tab, Tabs, TabList, TabPanel } from 'react-tabs'; 3 | import CSSModules from 'react-css-modules'; 4 | import { inject, observer } from 'mobx-react'; 5 | 6 | import TorrentStats from 'stores/torrent-stats'; 7 | 8 | import infoImage from 'images/inspector-info.png'; 9 | import peersImage from 'images/inspector-peers.png'; 10 | import trackersImage from 'images/inspector-trackers.png'; 11 | import filesImage from 'images/inspector-files.png'; 12 | 13 | import Activity from './Activity'; 14 | import Details from './Details'; 15 | import Peers from './Peers'; 16 | import Trackers from './Trackers'; 17 | import Files from './Files'; 18 | 19 | import styles from './styles/index.css'; 20 | 21 | @inject('view_store', 'torrents_store') 22 | @observer 23 | @CSSModules(styles) 24 | class Inspector extends Component { 25 | render() { 26 | const selectedTorrentIds = this.props.view_store.selectedTorrents; 27 | const torrents = this.props.torrents_store.getByIds(selectedTorrentIds); 28 | 29 | const info = new TorrentStats(torrents); 30 | 31 | return ( 32 |
33 | 34 | 35 | Info 36 | Peers 37 | Trackers 38 | Files 39 | 40 | 41 |

{info.title}

42 | 43 |
44 | 45 | 46 |

{info.title}

47 | {info.peers.length > 0 && } 48 |
49 | 50 |

{info.title}

51 | {info.trackers.length > 0 && } 52 |
53 | 54 |

{info.title}

55 | {info.files.length > 0 && } 56 |
57 | 58 |
59 | ); 60 | } 61 | } 62 | 63 | export default Inspector; 64 | -------------------------------------------------------------------------------- /src/stores/session-store.js: -------------------------------------------------------------------------------- 1 | import { observable, action } from 'mobx'; 2 | 3 | class SessionStore { 4 | @observable sessionId = null; 5 | @observable settings = {}; 6 | @observable freeSpace = -1; // TODO: Decide if this should be in TorrentUpload 7 | 8 | constructor(rpc) { 9 | this.rpc = rpc; 10 | } 11 | 12 | @action getSession() { 13 | return this.rpc.sendRequest('session-get') 14 | .then(action((response) => { 15 | response.json().then(action((result) => { 16 | this.settings = result.arguments; 17 | })); 18 | })); 19 | } 20 | 21 | @action testPort(port) { 22 | return this.rpc.sendRequest('port-test') 23 | .then(action((response) => { 24 | return response.json().then(action((result) => { 25 | return result.arguments['port-is-open']; 26 | })); 27 | })); 28 | } 29 | 30 | @action getFreeSpace(downloadDir) { 31 | const data = { 32 | path: downloadDir, 33 | }; 34 | 35 | return this.rpc.sendRequest('free-space', data) 36 | .then(action((response) => { 37 | response.json().then(action((result) => { 38 | this.freeSpace = result.arguments['size-bytes']; 39 | })); 40 | })); 41 | } 42 | 43 | @action setRateLimit(direction, value) { 44 | let data; 45 | 46 | if (value > 0) { 47 | data = { 48 | [`speed-limit-${direction}`]: value, 49 | [`speed-limit-${direction}-enabled`]: true, 50 | }; 51 | } else { 52 | data = { 53 | [`speed-limit-${direction}-enabled`]: false, 54 | }; 55 | } 56 | 57 | this.settings = { 58 | ...this.settings, 59 | ...data, 60 | }; 61 | 62 | return this.rpc.sendRequest('session-set', data).then(action((response) => { 63 | response.json().then(action((result) => {})); 64 | })); 65 | } 66 | 67 | @action setPreference(id, value) { 68 | const data = { 69 | [id]: value, 70 | }; 71 | 72 | this.settings = { 73 | ...this.settings, 74 | ...data, 75 | }; 76 | 77 | return this.rpc.sendRequest('session-set', data).then(action((response) => { 78 | response.json().then(action((result) => {})); 79 | })); 80 | } 81 | 82 | @action togglePreference(id) { 83 | return this.setPreference(id, !this.settings[id]); 84 | } 85 | } 86 | 87 | export default SessionStore; 88 | -------------------------------------------------------------------------------- /src/components/Inspector/Trackers/services/tracker-stats.js: -------------------------------------------------------------------------------- 1 | import { timestamp, countString, timeInterval } from 'util/formatters'; 2 | 3 | import Tracker from 'stores/tracker'; 4 | 5 | export function lastAnnounceStatus(tracker) { 6 | let lastAnnounceLabel = 'Last Announce'; 7 | let lastAnnounce = ['N/A']; 8 | let lastAnnounceTime; 9 | 10 | if (tracker.hasAnnounced) { 11 | lastAnnounceTime = timestamp(tracker.lastAnnounceTime); 12 | 13 | if (tracker.lastAnnounceSucceeded) { 14 | lastAnnounce = [lastAnnounceTime, ' (got ', countString('peer', 'peers', tracker.lastAnnouncePeerCount), ')']; 15 | } else { 16 | lastAnnounceLabel = 'Announce error'; 17 | lastAnnounce = [(tracker.lastAnnounceResult ? (tracker.lastAnnounceResult + ' - ') : ''), lastAnnounceTime]; 18 | } 19 | } 20 | 21 | return { 22 | label: lastAnnounceLabel, 23 | value: lastAnnounce.join(''), 24 | }; 25 | } 26 | 27 | export function getAnnounceState(tracker) { 28 | let timeUntilAnnounce; 29 | let s = ''; 30 | 31 | switch (tracker.announceState) { 32 | case Tracker.STATUS_ACTIVE: 33 | s = 'Announce in progress'; 34 | break; 35 | case Tracker.STATUS_WAITING: 36 | timeUntilAnnounce = tracker.nextAnnounceTime - ((new Date()).getTime() / 1000); 37 | if (timeUntilAnnounce < 0) { 38 | timeUntilAnnounce = 0; 39 | } 40 | s = 'Next announce in ' + timeInterval(timeUntilAnnounce); 41 | break; 42 | case Tracker.STATUS_QUEUED: 43 | s = 'Announce is queued'; 44 | break; 45 | case Tracker.STATUS_INACTIVE: 46 | s = tracker.isBackup 47 | ? 'Tracker will be used as a backup' 48 | : 'Announce not scheduled'; 49 | break; 50 | default: 51 | s = 'unknown announce state: ' + tracker.announceState; 52 | } 53 | return s; 54 | } 55 | 56 | export function lastScrapeStatus(tracker) { 57 | let lastScrapeLabel = 'Last Scrape'; 58 | let lastScrape = 'N/A'; 59 | let lastScrapeTime; 60 | 61 | if (tracker.hasScraped) { 62 | lastScrapeTime = timestamp(tracker.lastScrapeTime); 63 | if (tracker.lastScrapeSucceeded) { 64 | lastScrape = lastScrapeTime; 65 | } else { 66 | lastScrapeLabel = 'Scrape error'; 67 | lastScrape = (tracker.lastScrapeResult ? tracker.lastScrapeResult + ' - ' : '') + lastScrapeTime; 68 | } 69 | } 70 | 71 | return { 72 | label: lastScrapeLabel, 73 | value: lastScrape, 74 | }; 75 | } 76 | -------------------------------------------------------------------------------- /src/stores/torrent.js: -------------------------------------------------------------------------------- 1 | import { computed, extendObservable } from 'mobx'; 2 | import { zip } from 'lodash'; 3 | 4 | class Torrent { 5 | static STATUS_STOPPED = 0; 6 | static STATUS_CHECK_WAIT = 1; 7 | static STATUS_CHECK = 2; 8 | static STATUS_DOWNLOAD_WAIT = 3; 9 | static STATUS_DOWNLOAD = 4; 10 | static STATUS_SEED_WAIT = 5; 11 | static STATUS_SEED = 6; 12 | 13 | static ERR_NONE = 0; 14 | static ERR_TRACKER_WARNING = 1; 15 | static ERR_TRACKER_ERROR = 2; 16 | static ERR_LOCAL_ERROR = 3; 17 | 18 | constructor(torrent) { 19 | // TODO: Maybe filter torrent attributes (to not make everything observable) 20 | extendObservable(this, torrent); 21 | } 22 | 23 | update(torrent) { 24 | extendObservable(this, torrent); 25 | } 26 | 27 | // TODO: Find a better name for this (displayName, compareName, etc.) 28 | @computed get publicName() { 29 | if (!this.collatedName && this.name) { 30 | return this.name; 31 | } 32 | 33 | return this.collatedName || ''; 34 | } 35 | 36 | @computed get hasErrors() { 37 | return this.error > Torrent.ERR_NONE; 38 | } 39 | 40 | @computed get errorDescription() { 41 | return this.errorString; 42 | } 43 | 44 | @computed get isSeeding() { 45 | return this.status === Torrent.STATUS_SEED; 46 | } 47 | 48 | @computed get isStopped() { 49 | return this.status === Torrent.STATUS_STOPPED; 50 | } 51 | 52 | @computed get isChecking() { 53 | return this.status === Torrent.STATUS_CHECK; 54 | } 55 | 56 | @computed get isDownloading() { 57 | return this.status === Torrent.STATUS_DOWNLOAD; 58 | } 59 | 60 | @computed get isSeedingQueued() { 61 | return this.status === Torrent.STATUS_SEED_WAIT; 62 | } 63 | 64 | @computed get isDownloadingQueued() { 65 | return this.status === Torrent.STATUS_DOWNLOAD_WAIT; 66 | } 67 | 68 | @computed get isQueued() { 69 | return this.isDownloadingQueued || this.isSeedingQueued; 70 | } 71 | 72 | @computed get isDone() { 73 | return this.leftUntilDone < 1; 74 | } 75 | 76 | @computed get needsMetaData() { 77 | return this.metadataPercentComplete < 1; 78 | } 79 | 80 | @computed get have() { 81 | return this.haveValid + this.haveUnchecked; 82 | } 83 | 84 | @computed get filesAndStats() { 85 | return zip(this.files, this.fileStats).map(([file, fileStat]) => ({ 86 | ...file, 87 | ...fileStat, 88 | })); 89 | } 90 | } 91 | 92 | export default Torrent; 93 | -------------------------------------------------------------------------------- /src/components/Inspector/Peers/PeerGroup.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import CSSModules from 'react-css-modules'; 3 | 4 | import { speedBps } from 'util/formatters'; 5 | 6 | import lockIconImage from 'images/lock_icon.png'; 7 | 8 | import styles from './styles/index.css'; 9 | 10 | const flagMap = { 11 | 'O': 'Optimistic unchoke', 12 | 'D': 'Downloading from this peer', 13 | 'd': "We would download from this peer if they'd let us", 14 | 'U': 'Uploading to peer', 15 | 'u': "We would upload to this peer if they'd ask", 16 | 'K': "Peer has unchoked us, but we're not interested", 17 | '?': "We unchoked this peer, but they're not interested", 18 | 'E': 'Encrypted Connection', 19 | 'H': 'Peer was discovered through Distributed Hash Table (DHT)', 20 | 'X': 'Peer was discovered through Peer Exchange (PEX)', 21 | 'I': 'Peer is an incoming connection', 22 | 'T': 'Peer is connected via uTP', 23 | }; 24 | 25 | function formatPeerFlag(flag) { 26 | const flagExplanation = flagMap[flag]; 27 | 28 | if (!flagExplanation) { 29 | return String(flag); 30 | } 31 | 32 | return `${flag}: ${flagExplanation}`; 33 | } 34 | 35 | function PeerGroup({ peers }) { 36 | return ( 37 |
38 | {peers.length > 0 && 39 | 40 | 41 | 42 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | {peers.map((peer, index) => ( 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | ))} 63 | 64 |
43 | UpDown%StatusAddressClient
{peer.isEncrypted && Encrypted Connection}{peer.isUploadingTo && speedBps(peer.rateToPeer)}{peer.isDownloadingFrom && speedBps(peer.rateToClient)}{`${Math.floor(peer.progress * 100)}%`}{[...peer.flagStr].map((flag) => {flag})}{peer.address}{peer.clientName}
65 | } 66 |
67 | ); 68 | } 69 | 70 | export default CSSModules(styles)(PeerGroup); 71 | -------------------------------------------------------------------------------- /src/components/Torrent/StatusDetails/__tests__/Status.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import createComponentWithIntl from 'test/util/createComponentWithIntl'; 3 | 4 | import Status from '../Status'; 5 | 6 | import Torrent from 'stores/torrent'; 7 | 8 | const { 9 | STATUS_STOPPED, 10 | STATUS_CHECK_WAIT, 11 | STATUS_CHECK, 12 | STATUS_DOWNLOAD_WAIT, 13 | STATUS_DOWNLOAD, 14 | STATUS_SEED_WAIT, 15 | STATUS_SEED, 16 | } = Torrent; 17 | 18 | test('Status check wait', () => { 19 | const component = createComponentWithIntl( 20 | 21 | ); 22 | 23 | expect(component.toJSON()).toMatchSnapshot(); 24 | }); 25 | 26 | test('Status check', () => { 27 | const component = createComponentWithIntl( 28 | 29 | ); 30 | 31 | expect(component.toJSON()).toMatchSnapshot(); 32 | }); 33 | 34 | test('Status download wait', () => { 35 | const component = createComponentWithIntl( 36 | 37 | ); 38 | 39 | expect(component.toJSON()).toMatchSnapshot(); 40 | }); 41 | 42 | test('Status download', () => { 43 | const component = createComponentWithIntl( 44 | 45 | ); 46 | 47 | expect(component.toJSON()).toMatchSnapshot(); 48 | }); 49 | 50 | test('Status seed wait', () => { 51 | const component = createComponentWithIntl( 52 | 53 | ); 54 | 55 | expect(component.toJSON()).toMatchSnapshot(); 56 | }); 57 | 58 | test('Status seed', () => { 59 | const component = createComponentWithIntl( 60 | 61 | ); 62 | 63 | expect(component.toJSON()).toMatchSnapshot(); 64 | }); 65 | 66 | test('Status stopped finished', () => { 67 | const component = createComponentWithIntl( 68 | 69 | ); 70 | 71 | expect(component.toJSON()).toMatchSnapshot(); 72 | }); 73 | 74 | test('Status stopped not finished', () => { 75 | const component = createComponentWithIntl( 76 | 77 | ); 78 | 79 | expect(component.toJSON()).toMatchSnapshot(); 80 | }); 81 | 82 | test('Status unknown', () => { 83 | const component = createComponentWithIntl( 84 | 85 | ); 86 | 87 | expect(component.toJSON()).toMatchSnapshot(); 88 | }); 89 | 90 | test('Status other', () => { 91 | const component = createComponentWithIntl( 92 | 93 | ); 94 | 95 | expect(component.toJSON()).toMatchSnapshot(); 96 | }); 97 | -------------------------------------------------------------------------------- /src/components/Torrent/services/progress-details.js: -------------------------------------------------------------------------------- 1 | import { 2 | size, 3 | ratioString, 4 | percentString, 5 | timeInterval, 6 | } from 'util/formatters'; 7 | 8 | /* 9 | function formatProgressDone() { 10 | 11 | } 12 | 13 | function formatProgressRemaining() { 14 | 15 | } 16 | 17 | function formatProgressETA() { 18 | 19 | } 20 | */ 21 | 22 | function formatProgressMagnet(torrent) { 23 | let metaDataStatus = 'retrieving'; 24 | 25 | if (torrent.isStopped) { 26 | metaDataStatus = 'needs'; 27 | } 28 | 29 | const percent = 100 * torrent.metadataPercentComplete; 30 | 31 | return [ 32 | 'Magnetized transfer - ' + metaDataStatus + ' metadata (', 33 | percentString(percent), 34 | '%)', 35 | ].join(''); 36 | } 37 | 38 | export default function getProgressDetails(torrent) { 39 | if (torrent.needsMetaData) { 40 | return formatProgressMagnet(torrent); 41 | } 42 | 43 | const sizeWhenDone = torrent.sizeWhenDone; 44 | const totalSize = torrent.totalSize; 45 | const isDone = torrent.isDone || torrent.isSeeding; 46 | // TODO: Use either global or torrent one 47 | const seedRatioLimit = torrent.seedRatioLimit; 48 | 49 | let c; 50 | 51 | if (isDone) { 52 | // seed: '698.05 MiB' 53 | if (totalSize === sizeWhenDone) { 54 | c = [ 55 | size(totalSize), 56 | ]; 57 | // partial seed: '127.21 MiB of 698.05 MiB (18.2%)' 58 | } else { 59 | c = [ 60 | size(sizeWhenDone), 61 | ' of ', 62 | size(totalSize), 63 | ' (', 64 | percentString(100 * torrent.percentDone), 65 | '%)', 66 | ]; 67 | } 68 | 69 | // append UL stats: ', uploaded 8.59 GiB (Ratio: 12.3)' 70 | c.push( 71 | ', uploaded ', 72 | size(torrent.uploadedEver), 73 | ' (Ratio ', 74 | ratioString(torrent.uploadRatio), 75 | ')' 76 | ); 77 | // not done yet 78 | } else { 79 | c = [ 80 | size(sizeWhenDone - torrent.leftUntilDone), 81 | ' of ', 82 | size(sizeWhenDone), 83 | ' (', 84 | percentString(100 * torrent.percentDone), 85 | '%)', 86 | ]; 87 | } 88 | 89 | // maybe append ETA 90 | if (!torrent.isStopped && (!isDone || seedRatioLimit > 0)) { 91 | c.push(' - '); 92 | 93 | const eta = torrent.eta; 94 | // Magic number 95 | const MAX_ETA = 999 * 60 * 60; 96 | 97 | if (eta < 0 || eta >= MAX_ETA) { 98 | c.push('remaining time unknown'); 99 | } else { 100 | c.push( 101 | timeInterval(eta), 102 | ' remaining' 103 | ); 104 | } 105 | } 106 | 107 | return c.join(''); 108 | } 109 | -------------------------------------------------------------------------------- /src/components/Torrent/StatusDetails/Downloading.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FormattedMessage } from 'react-intl'; 3 | 4 | import { formatUL, formatDL } from 'util/formatters'; 5 | 6 | function DownloadingFromPeersAndWebSeeds({ torrent }) { 7 | const { peersSendingToUs, peersConnected, webseedsSendingToUs, rateUpload, rateDownload } = torrent; 8 | 9 | return ( 10 | 21 | ); 22 | } 23 | 24 | function DownloadingFromWebSeeds({ torrent }) { 25 | const { webseedsSendingToUs, rateUpload, rateDownload } = torrent; 26 | 27 | return ( 28 | 37 | ); 38 | } 39 | 40 | function DownloadingFromPeers({ torrent }) { 41 | const { peersSendingToUs, peersConnected, rateUpload, rateDownload } = torrent; 42 | 43 | return ( 44 | 54 | ); 55 | } 56 | 57 | export default function({ torrent }) { 58 | const { peersConnected, webseedsSendingToUs } = torrent; 59 | 60 | if (webseedsSendingToUs && peersConnected) { 61 | return ; 62 | } 63 | 64 | if (webseedsSendingToUs) { 65 | return ; 66 | } 67 | 68 | return ; 69 | } 70 | -------------------------------------------------------------------------------- /src/components/menus/RateContextMenu/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import CSSModules from 'react-css-modules'; 3 | import { inject } from 'mobx-react'; 4 | import autobind from 'autobind-decorator'; 5 | 6 | import ContextMenu from 'components/menus/ContextMenu'; 7 | 8 | import styles from './styles/index.css'; 9 | 10 | @inject('view_store', 'session_store') 11 | @CSSModules(styles) 12 | class RateContextMenu extends Component { 13 | @autobind onToggleRateContextMenu() { 14 | // FIXME 15 | const string = this.props.direction; 16 | const direction = string.charAt(0).toUpperCase() + string.slice(1); 17 | 18 | this.props.view_store[`toggle${direction}loadRateContextMenu`](); 19 | } 20 | 21 | @autobind onToggleContextMenu() { 22 | // TODO: Move it to ContextMenu component 23 | this.props.view_store.toggleContextMenus(); 24 | } 25 | 26 | @autobind onSetRateLimit(rateLimit) { 27 | this.props.session_store.setRateLimit(this.props.direction, rateLimit); 28 | } 29 | 30 | render() { 31 | const directionKey = `speed-limit-${this.props.direction}`; 32 | const rateLimit = this.props.session_store.settings[directionKey]; 33 | const enabled = this.props.session_store.settings[`${directionKey}-enabled`]; 34 | 35 | const rateList = { 36 | '5': '5 kB/s', 37 | '10': '10 kB/s', 38 | '20': '20 kB/s', 39 | '30': '30 kB/s', 40 | '40': '40 kB/s', 41 | '50': '50 kB/s', 42 | '75': '75 kB/s', 43 | '100': '100 kB/s', 44 | '150': '150 kB/s', 45 | '200': '200 kB/s', 46 | '250': '250 kB/s', 47 | '500': '500 kB/s', 48 | '750': '750 kB/s', 49 | }; 50 | 51 | return ( 52 | 58 |
    64 |
  • this.onSetRateLimit(0)}>Unlimited
  • 65 |
  • Limit ({rateList[`${rateLimit}`]})
  • 66 |
  • 67 | {Object.keys(rateList).map((key) => ( 68 |
  • this.onSetRateLimit(+key)}>{rateList[key]}
  • 69 | ))} 70 | 71 |
72 |
73 | ); 74 | } 75 | } 76 | 77 | export default RateContextMenu; 78 | -------------------------------------------------------------------------------- /src/components/dialogs/PreferencesDialog/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component} from 'react'; 2 | import CSSModules from 'react-css-modules'; 3 | import { inject, observer } from 'mobx-react'; 4 | import autobind from 'autobind-decorator'; 5 | 6 | import { Tab, Tabs, TabList, TabPanel } from 'react-tabs'; 7 | 8 | import Dialog from '../Dialog'; 9 | 10 | import TorrentsTabPanel from './TorrentsTabPanel'; 11 | import SpeedTabPanel from './SpeedTabPanel'; 12 | import PeersTabPanel from './PeersTabPanel'; 13 | import NetworkTabPanel from './NetworkTabPanel'; 14 | 15 | import styles from './styles/index.css'; 16 | 17 | @inject('view_store', 'session_store') 18 | @observer 19 | @CSSModules(styles) 20 | class PreferencesDialog extends Component { 21 | @autobind onBlur(event) { 22 | const type = event.target.type; 23 | const id = event.target.attributes.id.value; 24 | const value = event.target.value; 25 | 26 | if (type !== 'checkbox' && type !== 'radio') { 27 | this.props.session_store.setPreference(id, parseAsNumberIfNumber(value)); 28 | } 29 | } 30 | 31 | @autobind onChange(event) { 32 | const type = event.target.type; 33 | const id = event.target.attributes.id.value; 34 | const value = event.target.checked; 35 | 36 | if (type === 'checkbox') { 37 | this.props.session_store.setPreference(id, value); 38 | } 39 | } 40 | 41 | @autobind onHide() { 42 | this.props.view_store.togglePreferencesDialog(); 43 | } 44 | 45 | render() { 46 | return ( 47 | 52 |
53 |
54 | 55 | 56 | Torrents 57 | Speed 58 | Peers 59 | Network 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 |
75 |
76 |
77 | ); 78 | } 79 | } 80 | 81 | function parseAsNumberIfNumber(str) { 82 | if (parseInt(str, 10).toString() === str) { 83 | return parseInt(str, 10); 84 | } 85 | if (parseFloat(str).toString() === str) { 86 | return parseFloat(str); 87 | } 88 | } 89 | 90 | export default PreferencesDialog; 91 | -------------------------------------------------------------------------------- /src/components/Inspector/Trackers/TrackerGroup.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import CSSModules from 'react-css-modules'; 3 | 4 | import { 5 | lastAnnounceStatus, 6 | getAnnounceState, 7 | lastScrapeStatus, 8 | } from './services/tracker-stats'; 9 | 10 | import styles from './styles/index.css'; 11 | 12 | // NOTE: We're not currently supporting multitracker entries. 13 | // See http://www.bittornado.com/docs/multitracker-spec.txt for more info. 14 | function TrackerGroup({ trackers }) { 15 | return ( 16 |
17 | {trackers.length > 0 && 18 |
    19 | {trackers.map((tracker, index) => { 20 | const lastAnnounceStatusHash = lastAnnounceStatus(tracker); 21 | const announceState = getAnnounceState(tracker); 22 | const lastScrapeStatusHash = lastScrapeStatus(tracker); 23 | 24 | return ( 25 |
  • 26 |

    Tier {tracker.tier + 1}

    27 |
    28 |
    29 | {tracker.host || tracker.announce} 30 |
    31 |
    32 |
    33 |

    {lastAnnounceStatusHash.label}: {lastAnnounceStatusHash.value}

    34 |

    {announceState}

    35 |

    {lastScrapeStatusHash.label}: {lastScrapeStatusHash.value}

    36 |
    37 |
    38 |
    39 |
    Seeders:
    40 |
    {tracker.seederCount > -1 ? tracker.seederCount : 'N/A'}
    41 |
    42 |
    43 |
    Leechers:
    44 |
    {tracker.leecherCount > -1 ? tracker.leecherCount : 'N/A'}
    45 |
    46 |
    47 |
    Downloads:
    48 |
    {tracker.downloadCount > -1 ? tracker.downloadCount : 'N/A'}
    49 |
    50 |
    51 |
    52 |
    53 |
  • 54 | ); 55 | })} 56 |
57 | } 58 |
59 | ); 60 | } 61 | 62 | export default CSSModules(styles)(TrackerGroup); 63 | -------------------------------------------------------------------------------- /src/components/SelectableList/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component} from 'react'; 2 | import CSSModules from 'react-css-modules'; 3 | import autobind from 'autobind-decorator'; 4 | 5 | import Item from './Item'; 6 | 7 | import styles from './styles/index.css'; 8 | 9 | @CSSModules(styles) 10 | class SelectableList extends Component { 11 | @autobind onKeyDown(event) { 12 | const { children, lastSelectedItemId } = this.props; 13 | const key = event.key; 14 | const childIds = children.map((child) => child.props.id); 15 | const position = childIds.indexOf(lastSelectedItemId); 16 | let newSelectedId; 17 | 18 | if (key === 'ArrowUp') { 19 | newSelectedId = childIds[position - 1]; 20 | } 21 | 22 | if (key === 'ArrowDown') { 23 | newSelectedId = childIds[position + 1]; 24 | } 25 | 26 | if (newSelectedId) { 27 | this.props.onSelectItem(newSelectedId); 28 | } 29 | } 30 | 31 | @autobind onContextMenu(event, id) { 32 | const {selectedItemIds} = this.props; 33 | 34 | if (!selectedItemIds.includes(id)) { 35 | this.props.onSelectItem(id); 36 | } 37 | } 38 | 39 | @autobind onClick(event, id) { 40 | if (event.ctrlKey || event.metaKey) { 41 | this.props.onToggleSelectItem(id); 42 | return; 43 | } 44 | 45 | if (event.shiftKey) { 46 | const { children, lastSelectedItemId } = this.props; 47 | 48 | const childIds = children.map((child) => child.props.id); 49 | const selectedTorrentIndex = childIds.indexOf(id); 50 | const lastSelectedTorrentIndex = childIds.indexOf(lastSelectedItemId); 51 | const [lower, upper] = [lastSelectedTorrentIndex, selectedTorrentIndex].sort(); 52 | const selectedIds = childIds.filter((_, index) => index >= lower && index <= upper); 53 | 54 | this.props.onSelectRange(id, selectedIds); 55 | return; 56 | } 57 | 58 | this.props.onSelectItem(id); 59 | } 60 | 61 | render() { 62 | const {selectedItemIds} = this.props; 63 | 64 | return ( 65 |
    66 | {this.props.children.map((child, index) => { 67 | const childId = child.props.id; 68 | const isSelected = selectedItemIds.includes(childId); 69 | const isEven = index % 2 === 1; // Zero indexed. 70 | const className = `${styles.row} ${isSelected ? styles.selected : ''} ${isEven ? styles.even : ''}`; 71 | 72 | return ( 73 |
  • this.onClick(event, childId)} 77 | onContextMenu={(event) => this.onContextMenu(event, childId)} 78 | tabIndex={0} 79 | > 80 | {child} 81 |
  • 82 | ); 83 | })} 84 |
85 | ); 86 | } 87 | } 88 | 89 | SelectableList.Item = Item; 90 | 91 | export default SelectableList; 92 | -------------------------------------------------------------------------------- /src/components/dialogs/StatisticsDialog/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component} from 'react'; 2 | import CSSModules from 'react-css-modules'; 3 | import { inject, observer } from 'mobx-react'; 4 | import autobind from 'autobind-decorator'; 5 | 6 | import { size, timeInterval } from 'util/formatters'; 7 | 8 | import Dialog from '../Dialog'; 9 | 10 | import styles from './styles/index.css'; 11 | 12 | @inject('view_store', 'stats_store') 13 | @observer 14 | @CSSModules(styles) 15 | class StatisticsDialog extends Component { 16 | @autobind onHide() { 17 | this.props.view_store.toggleStatisticsDialog(); 18 | } 19 | 20 | render() { 21 | if (!this.props.view_store.isStatisticsDialogShown) { 22 | return false; 23 | } 24 | 25 | const cumulativeSession = this.props.stats_store.currentStats; 26 | const cumulativeTotal = this.props.stats_store.cumulativeStats; 27 | 28 | return ( 29 | 34 |
35 |

Current Session

36 |
37 |
Uploaded:
38 |
{ size(cumulativeSession.uploadedBytes) }
39 |
40 |
41 |
Downloaded:
42 |
{ size(cumulativeSession.downloadedBytes) }
43 |
44 |
45 |
Ratio:
46 |
None
47 |
48 |
49 |
Running Time:
50 |
{ timeInterval(cumulativeSession.secondsActive) } seconds
51 |
52 | 53 |

Total

54 |
55 |
Started:
56 |
{ cumulativeTotal.sessionCount } times
57 |
58 |
59 |
Uploaded:
60 |
{ size(cumulativeTotal.uploadedBytes) }
61 |
62 |
63 |
Downloaded:
64 |
{ size(cumulativeTotal.downloadedBytes) }
65 |
66 |
67 |
Ratio:
68 |
None
69 |
70 |
71 |
Running Time:
72 |
{ timeInterval(cumulativeTotal.secondsActive) } seconds
73 |
74 |
75 | 76 |
77 | ); 78 | } 79 | } 80 | 81 | export default StatisticsDialog; 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-transmission 2 | 3 | ## Introduction 4 | 5 | React Transmission is an ongoing reimplementation of [Transmission](https://transmissionbt.com) web interface. 6 | 7 | ![react-transmission](https://cloud.githubusercontent.com/assets/135988/20881507/fe5e60b8-badc-11e6-91b0-8bbe6636056d.png) 8 | 9 | You can find the original web interface source code [here](https://github.com/transmission/transmission/blob/master/web/) 10 | 11 | The official [Transmission repository](https://github.com/transmission/transmission) 12 | 13 | ## Goals 14 | 15 | - **Bump technology used:** More modern frameworks and technologies for a better modularization, performance and correctness. 16 | - **Accelerate development:** With the new set of technologies and components available in this project, new features are easier to implement. 17 | - **Javascript best practices:** Better source modularization, more documentation, source linting and many other improvements. 18 | - **More reliable & accurate behaviour:** Guarantee always that the interface reacts as expected and doesn't face UI race conditions. 19 | - **Tested:** A set of tests to guarantee the everything works as expected. 20 | - **Internationalization:** to be able to translate the interface easialy to other languages. 21 | 22 | ## Roadmap 23 | 24 | - First stage: achieve 100% feature parity with the original web interface. 25 | - Second stage: present this project to the main [Transmission](https://transmissionbt.com) development team to be evaluated 26 | - Third stage: merge this project to the [Transmission repository](https://github.com/transmission/transmission) 27 | - Fourth stage: improve the user interface with new features, more tests, add new languages, etc. 28 | 29 | ## Technology 30 | 31 | - [Webpack](https://webpack.github.io/) 32 | - [React](https://facebook.github.io/react/) 33 | - [Mobx](https://mobxjs.github.io/mobx/) 34 | - [CSS modules](https://github.com/css-modules/css-modules) 35 | 36 | ## Requirements 37 | 38 | - Node 4.5 39 | - Yarn 0.19 40 | 41 | ## Installation 42 | 43 | If you want to test this project without dealing with all the Node.js stuff, you can download the lastest release from here: 44 | 45 | https://github.com/fcsonline/react-transmission/releases 46 | 47 | Decompress it with: 48 | 49 | ``` 50 | unzip react-transmission-X.X.X.zip 51 | ``` 52 | 53 | Then, set this environment variable in your `.bashrc` 54 | 55 | ```bash 56 | export TRANSMISSION_WEB_HOME= 57 | ``` 58 | 59 | And start your Transmission instance. If everything gone well, your 60 | `react-transmission` instance should be like the previous one but you can 61 | differenciate because the background is a bit light yellow. 62 | 63 | Happy testing! 64 | 65 | ## Developement environment 66 | 67 | To be able to build this project, execute: 68 | 69 | ```bash 70 | git clone https://github.com/fcsonline/react-transmission 71 | cd react-transmission 72 | yarn install 73 | yarn start 74 | ``` 75 | 76 | Open Transmission daemon and then enable the web interface from the Settings window. 77 | 78 | Check this new interface out going to: `http://localhost:3000` 79 | 80 | ## License 81 | 82 | MIT 83 | -------------------------------------------------------------------------------- /src/components/toolbars/FilterToolbar/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component} from 'react'; 2 | import CSSModules from 'react-css-modules'; 3 | import { inject, observer } from 'mobx-react'; 4 | import autobind from 'autobind-decorator'; 5 | 6 | import { speedBps } from 'util/formatters'; 7 | import Torrent from 'stores/torrent'; 8 | 9 | import arrowUpImage from 'images/arrow-up.png'; 10 | import arrowDownImage from 'images/arrow-down.png'; 11 | 12 | import styles from './styles/index.css'; 13 | 14 | @inject('view_store', 'stats_store', 'torrents_store') 15 | @observer 16 | @CSSModules(styles) 17 | class FilterToolbar extends Component { 18 | @autobind deselectAllTorrents() { 19 | this.props.view_store.selectTorrents([]); 20 | } 21 | 22 | @autobind onChangeFilterState(event) { 23 | this.deselectAllTorrents(); 24 | this.props.torrents_store.setStatusFilter(+event.target.value); 25 | } 26 | 27 | @autobind onChangeFilterTracker(event) { 28 | this.deselectAllTorrents(); 29 | this.props.torrents_store.setTrackerFilter(event.target.value); 30 | } 31 | 32 | @autobind onChangeFilterText(event) { 33 | this.deselectAllTorrents(); 34 | this.props.torrents_store.setTextFilter(event.target.value); 35 | } 36 | 37 | render() { 38 | const torrentCount = this.props.stats_store.stats.torrentCount; 39 | const states = [ 40 | {value: -1, label: 'All'}, 41 | {value: 11, label: 'Active'}, 42 | {value: Torrent.STATUS_DOWNLOAD, label: 'Downloading'}, 43 | {value: Torrent.STATUS_SEED, label: 'Seeding'}, 44 | {value: Torrent.STATUS_STOPPED, label: 'Paused'}, 45 | {value: 55, label: 'Finished'}, 46 | ]; 47 | 48 | const trackers = this.props.torrents_store.trackers.map((domain) => { 49 | const label = domain.replace(/\b\w/g, (l) => l.toUpperCase()); // Capitalize 50 | 51 | return {value: domain, label}; 52 | }); 53 | 54 | return ( 55 |
56 | Show 57 | 58 |
59 | 62 | 66 | 67 | {torrentCount} Transfers 68 |
69 | 70 |
71 | 72 | Download speed 73 | {speedBps(this.props.torrents_store.totalDownloadSpeed)} 74 | 75 | 76 | Upload speed 77 | {speedBps(this.props.torrents_store.totalUploadSpeed)} 78 | 79 |
80 |
81 | ); 82 | } 83 | } 84 | 85 | export default FilterToolbar; 86 | -------------------------------------------------------------------------------- /src/components/toolbars/StatusToolbar/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component} from 'react'; 2 | import { findDOMNode } from 'react-dom'; 3 | import CSSModules from 'react-css-modules'; 4 | import { inject, observer } from 'mobx-react'; 5 | import autobind from 'autobind-decorator'; 6 | 7 | import settingsImage from 'images/settings.png'; 8 | import preferencesImage from 'images/wrench.png'; 9 | import compactImage from 'images/compact.png'; 10 | 11 | import SettingsContextMenu from 'components/menus/SettingsContextMenu'; 12 | 13 | import styles from './styles/index.css'; 14 | 15 | @inject('view_store', 'session_store') 16 | @observer 17 | @CSSModules(styles) 18 | class StatusToolbar extends Component { 19 | constructor(props) { 20 | super(props); 21 | 22 | this.state = { 23 | position: { 24 | left: 0, 25 | top: 0, 26 | }, 27 | }; 28 | } 29 | 30 | @autobind onTogglePreferences() { 31 | this.props.view_store.togglePreferencesDialog(); 32 | } 33 | 34 | @autobind onToggleCompact() { 35 | this.props.view_store.toggleCompact(); 36 | } 37 | 38 | @autobind onToggleTurtle() { 39 | this.props.session_store.togglePreference('alt-speed-enabled'); 40 | } 41 | 42 | @autobind onToggleSettings(event) { 43 | const { clientX, clientY } = event; 44 | 45 | event.preventDefault(); 46 | event.stopPropagation(); 47 | 48 | this.toggleContextMenu({left: clientX, top: clientY}); 49 | } 50 | 51 | @autobind toggleContextMenu(position) { 52 | this.props.view_store.toggleSettingsContextMenu(); 53 | this.setState({position}); 54 | } 55 | 56 | @autobind renderContextMenu() { 57 | const { position } = this.state; 58 | 59 | return ( 60 |
61 | findDOMNode(this.refs.target)} 65 | onHide={() => this.props.view_store.toggleSettingsContextMenu()} 66 | /> 67 |
68 | ); 69 | } 70 | 71 | render() { 72 | let compactClassName = styles.button; 73 | let turtleClassName = styles.buttonTurtle; 74 | 75 | if (this.props.view_store.compact) { 76 | compactClassName += ` ${styles.buttonActive}`; 77 | } 78 | 79 | if (this.props.session_store.settings['alt-speed-enabled']) { 80 | turtleClassName += ` ${styles.turtleActive}`; 81 | } 82 | 83 | return ( 84 |
85 | 88 | 91 | 95 | {this.renderContextMenu()} 96 |
97 | ); 98 | } 99 | } 100 | 101 | export default StatusToolbar; 102 | -------------------------------------------------------------------------------- /src/stores/__tests__/torrent-store.spec.js: -------------------------------------------------------------------------------- 1 | import TorrentStore from '../torrent-store'; 2 | import Torrent from '../torrent'; 3 | 4 | const { 5 | STATUS_STOPPED, 6 | STATUS_CHECK_WAIT, 7 | STATUS_CHECK, 8 | STATUS_DOWNLOAD_WAIT, 9 | STATUS_DOWNLOAD, 10 | STATUS_SEED_WAIT, 11 | STATUS_SEED, 12 | } = Torrent; 13 | 14 | const mapStatus = (arr) => arr.map(({ status }) => status); 15 | 16 | describe('TorrentStore', () => { 17 | describe('filteredTorrents', () => { 18 | let store; 19 | beforeEach(() => { 20 | store = new TorrentStore(); 21 | store.torrents = [ 22 | new Torrent({ status: Torrent.STATUS_STOPPED }), 23 | new Torrent({ status: Torrent.STATUS_CHECK_WAIT }), 24 | new Torrent({ status: Torrent.STATUS_CHECK }), 25 | new Torrent({ status: Torrent.STATUS_DOWNLOAD_WAIT }), 26 | new Torrent({ status: Torrent.STATUS_DOWNLOAD }), 27 | new Torrent({ status: Torrent.STATUS_SEED_WAIT }), 28 | new Torrent({ status: Torrent.STATUS_SEED }), 29 | ]; 30 | }); 31 | 32 | describe('when showing all', () => { 33 | it('should show all torrents', () => { 34 | expect(store.filteredTorrents.length).toBe(7); 35 | }); 36 | }); 37 | describe('when showing active', () => { 38 | xit('should show active torrents'); 39 | }); 40 | describe('when showing downloading', () => { 41 | it('should only show downloading torrents', () => { 42 | store.statusFilter = STATUS_DOWNLOAD; 43 | 44 | expect(mapStatus(store.filteredTorrents)).toContain(STATUS_DOWNLOAD); 45 | expect(store.filteredTorrents.length).toBe(1); 46 | }); 47 | }); 48 | describe('when showing seeding', () => { 49 | it('should only show seeding torrents', () => { 50 | store.statusFilter = STATUS_SEED; 51 | 52 | expect(mapStatus(store.filteredTorrents)).toContain(STATUS_SEED); 53 | expect(store.filteredTorrents.length).toBe(1); 54 | }); 55 | }); 56 | describe('when showing paused', () => { 57 | it('should only show paused torrents', () => { 58 | store.statusFilter = STATUS_STOPPED; 59 | 60 | expect(store.filteredTorrents[0].status).toBe(STATUS_STOPPED); 61 | expect(store.filteredTorrents.length).toBe(1); 62 | }); 63 | }); 64 | 65 | // TODO: Review those specs 66 | describe('when showing waiting for downloading', () => { 67 | it('should only show waiting downloading torrents', () => { 68 | store.statusFilter = STATUS_DOWNLOAD_WAIT; 69 | 70 | expect(mapStatus(store.filteredTorrents)).toContain(STATUS_DOWNLOAD_WAIT); 71 | expect(store.filteredTorrents.length).toBe(1); 72 | }); 73 | }); 74 | describe('when showing waiting for checking', () => { 75 | it('should only show checking torrents', () => { 76 | store.statusFilter = STATUS_CHECK_WAIT; 77 | 78 | expect(mapStatus(store.filteredTorrents)).toContain(STATUS_CHECK_WAIT); 79 | expect(store.filteredTorrents.length).toBe(1); 80 | }); 81 | }); 82 | describe('when showing waiting for seeding', () => { 83 | it('should only show waiting seeding torrents', () => { 84 | store.statusFilter = STATUS_SEED_WAIT; 85 | 86 | expect(store.filteredTorrents[0].status).toBe(STATUS_SEED_WAIT); 87 | expect(store.filteredTorrents.length).toBe(1); 88 | }); 89 | }); 90 | describe('when showing checked', () => { 91 | it('should only show checked torrents', () => { 92 | store.statusFilter = STATUS_CHECK; 93 | 94 | expect(store.filteredTorrents[0].status).toBe(STATUS_CHECK); 95 | expect(store.filteredTorrents.length).toBe(1); 96 | }); 97 | }); 98 | describe('when showing finished', () => { 99 | xit('should show finished torrents'); 100 | }); 101 | }); 102 | }); 103 | -------------------------------------------------------------------------------- /src/components/toolbars/ActionToolbar/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component} from 'react'; 2 | import CSSModules from 'react-css-modules'; 3 | import { inject, observer } from 'mobx-react'; 4 | import autobind from 'autobind-decorator'; 5 | 6 | import toolbarFolderImage from 'images/toolbar-folder.png'; 7 | import toolbarCloseImage from 'images/toolbar-close.png'; 8 | import toolbarPauseImage from 'images/toolbar-pause.png'; 9 | import toolbarStartImage from 'images/toolbar-start.png'; 10 | import toolbarPauseAllImage from 'images/toolbar-pause-all.png'; 11 | import toolbarStartAllImage from 'images/toolbar-start-all.png'; 12 | import toolbarInfoImage from 'images/toolbar-info.png'; 13 | 14 | import styles from './styles/index.css'; 15 | 16 | @inject('torrents_store', 'view_store') 17 | @observer 18 | @CSSModules(styles) 19 | class ActionToolbar extends Component { 20 | @autobind onOpen() { 21 | this.props.view_store.toggleOpenDialog(); 22 | } 23 | 24 | @autobind onRemove() { 25 | const confirmRemove = confirm(`Once removed, continuing the transfer will require the torrent file. Are you sure you want to remove it?`); 26 | 27 | if (!confirmRemove) return; 28 | 29 | this.props.torrents_store.remove(this.props.view_store.selectedTorrents); 30 | } 31 | 32 | @autobind onPause() { 33 | this.props.torrents_store.stop(this.props.view_store.selectedTorrents); 34 | } 35 | 36 | @autobind onStart() { 37 | this.props.torrents_store.start(this.props.view_store.selectedTorrents); 38 | } 39 | 40 | @autobind onPauseAll() { 41 | this.props.torrents_store.stop(this.props.torrents_store.torrents.map((torrent) => torrent.id)); 42 | } 43 | 44 | @autobind onStartAll() { 45 | this.props.torrents_store.start(this.props.torrents_store.torrents.map((torrent) => torrent.id)); 46 | } 47 | 48 | @autobind onToggleInspector() { 49 | this.props.view_store.toggleInspector(); 50 | } 51 | 52 | render() { 53 | const { view_store, torrents_store } = this.props; 54 | 55 | const selectedTorrents = torrents_store.getByIds(view_store.selectedTorrents); 56 | const isAnySelected = selectedTorrents.length > 0; 57 | const isAnyStarted = selectedTorrents.some((torrent) => torrent.isDownloading || torrent.isSeeding); 58 | const isAnyPaused = selectedTorrents.some((torrent) => torrent.isStopped); 59 | 60 | return ( 61 |
62 | 65 | 68 | 69 | 72 | 75 | 76 | 79 | 82 | 85 |
86 | ); 87 | } 88 | } 89 | 90 | export default ActionToolbar; 91 | -------------------------------------------------------------------------------- /src/components/Torrent/services/peer-details.js: -------------------------------------------------------------------------------- 1 | import { 2 | speedBps, 3 | countString, 4 | percentString, 5 | ratioString, 6 | formatStatus, 7 | formatError, 8 | } from 'util/formatters'; 9 | 10 | function formatUL(torrent) { 11 | return `↑ ${speedBps(torrent.rateUpload)}`; 12 | }; 13 | 14 | function formatDL(torrent) { 15 | return `↓ ${speedBps(torrent.rateDownload)}`; 16 | }; 17 | 18 | // Downloading from 2 of 3 peer(s) and 2 webseed(s) 19 | function formatDownloadFromPeersAndWebseeds(torrent, peerCount, webseedCount) { 20 | return [ 21 | 'Downloading from', 22 | torrent.peersSendingToUs, 23 | 'of', 24 | countString('peer', 'peers', peerCount), 25 | 'and', 26 | countString('web seed', 'web seeds', webseedCount), 27 | '-', 28 | formatDL(torrent), 29 | formatUL(torrent), 30 | ].join(' '); 31 | } 32 | 33 | // Downloading from 2 webseed(s) 34 | function formatDownloadFromWebseeds(torrent, webseedCount) { 35 | return [ 36 | 'Downloading from', 37 | countString('web seed', 'web seeds', webseedCount), 38 | '-', 39 | formatDL(torrent), 40 | formatUL(torrent), 41 | ].join(' '); 42 | } 43 | 44 | // Downloading from 2 of 3 peer(s) 45 | function formatDownloadFromSeeds(torrent, peerCount) { 46 | return [ 47 | 'Downloading from', 48 | torrent.peersSendingToUs, 49 | 'of', 50 | countString('peer', 'peers', peerCount), 51 | '-', 52 | formatDL(torrent), 53 | formatUL(torrent), 54 | ].join(' '); 55 | } 56 | 57 | function formatDownloading(torrent) { 58 | const peerCount = torrent.peersConnected; 59 | const webseedCount = torrent.webseedsSendingToUs; 60 | 61 | if (webseedCount && peerCount) { 62 | return formatDownloadFromPeersAndWebseeds(torrent, peerCount, webseedCount); 63 | } 64 | 65 | if (webseedCount) { 66 | return formatDownloadFromWebseeds(torrent, webseedCount); 67 | } 68 | 69 | return formatDownloadFromSeeds(torrent, peerCount); 70 | } 71 | 72 | function formatSeeding(torrent) { 73 | return [ 74 | 'Seeding to', 75 | torrent.peersGettingFromUs, 76 | 'of', 77 | countString('peer', 'peers', torrent.peersConnected), 78 | '-', 79 | formatUL(torrent), 80 | ].join(' '); 81 | } 82 | 83 | function formatChecking(torrent) { 84 | return [ 85 | 'Verifying local data (', 86 | percentString(100.0 * torrent.recheckProgress), 87 | '% tested)', 88 | ].join(''); 89 | } 90 | 91 | function formatSeedingShort(torrent) { 92 | return [ 93 | 'Ratio: ', 94 | ratioString(torrent.uploadRatio), 95 | ', ', 96 | formatUL(torrent), 97 | ].join(''); 98 | } 99 | 100 | function formatDownloadingShort(torrent) { 101 | const hasDownloadActivity = torrent.rateDownload > 0; 102 | const hasUploadActivity = torrent.rateUpload > 0; 103 | 104 | if (!hasUploadActivity && !hasDownloadActivity) { 105 | return 'Idle'; 106 | } 107 | 108 | let s = ''; 109 | 110 | if (hasDownloadActivity) { 111 | s += formatDL(torrent); 112 | } 113 | 114 | if (hasDownloadActivity && hasUploadActivity) { 115 | s += ' '; 116 | } 117 | 118 | if (hasUploadActivity) { 119 | s += formatUL(torrent); 120 | } 121 | 122 | return s; 123 | } 124 | 125 | export function getPeerDetails(torrent) { 126 | if (torrent.hasErrors) { 127 | return formatError(torrent); 128 | } 129 | 130 | if (torrent.isDownloading) { 131 | return formatDownloading(torrent); 132 | } 133 | 134 | if (torrent.isSeeding) { 135 | return formatSeeding(torrent); 136 | } 137 | 138 | if (torrent.isChecking) { 139 | return formatChecking(torrent); 140 | } 141 | 142 | return formatStatus(torrent); 143 | } 144 | 145 | export function getPeerDetailsShort(torrent) { 146 | if (torrent.hasErrors) { 147 | return formatError(torrent); 148 | } 149 | 150 | if (torrent.isDownloading) { 151 | return formatDownloadingShort(torrent); 152 | } 153 | 154 | if (torrent.isSeeding) { 155 | return formatSeedingShort(torrent); 156 | } 157 | 158 | return formatStatus(torrent); 159 | } 160 | -------------------------------------------------------------------------------- /src/stores/view-store.js: -------------------------------------------------------------------------------- 1 | import {observable, action} from 'mobx'; 2 | 3 | class ViewStore { 4 | @observable currentFilter = 'all'; 5 | // TODO: Rename to selectedTorrentIds 6 | @observable selectedTorrents = []; 7 | @observable lastSelectedTorrent = null; 8 | @observable compact = false; 9 | @observable notificationsEnabled = false; 10 | 11 | @observable torrentContextMenuShown = null; 12 | 13 | @observable isSettingsContextMenuShown = false; 14 | @observable isSortByContextMenuShown = false; 15 | @observable isDownloadRateContextMenuShown = false; 16 | @observable isUploadRateContextMenuShown = false; 17 | 18 | @observable isRenamePromptShown = false; 19 | @observable isLocationPromptShown = false; 20 | 21 | // TODO: find a better way to manage them 22 | @observable isOpenDialogShown = false; 23 | @observable isPreferencesDialogShown = false; 24 | @observable isConnectionDialogShown = false; 25 | @observable isStatisticsDialogShown = false; 26 | @observable isAboutDialogShown = false; 27 | @observable isInspectorShown = false; 28 | 29 | @action toggleContextMenus() { 30 | this.isSettingsContextMenuShown = false; 31 | this.isTorrentContextMenuShown = false; 32 | this.isSortByContextMenuShown = false; 33 | this.isDownloadRateContextMenuShown = false; 34 | this.isUploadRateContextMenuShown = false; 35 | } 36 | 37 | @action toggleSettingsContextMenu() { 38 | this.toggleContextMenus(); 39 | this.isSettingsContextMenuShown = !this.isSettingsContextMenuShown; 40 | } 41 | 42 | @action toggleTorrentContextMenu(id) { 43 | this.toggleContextMenus(); 44 | this.torrentContextMenuShown = id; 45 | } 46 | 47 | @action toggleSortByContextMenu() { 48 | this.isSortByContextMenuShown = !this.isSortByContextMenuShown; 49 | } 50 | 51 | @action toggleDownloadRateContextMenu() { 52 | this.isDownloadRateContextMenuShown = !this.isDownloadRateContextMenuShown; 53 | } 54 | 55 | @action toggleUploadRateContextMenu() { 56 | this.isUploadRateContextMenuShown = !this.isUploadRateContextMenuShown; 57 | } 58 | 59 | @action toggleRenamePrompt() { 60 | this.isRenamePromptShown = !this.isRenamePromptShown; 61 | } 62 | 63 | @action toggleLocationPrompt() { 64 | this.isLocationPromptShown = !this.isLocationPromptShown; 65 | } 66 | 67 | @action toggleOpenDialog() { 68 | this.isOpenDialogShown = !this.isOpenDialogShown; 69 | } 70 | 71 | @action togglePreferencesDialog() { 72 | this.isPreferencesDialogShown = !this.isPreferencesDialogShown; 73 | } 74 | 75 | @action toggleConnectionDialog(value) { 76 | this.isConnectionDialogShown = value; 77 | } 78 | 79 | @action toggleStatisticsDialog() { 80 | this.isStatisticsDialogShown = !this.isStatisticsDialogShown; 81 | } 82 | 83 | @action toggleAboutDialog() { 84 | this.isAboutDialogShown = !this.isAboutDialogShown; 85 | } 86 | 87 | @action toggleInspector() { 88 | this.isInspectorShown = !this.isInspectorShown; 89 | } 90 | 91 | @action toggleCompact() { 92 | this.compact = !this.compact; 93 | } 94 | 95 | @action setFilter(filter) { 96 | this.currentFilter = filter; 97 | } 98 | 99 | @action setSelected(id) { 100 | this.lastSelectedTorrent = id; 101 | this.selectedTorrents = [id]; 102 | } 103 | 104 | @action toggleSelected(id) { 105 | const torrent = this.selectedTorrents.find((torrentId) => id === torrentId); 106 | 107 | this.lastSelectedTorrent = id; 108 | 109 | if (torrent) { 110 | this.selectedTorrents.remove(torrent); 111 | return; 112 | } 113 | 114 | this.selectedTorrents.push(id); 115 | } 116 | 117 | @action addSelectedRange(id, ids) { 118 | this.selectedTorrents = [...new Set(this.selectedTorrents.concat(ids))]; // Unique 119 | this.lastSelectedTorrent = id; 120 | } 121 | 122 | @action selectTorrents(torrentIds) { 123 | this.selectedTorrents = torrentIds; 124 | } 125 | 126 | @action toggleNotificationsEnabled(enable) { 127 | this.notificationsEnabled = enable; 128 | } 129 | } 130 | 131 | export default ViewStore; 132 | -------------------------------------------------------------------------------- /src/components/dialogs/OpenDialog/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component} from 'react'; 2 | import CSSModules from 'react-css-modules'; 3 | import { inject, observer } from 'mobx-react'; 4 | import autobind from 'autobind-decorator'; 5 | import { size } from 'util/formatters'; 6 | 7 | import Dialog from '../Dialog'; 8 | import logoImage from 'images/logo.png'; 9 | 10 | import TorrentUpload from 'stores/torrent-upload'; 11 | 12 | import styles from './styles/index.css'; 13 | 14 | @inject('view_store', 'torrents_store', 'session_store') 15 | @observer 16 | @CSSModules(styles) 17 | class OpenDialog extends Component { 18 | constructor(props) { 19 | super(props); 20 | 21 | this.torrentUpload = new TorrentUpload(); 22 | this.torrentUpload.setDownloadDir(this.props.session_store.settings['download-dir']); 23 | } 24 | 25 | @autobind onUpload(event) { 26 | event.preventDefault(); 27 | this.torrentUpload.serialize().then((torrents) => { 28 | torrents.forEach((torrentData) => this.props.torrents_store.add(torrentData)); 29 | }); 30 | this.props.view_store.toggleOpenDialog(); 31 | } 32 | 33 | @autobind onCancel(event) { 34 | event.preventDefault(); 35 | this.props.view_store.toggleOpenDialog(); 36 | } 37 | 38 | @autobind onHide() { 39 | this.props.view_store.toggleOpenDialog(); 40 | } 41 | 42 | @autobind onKeyDown(event) { 43 | const key = event.key; 44 | 45 | if (key === 'Enter') { 46 | this.onUpload(event); 47 | } 48 | } 49 | 50 | @autobind onChangeFiles({ target }) { 51 | this.torrentUpload.setTorrentFiles(target.files); 52 | } 53 | 54 | @autobind onChangeUrl({ target }) { 55 | this.torrentUpload.setTorrentUrl(target.value); 56 | } 57 | 58 | @autobind onChangeDownloadDirectory({ target }) { 59 | this.torrentUpload.setDownloadDir(target.value); 60 | } 61 | 62 | @autobind onBlurDownloadDirectory({ target }) { 63 | this.props.session_store.getFreeSpace(target.value); 64 | } 65 | 66 | @autobind onChangeStart({ target }) { 67 | this.torrentUpload.setPaused(!target.checked); 68 | } 69 | 70 | renderFreeSpace() { 71 | const freeSpace = this.props.session_store.freeSpace; 72 | 73 | if (freeSpace < 0) { 74 | return null; 75 | } 76 | 77 | return ( 78 | ({ size(freeSpace) } Free) 79 | ); 80 | } 81 | 82 | render() { 83 | return ( 84 | 89 |
90 |
91 | logo 92 |
93 |
94 |
95 |
96 |
97 | 98 | 99 |
100 | 101 |
102 | 103 | 104 |
105 | 106 |
107 | 108 | 109 |
110 | 111 |
112 | 116 |
117 |
118 |
119 | 120 | 121 |
122 |
123 |
124 |
125 | 126 |
127 | ); 128 | } 129 | } 130 | 131 | export default OpenDialog; 132 | -------------------------------------------------------------------------------- /src/components/menus/SettingsContextMenu/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { findDOMNode } from 'react-dom'; 3 | import CSSModules from 'react-css-modules'; 4 | import { inject, observer } from 'mobx-react'; 5 | import autobind from 'autobind-decorator'; 6 | 7 | import ContextMenu from 'components/menus/ContextMenu'; 8 | import SortByContextMenu from 'components/menus/SortByContextMenu'; 9 | import RateContextMenu from 'components/menus/RateContextMenu'; 10 | 11 | import styles from './styles/index.css'; 12 | 13 | @inject('view_store') 14 | @observer 15 | @CSSModules(styles) 16 | class TorrentContextMenu extends Component { 17 | @autobind onAbout() { 18 | this.props.view_store.toggleAboutDialog(); 19 | } 20 | 21 | @autobind onStatistics() { 22 | this.props.view_store.toggleStatisticsDialog(); 23 | } 24 | 25 | @autobind onToggleSortByContextMenu() { 26 | this.props.view_store.toggleSortByContextMenu(); 27 | } 28 | 29 | @autobind onToggleDownloadRateContextMenu() { 30 | this.props.view_store.toggleDownloadRateContextMenu(); 31 | } 32 | 33 | @autobind onToggleUploadRateContextMenu() { 34 | this.props.view_store.toggleUploadRateContextMenu(); 35 | } 36 | 37 | @autobind onToggleContextMenu() { 38 | // TODO: Move it to ContextMenu component 39 | this.props.view_store.toggleContextMenus(); 40 | } 41 | 42 | render() { 43 | return ( 44 | 50 |
    51 |
  • About
  • 52 |
  • 53 |
  • 54 | 55 | Transmission Home page 56 | 57 |
  • 58 |
  • 59 | 60 | Transmission Tip Jar 61 | 62 |
  • 63 |
  • 64 |
  • Statistics
  • 65 |
  • 66 |
  • 72 | Total Download rate 73 | findDOMNode(this.refs.downloadRateTarget)} 78 | onHide={this.props.onHide} 79 | /> 80 |
  • 81 |
  • 87 | Total Upload rate 88 | findDOMNode(this.refs.uploadRateTarget)} 93 | onHide={this.props.onHide} 94 | /> 95 |
  • 96 |
  • 97 |
  • 103 | Sort Transfers By 104 | findDOMNode(this.refs.sortByTarget)} 108 | onHide={this.props.onHide} 109 | /> 110 |
  • 111 |
112 |
113 | ); 114 | } 115 | } 116 | 117 | export default TorrentContextMenu; 118 | -------------------------------------------------------------------------------- /src/components/menus/TorrentContextMenu/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import CSSModules from 'react-css-modules'; 3 | import { inject } from 'mobx-react'; 4 | import autobind from 'autobind-decorator'; 5 | 6 | import ContextMenu from 'components/menus/ContextMenu'; 7 | 8 | import styles from './styles/index.css'; 9 | 10 | @inject('view_store', 'torrents_store') 11 | @CSSModules(styles) 12 | class TorrentContextMenu extends Component { 13 | @autobind pause() { 14 | this.props.torrents_store.stop(this.props.view_store.selectedTorrents); 15 | } 16 | 17 | @autobind resume() { 18 | this.props.torrents_store.start(this.props.view_store.selectedTorrents); 19 | } 20 | 21 | @autobind resumeNow() { 22 | this.props.torrents_store.startNow(this.props.view_store.selectedTorrents); 23 | } 24 | 25 | @autobind remove() { 26 | // TODO: Confirm dialog 27 | this.props.torrents_store.remove(this.props.view_store.selectedTorrents); 28 | } 29 | 30 | @autobind trashAndRemove() { 31 | // TODO: Confirm dialog 32 | this.props.torrents_store.remove(this.props.view_store.selectedTorrents, { 33 | 'delete-local-data': true, 34 | }); 35 | } 36 | 37 | @autobind setLocation() { 38 | this.props.view_store.toggleLocationPrompt(); 39 | } 40 | 41 | @autobind rename() { 42 | this.props.view_store.toggleRenamePrompt(); 43 | } 44 | 45 | @autobind queueMoveTop() { 46 | this.props.torrents_store.queueMoveTop(this.props.view_store.selectedTorrents); 47 | } 48 | 49 | @autobind queueMoveUp() { 50 | this.props.torrents_store.queueMoveUp(this.props.view_store.selectedTorrents); 51 | } 52 | 53 | @autobind queueMoveDown() { 54 | this.props.torrents_store.queueMoveDown(this.props.view_store.selectedTorrents); 55 | } 56 | 57 | @autobind queueMoveBottom() { 58 | this.props.torrents_store.queueMoveBottom(this.props.view_store.selectedTorrents); 59 | } 60 | 61 | @autobind verify() { 62 | this.props.torrents_store.verify(this.props.view_store.selectedTorrents); 63 | } 64 | 65 | @autobind askTrackerMorePeers() { 66 | this.props.torrents_store.askTrackerMorePeers(this.props.view_store.selectedTorrents); 67 | } 68 | 69 | @autobind onSelectAll() { 70 | this.props.view_store.selectTorrents(this.props.torrents_store.filteredTorrents.map((torrent) => torrent.id)); 71 | } 72 | 73 | @autobind onDeselectAll() { 74 | this.props.view_store.selectTorrents([]); 75 | } 76 | 77 | @autobind onToggleContextMenu() { 78 | // TODO: Move it to ContextMenu component 79 | this.props.view_store.toggleContextMenus(); 80 | } 81 | 82 | render() { 83 | const selectedTorrents = this.props.view_store.selectedTorrents; 84 | const noMultiple = selectedTorrents.length > 1 ? styles.torrentMenuItemDisabled : styles.torrentMenuItem; 85 | 86 | return ( 87 | 93 |
    94 |
  • Pause
  • 95 |
  • Resume
  • 96 |
  • Resume Now
  • 97 |
  • 98 |
  • Move to Top
  • 99 |
  • Move Up
  • 100 |
  • Move Down
  • 101 |
  • Move to Bottom
  • 102 |
  • 103 |
  • Remove From List...
  • 104 |
  • Trash Data & Remove From List...
  • 105 |
  • 106 |
  • Verify Local Data
  • 107 |
  • Set Location...
  • 108 |
  • Rename...
  • 109 |
  • 110 |
  • Ask tracker for more peers
  • 111 |
  • 112 |
  • Select All
  • 113 |
  • Deselect All
  • 114 |
115 |
116 | ); 117 | } 118 | } 119 | 120 | export default TorrentContextMenu; 121 | -------------------------------------------------------------------------------- /scripts/translate.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 8 | 9 | exports.default = printICUMessage; 10 | 11 | var _intlMessageformatParser = require('intl-messageformat-parser'); 12 | 13 | var _fs = require('fs'); 14 | 15 | var fs = _interopRequireWildcard(_fs); 16 | 17 | var _glob = require('glob'); 18 | 19 | var _mkdirp = require('mkdirp'); 20 | 21 | function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } 22 | 23 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 24 | 25 | var MESSAGES_PATTERN = './build/messages/**/*.json'; 26 | var LANG_DIR = './build/lang/'; 27 | 28 | var ESCAPED_CHARS = { 29 | '\\': '\\\\', 30 | '\\#': '\\#', 31 | '{': '\\{', 32 | '}': '\\}' 33 | }; 34 | 35 | var ESAPE_CHARS_REGEXP = /\\#|[{}\\]/g; 36 | 37 | function printICUMessage(ast) { 38 | return ast.elements.reduce(function (message, el) { 39 | var format = el.format, 40 | id = el.id, 41 | type = el.type, 42 | value = el.value; 43 | 44 | 45 | if (type === 'messageTextElement') { 46 | return message + value.replace(ESAPE_CHARS_REGEXP, function (char) { 47 | return ESCAPED_CHARS[char]; 48 | }); 49 | } 50 | 51 | if (!format) { 52 | return message + ('{' + id + '}'); 53 | } 54 | 55 | var formatType = format.type.replace(/Format$/, ''); 56 | 57 | var style = void 0, 58 | offset = void 0, 59 | options = void 0; 60 | 61 | switch (formatType) { 62 | case 'number': 63 | case 'date': 64 | case 'time': 65 | style = format.style ? ', ' + format.style : ''; 66 | return message + ('{' + id + ', ' + formatType + style + '}'); 67 | 68 | case 'plural': 69 | case 'selectOrdinal': 70 | case 'select': 71 | offset = format.offset ? ', offset:' + format.offset : ''; 72 | options = format.options.reduce(function (str, option) { 73 | var optionValue = printICUMessage(option.value); 74 | return str + (' ' + option.selector + ' {' + optionValue + '}'); 75 | }, ''); 76 | return message + ('{' + id + ', ' + formatType + offset + ',' + options + '}'); 77 | } 78 | }, ''); 79 | } 80 | 81 | var Translator = function () { 82 | function Translator(translateText) { 83 | _classCallCheck(this, Translator); 84 | 85 | this.translateText = translateText; 86 | } 87 | 88 | _createClass(Translator, [{ 89 | key: 'translate', 90 | value: function translate(message) { 91 | var ast = (0, _intlMessageformatParser.parse)(message); 92 | var translated = this.transform(ast); 93 | return print(translated); 94 | } 95 | }, { 96 | key: 'transform', 97 | value: function transform(ast) { 98 | var _this = this; 99 | 100 | ast.elements.forEach(function (el) { 101 | if (el.type === 'messageTextElement') { 102 | el.value = _this.translateText(el.value); 103 | } else { 104 | var options = el.format && el.format.options; 105 | if (options) { 106 | options.forEach(function (option) { 107 | return _this.transform(option.value); 108 | }); 109 | } 110 | } 111 | }); 112 | 113 | return ast; 114 | } 115 | }]); 116 | 117 | return Translator; 118 | }(); 119 | 120 | var defaultMessages = (0, _glob.sync)(MESSAGES_PATTERN).map(function (filename) { 121 | return fs.readFileSync(filename, 'utf8'); 122 | }).map(function (file) { 123 | return JSON.parse(file); 124 | }).reduce(function (collection, descriptors) { 125 | descriptors.forEach(function (_ref) { 126 | var id = _ref.id, 127 | defaultMessage = _ref.defaultMessage; 128 | 129 | if (collection.hasOwnProperty(id)) { 130 | throw new Error('Duplicate message id: ' + id); 131 | } 132 | 133 | collection[id] = defaultMessage; 134 | }); 135 | 136 | return collection; 137 | }, {}); 138 | 139 | (0, _mkdirp.sync)(LANG_DIR); 140 | fs.writeFileSync(LANG_DIR + 'en.json', JSON.stringify(defaultMessages, null, 2)); 141 | 142 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-transmission", 3 | "version": "0.1.0", 4 | "private": true, 5 | "contributors": [ 6 | "Ferran Basora ", 7 | "Eduardo Lanchares ", 8 | "Sebastian Boolean", 9 | "Secretmapper" 10 | ], 11 | "devDependencies": { 12 | "autoprefixer": "6.5.1", 13 | "babel-core": "6.17.0", 14 | "babel-eslint": "^7.2.2", 15 | "babel-jest": "17.0.2", 16 | "babel-loader": "6.2.7", 17 | "babel-plugin-react-intl": "^2.3.1", 18 | "babel-plugin-syntax-trailing-function-commas": "^6.20.0", 19 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 20 | "babel-plugin-transform-export-extensions": "^6.8.0", 21 | "babel-plugin-transform-object-rest-spread": "^6.20.2", 22 | "babel-preset-react-app": "^2.0.1", 23 | "case-sensitive-paths-webpack-plugin": "1.1.4", 24 | "chalk": "1.1.3", 25 | "connect-history-api-fallback": "1.3.0", 26 | "cross-spawn": "4.0.2", 27 | "css-loader": "0.26.0", 28 | "detect-port": "1.0.1", 29 | "dotenv": "2.0.0", 30 | "enzyme": "^2.7.0", 31 | "eslint": "^3.19.0", 32 | "eslint-config-react-app": "^0.5.0", 33 | "eslint-config-standard": "^10.2.0", 34 | "eslint-config-standard-react": "^4.3.0", 35 | "eslint-loader": "1.6.0", 36 | "eslint-plugin-flowtype": "^2.30.4", 37 | "eslint-plugin-import": "^2.2.0", 38 | "eslint-plugin-jsx-a11y": "^4.0.0", 39 | "eslint-plugin-node": "^4.2.2", 40 | "eslint-plugin-promise": "^3.5.0", 41 | "eslint-plugin-react": "^6.10.3", 42 | "eslint-plugin-standard": "^3.0.1", 43 | "extract-text-webpack-plugin": "1.0.1", 44 | "file-loader": "0.9.0", 45 | "filesize": "3.3.0", 46 | "fs-extra": "0.30.0", 47 | "gzip-size": "3.0.0", 48 | "html-webpack-plugin": "2.24.0", 49 | "http-proxy-middleware": "0.17.2", 50 | "identity-obj-proxy": "^3.0.0", 51 | "jest": "17.0.2", 52 | "json-loader": "0.5.4", 53 | "object-assign": "4.1.0", 54 | "path-exists": "2.1.0", 55 | "postcss-loader": "1.0.0", 56 | "promise": "7.1.1", 57 | "react-addons-test-utils": "^15.4.2", 58 | "react-dev-utils": "^0.4.2", 59 | "react-hot-loader": "3.0.0-beta.6", 60 | "react-test-renderer": "^15.4.2", 61 | "recursive-readdir": "2.1.0", 62 | "strip-ansi": "3.0.1", 63 | "style-loader": "0.13.1", 64 | "url-loader": "0.5.7", 65 | "webpack": "1.14.0", 66 | "webpack-dev-server": "1.16.2", 67 | "webpack-manifest-plugin": "1.1.0", 68 | "whatwg-fetch": "1.0.0" 69 | }, 70 | "dependencies": { 71 | "autobind-decorator": "1.3.4", 72 | "lodash": "^4.17.4", 73 | "mobx": "2.4.1", 74 | "mobx-react": "3.5.3", 75 | "mobx-react-devtools": "^4.2.11", 76 | "normalize.css": "^5.0.0", 77 | "react": "^15.4.2", 78 | "react-css-modules": "3.7.10", 79 | "react-dom": "^15.4.2", 80 | "react-dropzone": "^3.8.0", 81 | "react-intl": "^2.2.3", 82 | "react-overlays": "0.6.10", 83 | "react-tabs": "^0.8.2" 84 | }, 85 | "scripts": { 86 | "start": "node scripts/start.js", 87 | "build": "node scripts/build.js", 88 | "build:langs": "node scripts/translate.js", 89 | "test": "node --harmony_proxies scripts/test.js --env=jsdom", 90 | "lint": "eslint src", 91 | "lint:fix": "eslint --fix src" 92 | }, 93 | "jest": { 94 | "coverageDirectory": "/coverage", 95 | "collectCoverageFrom": [ 96 | "src/**/*.{js,jsx}" 97 | ], 98 | "setupFiles": [ 99 | "/config/polyfills.js" 100 | ], 101 | "testPathIgnorePatterns": [ 102 | "[/\\\\](build|docs|node_modules)[/\\\\]" 103 | ], 104 | "testEnvironment": "node", 105 | "testURL": "http://localhost", 106 | "transform": { 107 | "^.+\\.(js|jsx)$": "/node_modules/babel-jest", 108 | "^.+\\.css$": "/config/jest/cssTransform.js", 109 | "^(?!.*\\.(js|jsx|css|json)$)": "/config/jest/fileTransform.js" 110 | }, 111 | "transformIgnorePatterns": [ 112 | "[/\\\\]node_modules[/\\\\].+\\.(js|jsx)$" 113 | ], 114 | "moduleNameMapper": { 115 | "^react-native$": "react-native-web", 116 | "\\.css$": "identity-obj-proxy", 117 | "^test(.*)$": "/test$1" 118 | }, 119 | "modulePaths": [ 120 | "/src" 121 | ] 122 | }, 123 | "babel": { 124 | "presets": [ 125 | "react-app" 126 | ], 127 | "plugins": [ 128 | "react-hot-loader/babel", 129 | "babel-plugin-transform-decorators-legacy", 130 | "babel-plugin-transform-object-rest-spread", 131 | "babel-plugin-syntax-trailing-function-commas", 132 | "babel-plugin-transform-export-extensions", 133 | [ 134 | "babel-plugin-react-intl", 135 | { 136 | "messagesDir": "./build/messages/" 137 | } 138 | ] 139 | ] 140 | }, 141 | "eslintConfig": { 142 | "parser": "babel-eslint", 143 | "extends": ["react-app", "standard", "standard-react"], 144 | "rules": { 145 | "arrow-parens": [ 146 | "error", 147 | "always" 148 | ], 149 | "comma-dangle": [ 150 | "error", 151 | "always-multiline" 152 | ], 153 | "no-var": [ 154 | "error" 155 | ], 156 | "semi": [ 157 | "error", 158 | "always" 159 | ], 160 | "space-before-function-paren": [ 161 | "error", 162 | "never" 163 | ], 164 | "react/no-unused-prop-types": [ 165 | "off" 166 | ], 167 | "react/prop-types": [ 168 | "off" 169 | ], 170 | "camelcase": [ 171 | "off" 172 | ] 173 | } 174 | } 175 | } 176 | --------------------------------------------------------------------------------