├── 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 |
5 | `;
6 |
7 | exports[`test StatusButton resume 1`] = `
8 |
11 | `;
12 |
--------------------------------------------------------------------------------
/src/components/dialogs/AboutDialog/styles/index.css:
--------------------------------------------------------------------------------
1 | .body {
2 | composes: body from "../../Dialog/styles/index.css";
3 | }
4 |
5 | .content {
6 | padding: 30px;
7 | display: flex;
8 | flex-direction: column;
9 | align-items: center;
10 | }
11 |
12 | .logo {
13 | composes: logo from "../../Dialog/styles/index.css";
14 | }
15 |
--------------------------------------------------------------------------------
/src/components/dialogs/ConnectionDialog/styles/index.css:
--------------------------------------------------------------------------------
1 | .body {
2 | composes: body from "../../Dialog/styles/index.css";
3 | padding: 20px;
4 | }
5 |
6 | .logo {
7 | composes: logo from "../../Dialog/styles/index.css";
8 | }
9 |
10 | .content {
11 | display: flex;
12 | flex-direction: column;
13 | max-width: 250px;
14 | }
15 |
--------------------------------------------------------------------------------
/src/components/dialogs/PreferencesDialog/fields/SelectRow/styles/index.css:
--------------------------------------------------------------------------------
1 | .row {
2 | display: flex;
3 | padding-bottom: 10px;
4 | }
5 |
6 | .key {
7 | width: 180px;
8 | display: flex;
9 | }
10 |
11 | .key input {
12 | margin-right: 5px;
13 | }
14 |
15 | .key label {
16 | flex: 1;
17 | }
18 |
19 | .value {
20 | flex: 1;
21 | }
22 |
--------------------------------------------------------------------------------
/src/components/dialogs/PreferencesDialog/fields/CheckValueRow/styles/index.css:
--------------------------------------------------------------------------------
1 | .row {
2 | display: flex;
3 | padding-bottom: 10px;
4 | }
5 |
6 | .key {
7 | width: 180px;
8 | display: flex;
9 | }
10 |
11 | .key input {
12 | margin-right: 5px;
13 | }
14 |
15 | .key label {
16 | flex: 1;
17 | }
18 |
19 | .value {
20 | flex: 1;
21 | }
22 |
--------------------------------------------------------------------------------
/test/util/createComponentWithIntl.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import renderer from 'react-test-renderer';
3 | import { IntlProvider } from 'react-intl';
4 |
5 | export default function(children, props = { locale: 'en' }) {
6 | return renderer.create(
7 |
8 | {children}
9 |
10 | );
11 | }
12 |
--------------------------------------------------------------------------------
/config/jest/fileTransform.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | // This is a custom Jest transformer turning file imports into filenames.
4 | // http://facebook.github.io/jest/docs/tutorial-webpack.html
5 |
6 | module.exports = {
7 | process(src, filename) {
8 | return 'module.exports = ' + JSON.stringify(path.basename(filename)) + ';';
9 | },
10 | };
11 |
--------------------------------------------------------------------------------
/config/jest/cssTransform.js:
--------------------------------------------------------------------------------
1 | // This is a custom Jest transformer turning style imports into empty objects.
2 | // http://facebook.github.io/jest/docs/tutorial-webpack.html
3 |
4 | module.exports = {
5 | process() {
6 | return 'module.exports = {};';
7 | },
8 | getCacheKey(fileData, filename) {
9 | // The output is always the same.
10 | return 'cssTransform';
11 | },
12 | };
13 |
--------------------------------------------------------------------------------
/src/stores/tracker.js:
--------------------------------------------------------------------------------
1 | import { extendObservable } from 'mobx';
2 |
3 | class Tracker {
4 | static STATUS_INACTIVE = 0;
5 | static STATUS_WAITING = 1;
6 | static STATUS_QUEUED = 2;
7 | static STATUS_ACTIVE = 3;
8 |
9 | constructor(tracker) {
10 | // NOTE: tracker is already an observable!!!
11 | extendObservable(this, tracker);
12 | }
13 | }
14 |
15 | export default Tracker;
16 |
--------------------------------------------------------------------------------
/src/components/SelectableList/Item/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component} from 'react';
2 |
3 | class Item extends Component {
4 | render() {
5 | return (
6 |
7 | {this.props.children}
8 |
9 | );
10 | }
11 | }
12 |
13 | Item.propTypes = {
14 | id: React.PropTypes.number.isRequired,
15 | children: React.PropTypes.element.isRequired,
16 | };
17 |
18 | export default Item;
19 |
--------------------------------------------------------------------------------
/src/components/Torrent/StatusDetails/__tests__/Checking.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import createComponentWithIntl from 'test/util/createComponentWithIntl';
3 |
4 | import Checking from '../Checking';
5 |
6 | test('Checking', () => {
7 | const component = createComponentWithIntl(
8 |
9 | );
10 |
11 | expect(component.toJSON()).toMatchSnapshot();
12 | });
13 |
--------------------------------------------------------------------------------
/src/components/Inspector/Row.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import CSSModules from 'react-css-modules';
3 |
4 | import styles from './styles/index.css';
5 |
6 | function ActivityRow({ label, value }) {
7 | return (
8 |
9 |
{label}:
10 |
{value}
11 |
12 | );
13 | }
14 |
15 | export default CSSModules(styles)(ActivityRow);
16 |
--------------------------------------------------------------------------------
/src/components/Torrent/ProgressDetails/__tests__/Pending.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import createComponentWithIntl from 'test/util/createComponentWithIntl';
3 |
4 | import Pending from '../Pending';
5 |
6 | test('Pending', () => {
7 | const component = createComponentWithIntl(
8 |
9 | );
10 |
11 | expect(component.toJSON()).toMatchSnapshot();
12 | });
13 |
--------------------------------------------------------------------------------
/src/components/menus/ContextMenu/Menu.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import CSSModules from 'react-css-modules';
3 |
4 | import styles from './styles/index.css';
5 |
6 | function Menu({ style, children }) {
7 | return (
8 |
9 |
10 | {children}
11 |
12 |
13 | );
14 | }
15 |
16 | export default CSSModules(Menu, styles);
17 |
--------------------------------------------------------------------------------
/src/components/SelectableList/styles/index.css:
--------------------------------------------------------------------------------
1 | .list {
2 | margin: 0;
3 | padding: 0;
4 | text-align: left;
5 | list-style-type: none;
6 | }
7 |
8 | .row {
9 | border-bottom: 1px solid #ccc;
10 | background-color: white;
11 | user-select: none;
12 | align-items: center;
13 | cursor: pointer;
14 | outline: none;
15 | }
16 |
17 | .even {
18 | background-color: #F7F7F7;
19 | }
20 |
21 | .selected {
22 | background-color: #cdcdff;
23 | }
24 |
--------------------------------------------------------------------------------
/src/components/Torrent/StatusDetails/__tests__/__snapshots__/Seeding.spec.js.snap:
--------------------------------------------------------------------------------
1 | exports[`test Seeding multiple peers 1`] = `
2 |
3 | Seeding to 5 of 5 connected peers - ↑ 100 kB/s
4 |
5 | `;
6 |
7 | exports[`test Seeding none 1`] = `
8 |
9 | Seeding to 0 of 0 connected peers - ↑ 0 kB/s
10 |
11 | `;
12 |
13 | exports[`test Seeding single peers 1`] = `
14 |
15 | Seeding to 1 of 1 connected peer - ↑ 100 kB/s
16 |
17 | `;
18 |
--------------------------------------------------------------------------------
/src/components/Torrent/StatusDetails/__tests__/__snapshots__/Error.spec.js.snap:
--------------------------------------------------------------------------------
1 | exports[`test Error local error 1`] = `
2 |
3 | Error: Test error
4 |
5 | `;
6 |
7 | exports[`test Error track error 1`] = `
8 |
9 | Tracker returned an error: Test error
10 |
11 | `;
12 |
13 | exports[`test Error track warning 1`] = `
14 |
15 | Tracker returned a warning: Test error
16 |
17 | `;
18 |
19 | exports[`test Error undefined error 1`] = `null`;
20 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "4"
4 |
5 | sudo: required
6 |
7 | before_install:
8 | - sudo apt-key adv --fetch-keys http://dl.yarnpkg.com/debian/pubkey.gpg
9 | - echo "deb http://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
10 | - sudo apt-get update -qq
11 | - sudo apt-get install -y -qq yarn
12 |
13 | script:
14 | - yarn run lint -- --quiet
15 | - CI=true yarn test
16 |
17 | cache:
18 | directories:
19 | - $HOME/.cache/yarn
20 |
--------------------------------------------------------------------------------
/src/components/Torrent/ProgressDetails/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import Magnet from './Magnet';
4 | import Done from './Done';
5 | import Pending from './Pending';
6 |
7 | export default function({ torrent }) {
8 | if (torrent.needsMetaData) {
9 | return ;
10 | }
11 |
12 | // TODO: Append ETA
13 | if (torrent.isDone || torrent.isSeeding) {
14 | return ;
15 | }
16 |
17 | return ;
18 | }
19 |
--------------------------------------------------------------------------------
/src/util/converters.js:
--------------------------------------------------------------------------------
1 | export function fileToBase64(file) {
2 | return new Promise((resolve, reject) => {
3 | const reader = new FileReader();
4 |
5 | reader.onload = () => {
6 | // Remove file type chunk
7 | const [, encodedData = ''] = reader.result.split('base64,');
8 |
9 | resolve(encodedData);
10 | };
11 | reader.onabort = () => reject(reader.error);
12 | reader.onerror = () => reject(reader.error);
13 |
14 | reader.readAsDataURL(file);
15 | });
16 | }
17 |
--------------------------------------------------------------------------------
/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | Thanks for contributing to react-transmission!
2 |
3 | Here is a short checklist of additional things to keep in mind before submitting:
4 | * Please make sure your pull request description makes it very clear what you're trying to accomplish. If it's a bug fix, please also provide a failing test case (if possible). In either case, please add additional unit test coverage for your changes. :)
5 | * Run tests locally (`yarn test`) to ensure that your change did not break linting or functionality.
6 |
--------------------------------------------------------------------------------
/src/stores/__tests__/torrent-upload.test.js:
--------------------------------------------------------------------------------
1 | import TorrentUpload from '../torrent-upload';
2 |
3 | describe('TorrentUpload', () => {
4 | describe('serialize', () => {
5 | it('magnet url link should return single object', async () => {
6 | const store = new TorrentUpload();
7 | store.setTorrentUrl('url');
8 |
9 | const torrents = await store.serialize();
10 |
11 | expect(torrents.length).toBe(1);
12 | expect(torrents[0].filename).toBe('url');
13 | });
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/src/components/Torrent/StatusDetails/__tests__/__snapshots__/Downloading.spec.js.snap:
--------------------------------------------------------------------------------
1 | exports[`test Downloading from peers 1`] = `
2 |
3 | Downloading from 5 of 5 peers - ↓ 10.00 MB/s ↑ 100 kB/s
4 |
5 | `;
6 |
7 | exports[`test Downloading from peers and webseeds 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 Downloading from webseeds 1`] = `
14 |
15 | Downloading from 5 web seeds - ↓ 10.00 MB/s ↑ 100 kB/s
16 |
17 | `;
18 |
--------------------------------------------------------------------------------
/src/components/dialogs/PromptDialog/styles/index.css:
--------------------------------------------------------------------------------
1 | .body {
2 | composes: body from "../../Dialog/styles/index.css";
3 | flex-direction: row;
4 | padding: 20px;
5 | }
6 |
7 | .logo {
8 | composes: logo from "../../Dialog/styles/index.css";
9 | }
10 |
11 | .content {
12 | display: flex;
13 | flex-direction: column;
14 | flex: 1;
15 | }
16 |
17 | .body section.buttons {
18 | display: flex;
19 | justify-content: flex-end;
20 | padding-top: 10px;
21 | }
22 |
23 | .body section.buttons button {
24 | margin-left: 10px;
25 | }
26 |
--------------------------------------------------------------------------------
/src/components/menus/ContextMenu/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Overlay } from 'react-overlays';
3 |
4 | import Menu from './Menu';
5 |
6 | function ContextMenu(props) {
7 | // TODO: Container should be App element
8 | return (
9 |
17 |
18 |
19 | );
20 | }
21 |
22 | export default ContextMenu;
23 |
--------------------------------------------------------------------------------
/src/components/Inspector/Peers/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import CSSModules from 'react-css-modules';
3 |
4 | import PeerGroup from './PeerGroup';
5 |
6 | import styles from './styles/index.css';
7 |
8 | function Peers({ info }) {
9 | return (
10 |
11 | {info.peers.map(({ name, peers }, index) => (
12 |
13 | {info.peers.length > 1 &&
{name}
}
14 |
15 |
16 | ))}
17 |
18 | );
19 | }
20 |
21 | export default CSSModules(styles)(Peers);
22 |
--------------------------------------------------------------------------------
/src/components/Torrent/StatusDetails/Checking.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { FormattedMessage, injectIntl } from 'react-intl';
3 |
4 | function Checking({ torrent, intl }) {
5 | const { recheckProgress } = torrent;
6 |
7 | return (
8 |
15 | );
16 | }
17 |
18 | export default injectIntl(Checking);
19 |
--------------------------------------------------------------------------------
/src/components/Torrent/ProgressDetails/__tests__/__snapshots__/index.spec.js.snap:
--------------------------------------------------------------------------------
1 | exports[`test ProgressDetails done 1`] = `
2 |
3 | 100.0 kB, uploaded 100.0 kB (Ratio: 2.00)
4 |
5 | `;
6 |
7 | exports[`test ProgressDetails magnet 1`] = `
8 |
9 | Magnetized transfer - retrieving metadata (75%)
10 |
11 | `;
12 |
13 | exports[`test ProgressDetails pending 1`] = `
14 |
15 | 90.0 kB of 100.0 kB (90%)
16 |
17 | `;
18 |
19 | exports[`test ProgressDetails seeding 1`] = `
20 |
21 | 100.0 kB, uploaded 100.0 kB (Ratio: 2.00)
22 |
23 | `;
24 |
--------------------------------------------------------------------------------
/src/components/Inspector/Trackers/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import CSSModules from 'react-css-modules';
3 |
4 | import TrackerGroup from './TrackerGroup';
5 |
6 | import styles from './styles/index.css';
7 |
8 | function Trackers({ info }) {
9 | return (
10 |
11 | {info.trackers.map(({ name, trackers }, index) => (
12 |
13 | {info.trackers.length > 1 &&
{name}
}
14 |
15 |
16 | ))}
17 |
18 | );
19 | }
20 |
21 | export default CSSModules(styles)(Trackers);
22 |
--------------------------------------------------------------------------------
/src/components/dialogs/Dialog/Header/styles/index.css:
--------------------------------------------------------------------------------
1 | .header {
2 | display: flex;
3 | background: #ddd;
4 | padding: 8px;
5 | align-items: center;
6 | user-select: none;
7 | cursor: default;
8 | }
9 |
10 | .title {
11 | flex: 1;
12 | font-size: 1.2em;
13 | padding: 0px;
14 | color: #666;
15 | margin: 0;
16 | }
17 |
18 | .close {
19 | border-radius: 5px;
20 | border: 1px solid #bbb;
21 | width: 22px;
22 | height: 22px;
23 | cursor: pointer;
24 | outline: none;
25 | font-weight: bold;
26 | }
27 |
28 | .close:hover {
29 | border: 1px solid #888;
30 | background: #e8e8e8;
31 | }
32 |
--------------------------------------------------------------------------------
/src/components/dialogs/Dialog/Header/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('view_store')
8 | @observer
9 | @CSSModules(styles)
10 | class Header extends Component {
11 | render() {
12 | return (
13 |
14 | { this.props.title }
15 |
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 |
8 | `;
9 |
10 | exports[`test ProgressBar magnet 1`] = `
11 |
17 | `;
18 |
19 | exports[`test ProgressBar paused 1`] = `
20 |
26 | `;
27 |
28 | exports[`test ProgressBar seeding 1`] = `
29 |
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 |
25 | );
26 | }
27 | }
28 |
29 | export default StatusButton;
30 |
--------------------------------------------------------------------------------
/src/components/dialogs/PreferencesDialog/fields/CheckRow/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 CheckRow extends Component {
11 | render() {
12 | const check = this.props.session_store.settings[this.props.id];
13 |
14 | return (
15 |
16 |
17 |
18 |
19 | );
20 | }
21 | }
22 |
23 | export default CheckRow;
24 |
--------------------------------------------------------------------------------
/src/components/DropzoneLayer/styles/index.css:
--------------------------------------------------------------------------------
1 | .container .dropzoneContainer {
2 | visibility: hidden;
3 | position: fixed;
4 | width: 100%;
5 | height: 100%;
6 | left: 0;
7 | top: 0;
8 | }
9 |
10 | .container.activeContainer .dropzoneContainer {
11 | z-index: 1000;
12 | visibility: visible;
13 | }
14 |
15 | .container.activeContainer .childrenContainer {
16 | opacity: 0.3;
17 | }
18 |
19 | .dropzoneContent {
20 | display: flex;
21 | align-items: center;
22 | justify-content: center;
23 | margin: 100px;
24 | border: 2px dashed rgb(100, 100, 100);
25 | border-width: 3px;
26 | border-style: dashed;
27 | padding: 100px;
28 | }
29 |
30 | .description {
31 | flex: 1;
32 | font-size: 30px;
33 | color: rgb(100, 100, 100);
34 | margin-left: 10px;
35 | max-width: 500px;
36 | }
37 |
--------------------------------------------------------------------------------
/src/stores/index.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable camelcase */
2 | import { useStrict } from 'mobx';
3 |
4 | import RPC from 'util/rpc';
5 | import ViewStore from './view-store';
6 | import TorrentStore from './torrent-store';
7 | import StatsStore from './stats-store';
8 | import SessionStore from './session-store';
9 |
10 | // Force strict mode so mutations are only allowed within actions.
11 | useStrict(true);
12 |
13 | export const view_store = new ViewStore();
14 |
15 | const onConnect = () => view_store.toggleConnectionDialog(false);
16 | const onDisconnect = () => view_store.toggleConnectionDialog(true);
17 |
18 | const rpc = new RPC(onConnect, onDisconnect);
19 |
20 | export const torrents_store = new TorrentStore(rpc);
21 | export const stats_store = new StatsStore(rpc);
22 | export const session_store = new SessionStore(rpc);
23 |
--------------------------------------------------------------------------------
/src/util/common.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Given a numerator and denominator, return a ratio string
3 | */
4 | Math.ratio = function(numerator, denominator) {
5 | var result = Math.floor(100 * numerator / denominator) / 100;
6 |
7 | // check for special cases
8 | if (result === Number.POSITIVE_INFINITY || result === Number.NEGATIVE_INFINITY) result = -2;
9 | else if (isNaN(result)) result = -1;
10 |
11 | return result;
12 | };
13 |
14 | /**
15 | * Round a string of a number to a specified number of decimal places
16 | */
17 | export function toTruncFixed(number, place) {
18 | var ret = Math.floor(number * Math.pow (10, place)) / Math.pow(10, place);
19 | return ret.toFixed(place);
20 | }
21 |
22 | export function toStringWithCommas(number) {
23 | return number.toString().replace(/\B(?=(?:\d{3})+(?!\d))/g, ',');
24 | };
25 |
--------------------------------------------------------------------------------
/src/components/dialogs/Dialog/styles/index.css:
--------------------------------------------------------------------------------
1 | .modalStyle {
2 | position: fixed;
3 | top: 0;
4 | bottom: 0;
5 | left: 0;
6 | right: 0;
7 | z-index: 1040;
8 | }
9 |
10 | .dialogStyle {
11 | position: absolute;
12 | top: 50%;
13 | left: 50%;
14 | transform: translate(-50%, -50%);
15 | border: 1px solid #e5e5e5;
16 | background-color: #fff;
17 | box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);
18 | border-radius: 3px;
19 | }
20 |
21 | .dialogStyle:focus {
22 | outline: none;
23 | }
24 | .body {
25 | display: flex;
26 | flex-direction: column;
27 | color: black;
28 | min-width: 400px;
29 | background-color: #fefefe;
30 | }
31 | .body h2 {
32 | font-size: 1.2em;
33 | padding: 10px;
34 | background: #ddd;
35 | color: #666;
36 | margin: 0;
37 | }
38 | .logo {
39 | width: 64px;
40 | height: 64px;
41 | margin: 020px;
42 | }
43 |
--------------------------------------------------------------------------------
/src/components/Torrent/StatusButton/styles/index.css:
--------------------------------------------------------------------------------
1 | .statusButton {
2 | width: 14px;
3 | height: 14px;
4 | margin-left: 8px;
5 | background: url(images/buttons/torrent_buttons.png);
6 | border: 0;
7 | padding: 0;
8 | cursor: pointer;
9 | }
10 |
11 | .statusButton:focus {
12 | border: 0;
13 | outline: none;
14 | }
15 |
16 | .statusPause {
17 | composes: statusButton;
18 |
19 | background-position: left top;
20 | }
21 |
22 | .statusPause:hover {
23 | background-position: left center;
24 | }
25 |
26 | .statusPause:active {
27 | background-position: left bottom;
28 | }
29 |
30 | .statusResume {
31 | composes: statusButton;
32 |
33 | background-position: center top;
34 | }
35 |
36 | .statusResume:hover {
37 | background-position: center center;
38 | }
39 |
40 | .statusResume:active {
41 | background-position: center bottom;
42 | }
43 |
--------------------------------------------------------------------------------
/src/components/menus/TorrentContextMenu/styles/index.css:
--------------------------------------------------------------------------------
1 | .torrentMenu {
2 | bottom: 18px;
3 | min-width: 210px;
4 | background-color: white;
5 | padding: 5px 0;
6 | text-align: left;
7 | list-style: none;
8 | -webkit-border-radius: 5px;
9 | -webkit-box-shadow: 0 10px 25px rgba(0, 0, 0, 0.4);
10 | }
11 |
12 | .torrentMenuItem {
13 | margin: 0px;
14 | color: rgb(0, 0, 0);
15 | cursor: default;
16 | padding: 3px 10px 3px 20px;
17 | }
18 |
19 | .torrentMenuItemDisabled {
20 | composes: torrentMenuItem;
21 | color: rgb(170, 170, 170);
22 | pointer-events: none;
23 | }
24 |
25 | .torrentMenuItem:hover {
26 | color: rgb(255, 255, 255);
27 | background-color: rgb(34, 68, 238);
28 | }
29 |
30 | .torrentMenuSeparator {
31 | composes: torrentMenuItem;
32 |
33 | border-top: 1px solid #ddd;
34 | margin: 5px 0;
35 | padding: 0px;
36 | }
37 |
--------------------------------------------------------------------------------
/src/components/Inspector/Files/styles/index.css:
--------------------------------------------------------------------------------
1 | .fileList {
2 | list-style: none;
3 | padding: 0px;
4 | margin: 0px;
5 | border-collapse: collapse;
6 | margin-left: 16px;
7 | }
8 |
9 | .fileRow {
10 | display: flex;
11 | align-items: center;
12 | padding-bottom: 5px;
13 | }
14 |
15 | .fileRowContainer {
16 | width: 100%;
17 | }
18 |
19 | .name {
20 | flex: 1;
21 | padding-left: 5px;
22 | }
23 |
24 | .priorityButton {
25 | display: flex;
26 | }
27 |
28 | .priorityButton button {
29 | border: 1px solid #aaa;
30 | padding: 5px 10px;
31 | font-weight: bold;
32 | }
33 |
34 | .priorityButton button.selected {
35 | background-image: -webkit-linear-gradient(top, #cdcdff, white);
36 | }
37 |
38 | .priorityButton button:first-child {
39 | border-radius: 5px 0px 0px 5px;
40 | }
41 |
42 | .priorityButton button:last-child {
43 | border-radius: 0px 5px 5px 0px;
44 | }
45 |
--------------------------------------------------------------------------------
/src/components/App/styles/index.css:
--------------------------------------------------------------------------------
1 | @import "normalize.css";
2 |
3 | :global body {
4 | margin: 0px;
5 | padding: 0px;
6 |
7 | font: 62.5% "lucida grande", Tahoma, Verdana, Arial, Helvetica, sans-serif;
8 | height: 100vh;
9 | }
10 |
11 |
12 | :global #root {
13 | height: 100vh;
14 | }
15 |
16 | .container {
17 | position: relative;
18 | display: flex;
19 | flex-direction: column;
20 | height: 100vh;
21 | }
22 |
23 | .header {
24 | flex-shrink: 0;
25 | flex-basis: auto;
26 | }
27 |
28 | .main {
29 | display: flex;
30 | flex-direction: row;
31 | align-items: stretch;
32 | flex: 1;
33 | }
34 |
35 | .footer {
36 | flex-shrink: 0;
37 | flex-basis: auto;
38 | }
39 |
40 | .list {
41 | flex: 2;
42 | overflow-y: auto;
43 | background: #fffce8;
44 | }
45 |
46 | .details {
47 | min-width: 600px;
48 | flex: 1;
49 | overflow-y: auto;
50 | border-left: 1px solid #888;
51 | }
52 |
--------------------------------------------------------------------------------
/src/util/notifications.js:
--------------------------------------------------------------------------------
1 | import logoImage from 'images/logo.png';
2 |
3 | const CLOSE_TIMEOUT = 5000;
4 |
5 | export function buildTorrentAddedNotification(torrent) {
6 | return {
7 | title: 'Torrent Added',
8 | options: {
9 | body: torrent.name,
10 | icon: logoImage,
11 | tag: 'torrent_added',
12 | },
13 | };
14 | }
15 |
16 | export function buildTorrentCompletedNotification(torrent) {
17 | return {
18 | title: 'Torrent Completed',
19 | options: {
20 | body: torrent.name,
21 | icon: logoImage,
22 | tag: 'torrent_completed',
23 | },
24 | };
25 | }
26 |
27 | export function showNotification({ title, options }) {
28 | const notification = new Notification(title, options);
29 |
30 | notification.onclick = () => {
31 | window.focus();
32 | notification.close();
33 | };
34 |
35 | setTimeout(() => notification.close(), CLOSE_TIMEOUT);
36 | }
37 |
--------------------------------------------------------------------------------
/src/components/Torrent/Compact.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import CSSModules from 'react-css-modules';
3 | import { inject, observer } from 'mobx-react';
4 |
5 | import ProgressBar from './ProgressBar';
6 |
7 | import { getPeerDetailsShort } from './services';
8 |
9 | import styles from './styles/index.css';
10 |
11 | @inject('view_store')
12 | @observer
13 | @CSSModules(styles)
14 | class Compact extends Component {
15 | render() {
16 | const torrent = this.props.torrent;
17 |
18 | return (
19 |
20 |
21 | {torrent.name}
22 |
23 |
24 | {getPeerDetailsShort(torrent)}
25 |
26 |
29 |
30 | );
31 | }
32 | }
33 |
34 | export default Compact;
35 |
--------------------------------------------------------------------------------
/src/components/Torrent/StatusDetails/__tests__/Seeding.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import createComponentWithIntl from 'test/util/createComponentWithIntl';
3 |
4 | import Seeding from '../Seeding';
5 |
6 | test('Seeding none', () => {
7 | const component = createComponentWithIntl(
8 |
9 | );
10 |
11 | expect(component.toJSON()).toMatchSnapshot();
12 | });
13 |
14 | test('Seeding single peers', () => {
15 | const component = createComponentWithIntl(
16 |
17 | );
18 |
19 | expect(component.toJSON()).toMatchSnapshot();
20 | });
21 |
22 | test('Seeding multiple peers', () => {
23 | const component = createComponentWithIntl(
24 |
25 | );
26 |
27 | expect(component.toJSON()).toMatchSnapshot();
28 | });
29 |
--------------------------------------------------------------------------------
/src/components/Torrent/StatusButton/__tests__/index.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import renderer from 'react-test-renderer';
3 | import { shallow } from 'enzyme';
4 |
5 | import StatusButton from '../';
6 |
7 | test('StatusButton resume', () => {
8 | const component = renderer.create(
9 |
10 | );
11 |
12 | expect(component.toJSON()).toMatchSnapshot();
13 | });
14 |
15 | test('StatusButton pause', () => {
16 | const component = renderer.create(
17 |
18 | );
19 |
20 | expect(component.toJSON()).toMatchSnapshot();
21 | });
22 |
23 | test('StatusButton onToggle', () => {
24 | const toggleStatus = jest.fn();
25 |
26 | const component = shallow(
27 |
28 | );
29 |
30 | component.find('button').simulate('click');
31 |
32 | expect(toggleStatus).toHaveBeenCalledWith(999);
33 | });
34 |
--------------------------------------------------------------------------------
/src/components/dialogs/PreferencesDialog/NetworkTabPanel/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component} from 'react';
2 | import { inject, observer } from 'mobx-react';
3 |
4 | import TextRow from '../fields/TextRow';
5 | import PortTestRow from '../fields/PortTestRow';
6 | import CheckRow from '../fields/CheckRow';
7 |
8 | @inject('view_store', 'session_store')
9 | @observer
10 | class NetworkTabPanel extends Component {
11 | render() {
12 | return (
13 |
14 |
Listening port
15 |
16 |
17 |
18 |
19 |
20 |
21 |
Options
22 |
23 |
24 | );
25 | }
26 | }
27 |
28 | export default NetworkTabPanel;
29 |
--------------------------------------------------------------------------------
/src/components/Torrent/ProgressDetails/Magnet.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
3 |
4 | const messages = defineMessages({
5 | needs: {
6 | id: 'torrent.progress.magnet.needs',
7 | defaultMessage: 'needs',
8 | },
9 | retrieving: {
10 | id: 'torrent.progress.magnet.retrieving',
11 | defaultMessage: 'retrieving',
12 | },
13 | });
14 |
15 | function ProgressMagnet({ torrent, intl }) {
16 | const metadataStatus = torrent.isStopped ? messages.needs : messages.retrieving;
17 |
18 | return (
19 |
27 | );
28 | }
29 |
30 | export default injectIntl(ProgressMagnet);
31 |
--------------------------------------------------------------------------------
/src/components/Inspector/Details.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { parseUri } from 'util/uri';
4 |
5 | import DetailsRow from './Row';
6 |
7 | function formatComment(comment) {
8 | const uri = parseUri(comment);
9 |
10 | if (uri.protocol === 'http' || uri.parseUri === 'https') {
11 | const encodedUri = encodeURI(uri);
12 |
13 | return (
14 | {encodedUri}
15 | );
16 | }
17 |
18 | return comment;
19 | }
20 |
21 | function Details({ info }) {
22 | return (
23 |
24 |
Details
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | );
33 | }
34 |
35 | export default Details;
36 |
--------------------------------------------------------------------------------
/src/components/Inspector/Activity.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { FormattedMessage } from 'react-intl';
3 |
4 | import ActivityRow from './Row';
5 |
6 | function Activity({ info }) {
7 | return (
8 |
9 |
10 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | );
26 | }
27 |
28 | export default Activity;
29 |
--------------------------------------------------------------------------------
/src/components/Inspector/Files/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import CSSModules from 'react-css-modules';
3 | import { inject } from 'mobx-react';
4 |
5 | import { generateTree } from './services/generate-tree';
6 | import FileRow from './FileRow';
7 |
8 | import styles from './styles/index.css';
9 |
10 | function Files({ info, torrents_store }) {
11 | // TODO: Generate tree for each torrent files
12 | const torrentId = info.torrents[0].id;
13 | const tree = generateTree(info.files[0].files);
14 | const rootKey = Object.keys(tree)[0];
15 |
16 | return (
17 |
18 |
22 | torrents_store.setWanted(torrentId, wanted, fileIds)
23 | }
24 | setPriority={({ fileIds, priority }) =>
25 | torrents_store.setPriority(torrentId, priority, fileIds)
26 | }
27 | />
28 |
29 | );
30 | }
31 |
32 | export default inject('torrents_store')(CSSModules(styles)(Files));
33 |
--------------------------------------------------------------------------------
/src/components/dialogs/PreferencesDialog/fields/CheckValueRow/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 CheckValueRow extends Component {
11 | render() {
12 | const check = this.props.session_store.settings[this.props.idCheck];
13 | const value = this.props.session_store.settings[this.props.idValue];
14 |
15 | return (
16 |
25 | );
26 | }
27 | }
28 |
29 | export default CheckValueRow;
30 |
--------------------------------------------------------------------------------
/src/components/Inspector/Files/WantedButton.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import CSSModules from 'react-css-modules';
3 | import autobind from 'autobind-decorator';
4 |
5 | import styles from './styles/index.css';
6 |
7 | class WantedButton extends Component {
8 | constructor(props) {
9 | super(props);
10 |
11 | this.state = {
12 | checked: props.wanted,
13 | };
14 | }
15 |
16 | componentWillReceiveProps(nextProps) {
17 | this.setState({checked: nextProps.wanted});
18 | }
19 |
20 | @autobind onChange() {
21 | this.setState(
22 | (prevState) => ({checked: !prevState.checked}),
23 | () => this.props.setWanted({
24 | fileIds: this.props.fileIds,
25 | wanted: this.state.checked ? 'wanted' : 'unwanted',
26 | })
27 | );
28 | }
29 |
30 | render() {
31 | return (
32 |
37 | );
38 | }
39 | }
40 |
41 | export default CSSModules(styles)(WantedButton);
42 |
--------------------------------------------------------------------------------
/src/components/Torrent/StatusDetails/__tests__/__snapshots__/Status.spec.js.snap:
--------------------------------------------------------------------------------
1 | exports[`test Status check 1`] = `
2 |
3 | Verifying local data
4 |
5 | `;
6 |
7 | exports[`test Status check wait 1`] = `
8 |
9 | Queued for verification
10 |
11 | `;
12 |
13 | exports[`test Status download 1`] = `
14 |
15 | Downloading
16 |
17 | `;
18 |
19 | exports[`test Status download wait 1`] = `
20 |
21 | Queued for download
22 |
23 | `;
24 |
25 | exports[`test Status other 1`] = `
26 |
27 | Error
28 |
29 | `;
30 |
31 | exports[`test Status seed 1`] = `
32 |
33 | Seeding
34 |
35 | `;
36 |
37 | exports[`test Status seed wait 1`] = `
38 |
39 | Queued for seeding
40 |
41 | `;
42 |
43 | exports[`test Status stopped finished 1`] = `
44 |
45 | Finished
46 |
47 | `;
48 |
49 | exports[`test Status stopped not finished 1`] = `
50 |
51 | Paused
52 |
53 | `;
54 |
55 | exports[`test Status unknown 1`] = `
56 |
57 | Unknown
58 |
59 | `;
60 |
--------------------------------------------------------------------------------
/src/components/Inspector/Files/FileRow.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import CSSModules from 'react-css-modules';
3 |
4 | import FileList from './FileList';
5 | import PriorityButton from './PriorityButton';
6 | import WantedButton from './WantedButton';
7 |
8 | import styles from './styles/index.css';
9 |
10 | function FileRow({ name, node, setPriority, setWanted }) {
11 | const { priority, fileIds, entries } = node;
12 | const wanted = !!node.wanted;
13 |
14 | return (
15 |
16 |
17 |
22 |
{name}
23 |
28 |
29 | {entries &&
}
30 |
31 | );
32 | }
33 |
34 | export default CSSModules(styles)(FileRow);
35 |
--------------------------------------------------------------------------------
/src/components/Torrent/ProgressBar/__tests__/index.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import renderer from 'react-test-renderer';
3 | import ProgressBar from '../';
4 |
5 | test('ProgressBar seeding', () => {
6 | const component = renderer.create(
7 |
8 | );
9 |
10 | expect(component.toJSON()).toMatchSnapshot();
11 | });
12 |
13 | test('ProgressBar leeching', () => {
14 | const component = renderer.create(
15 |
16 | );
17 |
18 | expect(component.toJSON()).toMatchSnapshot();
19 | });
20 |
21 | test('ProgressBar paused', () => {
22 | const component = renderer.create(
23 |
24 | );
25 |
26 | expect(component.toJSON()).toMatchSnapshot();
27 | });
28 |
29 | test('ProgressBar magnet', () => {
30 | const component = renderer.create(
31 |
32 | );
33 |
34 | expect(component.toJSON()).toMatchSnapshot();
35 | });
36 |
--------------------------------------------------------------------------------
/src/components/menus/RateContextMenu/styles/index.css:
--------------------------------------------------------------------------------
1 | .torrentMenu {
2 | bottom: 18px;
3 | min-width: 210px;
4 | background-color: white;
5 | padding: 5px 0;
6 | text-align: left;
7 | list-style: none;
8 | -webkit-border-radius: 5px;
9 | -webkit-box-shadow: 0 10px 25px rgba(0, 0, 0, 0.4);
10 | }
11 |
12 | .torrentMenuItem {
13 | margin: 0px;
14 | color: rgb(0, 0, 0);
15 | cursor: default;
16 | padding: 3px 10px 3px 20px;
17 | }
18 |
19 | .torrentMenuSelected {
20 | composes: torrentMenuItem;
21 | }
22 |
23 | .torrentMenuSelected:before {
24 | content: "✔";
25 | margin-left: -14px;
26 | margin-right: 6px;
27 | }
28 |
29 | .torrentMenuItemDisabled {
30 | composes: torrentMenuItem;
31 | color: rgb(170, 170, 170);
32 | pointer-events: none;
33 | }
34 |
35 | .torrentMenuItem:hover {
36 | color: rgb(255, 255, 255);
37 | background-color: rgb(34, 68, 238);
38 | }
39 |
40 | .torrentMenuSeparator {
41 | composes: torrentMenuItem;
42 |
43 | border-top: 1px solid #ddd;
44 | margin: 5px 0;
45 | padding: 0px;
46 | }
47 |
48 | .torrentMenuSeparator:before {
49 | content: none;
50 | }
51 |
--------------------------------------------------------------------------------
/src/components/menus/SortByContextMenu/styles/index.css:
--------------------------------------------------------------------------------
1 | .torrentMenu {
2 | bottom: 18px;
3 | min-width: 210px;
4 | background-color: white;
5 | padding: 5px 0;
6 | text-align: left;
7 | list-style: none;
8 | -webkit-border-radius: 5px;
9 | -webkit-box-shadow: 0 10px 25px rgba(0, 0, 0, 0.4);
10 | }
11 |
12 | .torrentMenuItem {
13 | margin: 0px;
14 | color: rgb(0, 0, 0);
15 | cursor: default;
16 | padding: 3px 10px 3px 20px;
17 | }
18 |
19 | .torrentMenuSelected {
20 | composes: torrentMenuItem;
21 | }
22 |
23 | .torrentMenuSelected:before {
24 | content: "✔";
25 | margin-left: -14px;
26 | margin-right: 6px;
27 | }
28 |
29 | .torrentMenuItemDisabled {
30 | composes: torrentMenuItem;
31 | color: rgb(170, 170, 170);
32 | pointer-events: none;
33 | }
34 |
35 | .torrentMenuItem:hover {
36 | color: rgb(255, 255, 255);
37 | background-color: rgb(34, 68, 238);
38 | }
39 |
40 | .torrentMenuSeparator {
41 | composes: torrentMenuItem;
42 |
43 | border-top: 1px solid #ddd;
44 | margin: 5px 0;
45 | padding: 0px;
46 | }
47 |
48 | .torrentMenuSeparator:before {
49 | content: none;
50 | }
51 |
--------------------------------------------------------------------------------
/scripts/test.js:
--------------------------------------------------------------------------------
1 | process.env.NODE_ENV = 'test';
2 | process.env.PUBLIC_URL = '';
3 |
4 | // Load environment variables from .env file. Suppress warnings using silent
5 | // if this file is missing. dotenv will never modify any environment variables
6 | // that have already been set.
7 | // https://github.com/motdotla/dotenv
8 | require('dotenv').config({silent: true});
9 |
10 | const jest = require('jest');
11 | const argv = process.argv.slice(2);
12 |
13 | // Watch unless on CI or in coverage mode
14 | if (!process.env.CI && argv.indexOf('--coverage') < 0) {
15 | argv.push('--watch');
16 | }
17 |
18 | // A temporary hack to clear terminal correctly.
19 | // You can remove this after updating to Jest 18 when it's out.
20 | // https://github.com/facebook/jest/pull/2230
21 | var realWrite = process.stdout.write;
22 | var CLEAR = process.platform === 'win32' ? '\x1Bc' : '\x1B[2J\x1B[3J\x1B[H';
23 | process.stdout.write = function(chunk, encoding, callback) {
24 | if (chunk === '\x1B[2J\x1B[H') {
25 | chunk = CLEAR;
26 | }
27 | return realWrite.call(this, chunk, encoding, callback);
28 | };
29 |
30 |
31 | jest.run(argv);
32 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 Ferran Basora
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/components/Torrent/styles/index.css:
--------------------------------------------------------------------------------
1 | .torrent {
2 | color: #666;
3 | padding: 5px 10px;
4 | }
5 |
6 | .torrentCompact {
7 | color: #666;
8 | padding: 7px;
9 | display: flex;
10 | align-items: center;
11 | }
12 |
13 | .name {
14 | font-size: 1.3em;
15 | font-weight: bold;
16 | overflow: hidden;
17 | text-overflow: ellipsis;
18 | white-space: nowrap;
19 | color: #222;
20 | margin-top: 2px;
21 | margin-bottom: 2px;
22 | }
23 |
24 | .nameCompact {
25 | flex: 1;
26 | }
27 |
28 | .peerDetails {
29 | clear: left;
30 | overflow: hidden;
31 | text-overflow: ellipsis;
32 | white-space: nowrap;
33 | margin-bottom: 2px;
34 | }
35 |
36 | .peerDetailsError {
37 | composes: peerDetails;
38 |
39 | color: #F00;
40 | }
41 |
42 | .detailsCompact {
43 | margin-right: 10px;
44 | }
45 |
46 | .progressBarRow {
47 | display: flex;
48 | align-items: center;
49 | }
50 |
51 | .progressBarRow div {
52 | flex: 1;
53 | }
54 |
55 | .progressBarRowCompact {
56 | width: 50px;
57 | }
58 |
59 | .progressDetails {
60 | clear: left;
61 | overflow: hidden;
62 | text-overflow: ellipsis;
63 | white-space: nowrap;
64 | }
65 |
--------------------------------------------------------------------------------
/src/components/Inspector/Trackers/styles/index.css:
--------------------------------------------------------------------------------
1 | .trackerList {
2 | list-style: none;
3 | padding: 0px;
4 | margin: 0px;
5 | width: 100%;
6 | border-collapse: collapse;
7 | }
8 |
9 | .tierRow {
10 | }
11 |
12 | .trackerRow {
13 | margin: 2px 0 8px 0;
14 | padding: 3px 0 3px 2px;
15 | }
16 |
17 | .tierRow:nth-child(even) {
18 | background-color: #EEEEEE;
19 | }
20 |
21 | .trackerInfo {
22 | display: flex;
23 | justify-content: space-between;
24 | }
25 |
26 | .trackerActivity {
27 | color: #666;
28 | }
29 |
30 | .trackerActivityRow {
31 | margin: 0;
32 | padding: 2px;
33 | }
34 |
35 | .trackerHost {
36 | font-size: 1.2em;
37 | font-weight: bold;
38 | color: #222;
39 | }
40 |
41 | .trackerTier {
42 | margin: 0;
43 | padding: 2px;
44 | background-color: white;
45 | }
46 |
47 | .trackerStats {
48 | color: #666;
49 | width: 150px;
50 | margin: 0;
51 | padding-right: 3px;
52 | }
53 |
54 | .trackerStatsRow {
55 | padding: 1px;
56 | display: flex;
57 | }
58 |
59 | .trackerStatsTerm {
60 | flex: 3;
61 | font-weight: bold;
62 | text-align: right;
63 | }
64 |
65 | .trackerStatsDescription {
66 | flex: 1;
67 | margin-left: 3px;
68 | }
69 |
--------------------------------------------------------------------------------
/src/components/dialogs/PreferencesDialog/fields/SelectRow/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 SelectRow extends Component {
11 | render() {
12 | const value = this.props.session_store.settings[this.props.id];
13 |
14 | const options = Object.keys(this.props.options).map((key) => {
15 | return {
16 | key: key,
17 | value: this.props.options[key],
18 | };
19 | });
20 |
21 | return (
22 |
23 |
24 |
25 |
26 |
27 |
32 |
33 |
34 | );
35 | }
36 | }
37 |
38 | export default SelectRow;
39 |
--------------------------------------------------------------------------------
/src/components/dialogs/PreferencesDialog/TorrentsTabPanel/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 | import TextRow from '../fields/TextRow';
8 | import CheckRow from '../fields/CheckRow';
9 | import CheckValueRow from '../fields/CheckValueRow';
10 |
11 | @inject('view_store')
12 | @observer
13 | @CSSModules(styles)
14 | class TorrentsTabPanel extends Component {
15 | render() {
16 | return (
17 |
18 |
Downloading
19 |
20 |
21 |
22 |
23 | Seeding
24 |
25 |
26 |
27 | );
28 | }
29 | }
30 |
31 | export default TorrentsTabPanel;
32 |
--------------------------------------------------------------------------------
/src/components/dialogs/Dialog/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component} from 'react';
2 | import { Modal } from 'react-overlays';
3 | import CSSModules from 'react-css-modules';
4 | import { inject, observer } from 'mobx-react';
5 |
6 | import Header from './Header';
7 |
8 | import styles from './styles/index.css';
9 |
10 | @inject('view_store')
11 | @observer
12 | @CSSModules(styles)
13 | class Dialog extends Component {
14 | render() {
15 | // TODO; try to investigate how to move this to css module
16 | const backdropStyle = {
17 | position: 'fixed',
18 | top: 0,
19 | bottom: 0,
20 | left: 0,
21 | right: 0,
22 | zIndex: 'auto',
23 | backgroundColor: '#000',
24 | opacity: 0.2,
25 | };
26 |
27 | return (
28 |
34 |
35 |
36 | {this.props.children}
37 |
38 |
39 | );
40 | }
41 | }
42 |
43 | export default Dialog;
44 |
--------------------------------------------------------------------------------
/config/env.js:
--------------------------------------------------------------------------------
1 | // Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be
2 | // injected into the application via DefinePlugin in Webpack configuration.
3 |
4 | var REACT_APP = /^REACT_APP_/i;
5 |
6 | function getClientEnvironment(publicUrl) {
7 | var processEnv = Object
8 | .keys(process.env)
9 | .filter(key => REACT_APP.test(key))
10 | .reduce((env, key) => {
11 | env[key] = JSON.stringify(process.env[key]);
12 | return env;
13 | }, {
14 | // Useful for determining whether we’re running in production mode.
15 | // Most importantly, it switches React into the correct mode.
16 | 'NODE_ENV': JSON.stringify(
17 | process.env.NODE_ENV || 'development'
18 | ),
19 | // Useful for resolving the correct path to static assets in `public`.
20 | // For example,
.
21 | // This should only be used as an escape hatch. Normally you would put
22 | // images into the `src` and `import` them in code to get their paths.
23 | 'PUBLIC_URL': JSON.stringify(publicUrl)
24 | });
25 | return {'process.env': processEnv};
26 | }
27 |
28 | module.exports = getClientEnvironment;
29 |
--------------------------------------------------------------------------------
/src/components/menus/SettingsContextMenu/styles/index.css:
--------------------------------------------------------------------------------
1 | .torrentMenu {
2 | bottom: 18px;
3 | min-width: 210px;
4 | background-color: white;
5 | padding: 5px 0;
6 | text-align: left;
7 | list-style: none;
8 | -webkit-border-radius: 5px;
9 | -webkit-box-shadow: 0 10px 25px rgba(0, 0, 0, 0.4);
10 | }
11 |
12 | .torrentMenuItem {
13 | margin: 0px;
14 | color: rgb(0, 0, 0);
15 | cursor: default;
16 | padding: 3px 10px 3px 20px;
17 | }
18 |
19 | .torrentMenuSubitem {
20 | composes: torrentMenuItem;
21 | }
22 |
23 | .torrentMenuSubitem:after {
24 | content: "►";
25 | float: right;
26 | }
27 |
28 | .torrentMenuSelected {
29 | composes: torrentMenuItem;
30 | }
31 |
32 | .torrentMenuSelected:after {
33 | content: none;
34 | }
35 |
36 | .torrentMenuItem:hover {
37 | color: rgb(255, 255, 255);
38 | background-color: rgb(34, 68, 238);
39 | }
40 |
41 | .torrentMenuSeparator {
42 | composes: torrentMenuItem;
43 |
44 | border-top: 1px solid #ddd;
45 | margin: 5px 0;
46 | padding: 0px;
47 | }
48 |
49 | .torrentMenuItem a {
50 | text-decoration: none;
51 | color: rgb(0, 0, 0);
52 | }
53 |
54 | .torrentMenuItem a:visited {
55 | color: rgb(0, 0, 0);
56 | }
57 |
58 | .torrentMenuItem:hover a {
59 | color: rgb(255, 255, 255);
60 | }
61 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
16 | Transmission Web Interface
17 |
18 |
19 |
20 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/src/components/toolbars/ActionToolbar/styles/index.css:
--------------------------------------------------------------------------------
1 | .toolbar {
2 | background: #ccc;
3 |
4 | display: -ms-flexbox;
5 | display: -webkit-flex;
6 | display: flex;
7 |
8 | -ms-flex-align: center;
9 | -webkit-align-items: center;
10 | -webkit-box-align: center;
11 |
12 | align-items: center;
13 |
14 | border-bottom: 1px solid #AAA;
15 | background-color: #cccccc;
16 | background-image: -webkit-gradient(linear, left top, left bottom, from(#dddddd), to(#bbbbbb));
17 | background-image: -webkit-linear-gradient(top, #dddddd, #bbbbbb);
18 | background-image: -moz-linear-gradient(top, #dddddd, #bbbbbb);
19 | background-image: -ms-linear-gradient(top, #dddddd, #bbbbbb);
20 | background-image: -o-linear-gradient(top, #dddddd, #bbbbbb);
21 | background-image: linear-gradient(top, #dddddd, #bbbbbb);
22 | }
23 |
24 | .button {
25 | border: 0px;
26 | background: transparent;
27 | padding: 3px 5px;
28 | outline: none;
29 | }
30 |
31 | .inspector {
32 | align-self: flex-end;
33 | margin-left: auto;
34 | }
35 |
36 | .button:hover {
37 | cursor: pointer;
38 | background: #ddd;
39 | }
40 |
41 | .button:disabled {
42 | opacity: 0.25;
43 | }
44 |
45 | .separator {
46 | border-left: 1px solid #888;
47 | height: 30px;
48 | display: inline-block;
49 | }
50 |
--------------------------------------------------------------------------------
/src/components/dialogs/AboutDialog/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 Dialog from '../Dialog';
7 | import logoImage from 'images/logo.png';
8 |
9 | import styles from './styles/index.css';
10 |
11 | @inject('view_store', 'session_store')
12 | @observer
13 | @CSSModules(styles)
14 | class AboutDialog extends Component {
15 | @autobind onHide() {
16 | this.props.view_store.toggleAboutDialog();
17 | }
18 |
19 | render() {
20 | return (
21 |
38 | );
39 | }
40 | }
41 |
42 | export default AboutDialog;
43 |
--------------------------------------------------------------------------------
/src/components/Torrent/StatusDetails/__tests__/Error.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import createComponentWithIntl from 'test/util/createComponentWithIntl';
3 |
4 | import Error from '../Error';
5 |
6 | import Torrent from 'stores/torrent';
7 |
8 | const {
9 | ERR_TRACKER_WARNING,
10 | ERR_TRACKER_ERROR,
11 | ERR_LOCAL_ERROR,
12 | } = Torrent;
13 |
14 | test('Error track warning', () => {
15 | const component = createComponentWithIntl(
16 |
17 | );
18 |
19 | expect(component.toJSON()).toMatchSnapshot();
20 | });
21 |
22 | test('Error track error', () => {
23 | const component = createComponentWithIntl(
24 |
25 | );
26 |
27 | expect(component.toJSON()).toMatchSnapshot();
28 | });
29 |
30 | test('Error local error', () => {
31 | const component = createComponentWithIntl(
32 |
33 | );
34 |
35 | expect(component.toJSON()).toMatchSnapshot();
36 | });
37 |
38 | test('Error undefined error', () => {
39 | const component = createComponentWithIntl(
40 |
41 | );
42 |
43 | expect(component.toJSON()).toMatchSnapshot();
44 | });
45 |
--------------------------------------------------------------------------------
/src/components/Torrent/StatusDetails/__tests__/Downloading.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import createComponentWithIntl from 'test/util/createComponentWithIntl';
3 |
4 | import Downloading from '../Downloading';
5 |
6 | test('Downloading from peers and webseeds', () => {
7 | const component = createComponentWithIntl(
8 |
17 | );
18 |
19 | expect(component.toJSON()).toMatchSnapshot();
20 | });
21 |
22 | test('Downloading from webseeds', () => {
23 | const component = createComponentWithIntl(
24 |
31 | );
32 |
33 | expect(component.toJSON()).toMatchSnapshot();
34 | });
35 |
36 | test('Downloading from peers', () => {
37 | const component = createComponentWithIntl(
38 |
46 | );
47 |
48 | expect(component.toJSON()).toMatchSnapshot();
49 | });
50 |
--------------------------------------------------------------------------------
/src/components/Torrent/ProgressDetails/__tests__/index.spec.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import createComponentWithIntl from 'test/util/createComponentWithIntl';
3 |
4 | import ProgressDetails from '../';
5 |
6 | const torrent = {
7 | metadataPercentComplete: 0.75,
8 | sizeWhenDone: 1E5,
9 | leftUntilDone: 1E4,
10 | percentDone: 0.9,
11 | totalSize: 1E5,
12 | uploadedEver: 1E5,
13 | uploadRatio: 2,
14 | };
15 |
16 | test('ProgressDetails magnet', () => {
17 | const component = createComponentWithIntl(
18 |
19 | );
20 |
21 | expect(component.toJSON()).toMatchSnapshot();
22 | });
23 |
24 | test('ProgressDetails done', () => {
25 | const component = createComponentWithIntl(
26 |
27 | );
28 |
29 | expect(component.toJSON()).toMatchSnapshot();
30 | });
31 |
32 | test('ProgressDetails seeding', () => {
33 | const component = createComponentWithIntl(
34 |
35 | );
36 |
37 | expect(component.toJSON()).toMatchSnapshot();
38 | });
39 |
40 | test('ProgressDetails pending', () => {
41 | const component = createComponentWithIntl(
42 |
43 | );
44 |
45 | expect(component.toJSON()).toMatchSnapshot();
46 | });
47 |
--------------------------------------------------------------------------------
/src/components/dialogs/ConnectionDialog/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 Dialog from '../Dialog';
7 | import logoImage from 'images/logo.png';
8 |
9 | import styles from './styles/index.css';
10 |
11 | @inject('view_store')
12 | @observer
13 | @CSSModules(styles)
14 | class ConnectionDialog extends Component {
15 | @autobind onDismiss(event) {
16 | event.preventDefault();
17 | this.props.view_store.toggleConnectionDialog();
18 | }
19 |
20 | @autobind onHide() {
21 | this.props.view_store.toggleConnectionDialog();
22 | }
23 |
24 | render() {
25 | return (
26 |
43 | );
44 | }
45 | }
46 |
47 | export default ConnectionDialog;
48 |
--------------------------------------------------------------------------------
/src/util/uri.js:
--------------------------------------------------------------------------------
1 | /**
2 | * http://blog.stevenlevithan.com/archives/parseuri
3 | *
4 | * parseUri 1.2.2
5 | * (c) Steven Levithan
6 | * MIT License
7 | */
8 | export function parseUri(str) {
9 | var o = parseUri.options;
10 | var m = o.parser[o.strictMode ? "strict" : "loose"].exec(str);
11 | var uri = {};
12 | var i = 14;
13 |
14 | while (i--) {
15 | uri[o.key[i]] = m[i] || "";
16 | };
17 |
18 | uri[o.q.name] = {};
19 | uri[o.key[12]].replace(o.q.parser, function ($0, $1, $2) {
20 | if ($1) {
21 | uri[o.q.name][$1] = $2;
22 | };
23 | });
24 |
25 | return uri;
26 | };
27 |
28 | parseUri.options = {
29 | strictMode: false,
30 | key: ["source", "protocol", "authority", "userInfo", "user", "password", "host", "port", "relative", "path", "directory", "file", "query", "anchor"],
31 | q: {
32 | name: "queryKey",
33 | parser: /(?:^|&)([^&=]*)=?([^&]*)/g
34 | },
35 | parser: {
36 | strict: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/,
37 | loose: /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/
38 | }
39 | };
40 |
--------------------------------------------------------------------------------
/src/components/Inspector/Files/PriorityButton.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import CSSModules from 'react-css-modules';
3 |
4 | import lowImage from 'images/file-priority-low.png';
5 | import normalImage from 'images/file-priority-normal.png';
6 | import highImage from 'images/file-priority-high.png';
7 |
8 | import styles from './styles/index.css';
9 |
10 | function PriorityButton({ priority, fileIds, setPriority }) {
11 | return (
12 |
13 |
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 |
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 |
49 |
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 |

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 |
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 |
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 | |
43 | Up |
44 | Down |
45 | % |
46 | Status |
47 | Address |
48 | Client |
49 |
50 |
51 |
52 | {peers.map((peer, index) => (
53 |
54 | {peer.isEncrypted && } |
55 | {peer.isUploadingTo && speedBps(peer.rateToPeer)} |
56 | {peer.isDownloadingFrom && speedBps(peer.rateToClient)} |
57 | {`${Math.floor(peer.progress * 100)}%`} |
58 | {[...peer.flagStr].map((flag) => {flag})} |
59 | {peer.address} |
60 | {peer.clientName} |
61 |
62 | ))}
63 |
64 |
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 |
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 |
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 | 
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 |
73 | {speedBps(this.props.torrents_store.totalDownloadSpeed)}
74 |
75 |
76 |
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 |
92 |
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 |
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 |
--------------------------------------------------------------------------------