Organize filestructure

This commit is contained in:
John Furrow
2016-01-16 23:01:51 -08:00
parent 71141ac8ba
commit 20fcf21d80
45 changed files with 819 additions and 497 deletions
@@ -0,0 +1,16 @@
import React from 'react';
import Stop from '../icons/Stop';
export default class Action extends React.Component {
render() {
let classString = 'action action--' + this.props.slug;
return (
<div className={classString} onClick={this.props.clickHandler}>
{this.props.icon}
<span className="action__label">{this.props.label}</span>
</div>
);
}
}
@@ -0,0 +1,91 @@
import React from 'react';
import Action from './Action';
import Add from '../icons/Add';
import EventTypes from '../../constants/EventTypes';
import Pause from '../icons/Pause';
import SortDropdown from './SortDropdown';
import Start from '../icons/Start';
import Stop from '../icons/Stop';
import TorrentActions from '../../actions/TorrentActions';
import TorrentFilterStore from '../../stores/TorrentFilterStore';
import TorrentStore from '../../stores/TorrentStore';
import UIActions from '../../actions/UIActions';
const METHODS_TO_BIND = [
'handleAddTorrents',
'handleSortChange',
'handleStart',
'handleStop',
'onSortChange'
];
export default class ActionBar extends React.Component {
constructor() {
super();
this.state = {
sortBy: TorrentFilterStore.getTorrentsSort()
};
METHODS_TO_BIND.forEach((method) => {
this[method] = this[method].bind(this);
});
}
componentDidMount() {
TorrentFilterStore.listen(EventTypes.UI_TORRENTS_SORT_CHANGE, this.onSortChange);
}
componentWillUnmount() {
TorrentFilterStore.unlisten(EventTypes.UI_TORRENTS_SORT_CHANGE, this.onSortChange);
}
handleAddTorrents() {
UIActions.displayModal('add-torrents');
}
handleSortChange(sortBy) {
UIActions.setTorrentsSort(sortBy);
}
handleStart() {
TorrentActions.startTorrents(TorrentStore.getSelectedTorrents());
}
handleStop() {
TorrentActions.stopTorrents(TorrentStore.getSelectedTorrents());
}
onSortChange() {
this.setState({
sortBy: TorrentFilterStore.getTorrentsSort()
});
}
render() {
return (
<nav className="action-bar">
<div className="actions action-bar__item action-bar__item--sort-torrents">
<SortDropdown onSortChange={this.handleSortChange}
selectedItem={this.state.sortBy} />
</div>
<div className="actions action-bar__item action-bar__item--torrent-operations">
<div className="action-bar__group">
<Action label="Start Torrent" slug="start-torrent" icon={<Start />}
clickHandler={this.handleStart} />
<Action label="Stop Torrent" slug="stop-torrent" icon={<Stop />}
clickHandler={this.handleStop} />
<Action label="Pause Torrent" slug="pause-torrent" icon={<Pause />}
clickHandler={this.handlePause} />
</div>
<div className="action-bar__group action-bar__group--has-divider">
<Action label="Add Torrent" slug="add-torrent" icon={<Add />}
clickHandler={this.handleAddTorrents} />
</div>
</div>
</nav>
);
}
}
@@ -1,30 +0,0 @@
import React from 'react';
import File from '../icons/File';
export default class DirectoryFiles extends React.Component {
render() {
let branch = Object.assign([], this.props.branch);
branch.sort((a, b) => {
console.log(a, b);
return a.filename.localeCompare(b.filename);
});
let files = branch.map((file, fileIndex) => {
return (
<div className="directory-tree__node directory-tree__node--file"
key={`${fileIndex}`} title={file.filename}>
<File />
{file.filename}
</div>
);
});
return (
<div className="directory-tree__node directory-tree__node--file-list">
{files}
</div>
);
}
}
@@ -1,31 +0,0 @@
import React from 'react';
import DirectoryFileList from './DirectoryFileList';
import DirectoryTreeNode from './DirectoryTreeNode';
export default class DirectoryTree extends React.Component {
getDirectoryTreeDomNodes(tree, depth = 0) {
let index = 0;
depth++;
return Object.keys(tree).map((branchName) => {
let branch = tree[branchName];
index++;
if (branchName === 'files') {
return <DirectoryFileList branch={branch} key={`${index}${depth}`} />;
} else {
return <DirectoryTreeNode depth={depth} directoryName={branchName}
subTree={branch} key={`${index}${depth}`} />;
}
});
}
render() {
return (
<div className="directory-tree__tree">
{this.getDirectoryTreeDomNodes(this.props.tree, this.props.depth)}
</div>
);
}
}
@@ -1,60 +0,0 @@
import React from 'react';
import FolderClosedOutlined from '../icons/FolderClosedOutlined';
import FolderOpenOutlined from '../icons/FolderOpenOutlined';
import DirectoryTree from './DirectoryTree';
const METHODS_TO_BIND = ['handleDirectoryClick'];
export default class DirectoryTreeNode extends React.Component {
constructor() {
super();
this.state = {
expanded: false
};
METHODS_TO_BIND.forEach((method) => {
this[method] = this[method].bind(this);
});
}
getSubTree() {
if (this.state.expanded) {
return (
<div className="directory-tree__node directory-tree__node--group">
<DirectoryTree tree={this.props.subTree} depth={this.props.depth} />
</div>
);
} else {
return null;
}
}
handleDirectoryClick() {
this.setState({
expanded: !this.state.expanded
});
}
render() {
let classes = `directory-tree__branch directory-tree__branch--depth-${this.props.depth}`;
let icon = <FolderClosedOutlined />;
if (this.state.expanded) {
icon = <FolderOpenOutlined />;
}
return (
<div className={classes}>
<div className="directory-tree__node directory-tree__node--directory"
onClick={this.handleDirectoryClick} title={this.props.directoryName}>
{icon}
{this.props.directoryName}
</div>
{this.getSubTree()}
</div>
);
}
}
@@ -1,14 +0,0 @@
import React from 'react';
export default class ProgressBar extends React.Component {
render() {
let percent = this.props.percent;
let className = 'progress-bar';
return (
<div className={className}>
<div className="progress-bar__fill" style={{width: percent + '%'}}></div>
</div>
);
}
}
@@ -0,0 +1,116 @@
import classnames from 'classnames';
import CSSTransitionGroup from 'react-addons-css-transition-group';
import React from 'react';
import Dropdown from '../forms/Dropdown';
import UIActions from '../../actions/UIActions';
const METHODS_TO_BIND = [
'getDropdownHeader',
'handleItemSelect'
];
const SORT_PROPERTIES = [
{
displayName: 'Name',
value: 'name'
},
{
displayName: 'ETA',
value: 'eta'
},
{
displayName: 'Download Speed',
value: 'downloadRate'
},
{
displayName: 'Upload Speed',
value: 'uploadRate'
},
{
displayName: 'Ratio',
value: 'ratio'
},
{
displayName: 'Percent Complete',
value: 'percentComplete'
},
{
displayName: 'Downloaded',
value: 'downloadTotal'
},
{
displayName: 'Uploaded',
value: 'uploadTotal'
},
{
displayName: 'File Size',
value: 'sizeBytes'
},
{
displayName: 'Date Added',
value: 'added'
}
];
export default class SortDropdown extends React.Component {
constructor() {
super();
METHODS_TO_BIND.forEach((method) => {
this[method] = this[method].bind(this);
});
}
getDropdownHeader() {
return (
<a className="dropdown__button">
<label className="dropdown__label">Sort By</label>
<span className="dropdown__value">{this.props.selectedItem.displayName}</span>
</a>
);
}
getDropdownMenus() {
let items = SORT_PROPERTIES.map((sortProp) => {
return {
displayName: sortProp.displayName,
property: 'sortBy',
selected: this.props.selectedItem.value === sortProp.value,
value: sortProp.value
}
});
// Dropdown expects an array of arrays.
return [items];
}
handleItemSelect(sortBy) {
let direction = this.props.selectedItem.direction;
if (this.props.selectedItem.value === sortBy.value) {
direction = direction === 'asc' ? 'desc' : 'asc';
} else {
direction = 'asc';
}
let sortProperty = {
direction,
displayName: sortBy.displayName,
property: 'sortBy',
value: sortBy.value
};
this.props.onSortChange(sortProperty);
}
render() {
return (
<Dropdown
handleItemSelect={this.handleItemSelect}
header={this.getDropdownHeader()}
menuItems={this.getDropdownMenus()}
/>
);
}
}
@@ -3,7 +3,7 @@ import React from 'react';
import DotsMini from '../icons/DotsMini';
import format from '../../util/formatData';
import ProgressBar from './ProgressBar';
import ProgressBar from '../ui/ProgressBar';
const METHODS_TO_BIND = [
'handleClick',
@@ -1,239 +0,0 @@
import _ from 'lodash';
import classNames from 'classnames';
import React from 'react';
import CSSTransitionGroup from 'react-addons-css-transition-group';
import Download from '../../components/icons/Download';
import TorrentFiles from './TorrentFiles';
import EventTypes from '../../constants/EventTypes';
import ETA from '../../components/icons/ETA';
import format from '../../util/formatData';
import Ratio from '../../components/icons/Ratio';
import TorrentActions from '../../actions/TorrentActions';
import TorrentStore from '../../stores/TorrentStore';
import UIStore from '../../stores/UIStore';
import Upload from '../../components/icons/Upload';
const METHODS_TO_BIND = [
'onTorrentDetailsHashChange',
'onOpenChange',
'onTorrentDetailsChange',
'getHeading',
'getSidePanel'
];
export default class TorrentDetails extends React.Component {
constructor() {
super();
this.state = {
isOpen: false,
torrentDetailsSuccess: false,
torrentDetailsError: false,
selectedTorrent: {},
selectedTorrentHash: null,
torrentDetails: {}
};
METHODS_TO_BIND.forEach((method) => {
this[method] = this[method].bind(this);
});
}
componentDidMount() {
TorrentStore.listen(EventTypes.CLIENT_TORRENT_DETAILS_CHANGE, this.onTorrentDetailsChange);
UIStore.listen(EventTypes.UI_TORRENT_DETAILS_OPEN_CHANGE, this.onOpenChange);
UIStore.listen(EventTypes.UI_TORRENT_DETAILS_HASH_CHANGE, this.onTorrentDetailsHashChange);
}
componentWillUnmount() {
TorrentStore.stopPollingTorrentDetails();
TorrentStore.unlisten(EventTypes.CLIENT_TORRENT_DETAILS_CHANGE, this.onTorrentDetailsChange);
UIStore.unlisten(EventTypes.UI_TORRENT_DETAILS_OPEN_CHANGE, this.onOpenChange);
UIStore.unlisten(EventTypes.UI_TORRENT_DETAILS_HASH_CHANGE, this.onTorrentDetailsHashChange);
}
onTorrentDetailsHashChange() {
if (UIStore.isTorrentDetailsOpen()) {
TorrentStore.fetchTorrentDetails(UIStore.getTorrentDetailsHash());
}
}
onOpenChange() {
if (!UIStore.isTorrentDetailsOpen()) {
TorrentStore.stopPollingTorrentDetails();
} else {
TorrentStore.fetchTorrentDetails(UIStore.getTorrentDetailsHash());
}
this.setState({
isOpen: UIStore.isTorrentDetailsOpen()
});
}
onTorrentDetailsChange() {
this.setState({
torrentDetails: TorrentStore.getTorrentDetails(UIStore.getTorrentDetailsHash())
});
}
getHeading() {
// return (
// <div className="torrent-details__actions torrent-details__section">
// Dropdown
// </div>
// );
}
getPeerList(peers) {
if (peers) {
let peerList = null;
let peerCount = 0;
peerList = peers.map(function(peer, index) {
let downloadRate = format.data(peer.downloadRate, '/s');
let uploadRate = format.data(peer.uploadRate, '/s');
return (
<tr key={index}>
<td>{peer.address}</td>
<td>
{downloadRate.value}
<em className="unit">{downloadRate.unit}</em>
</td>
<td>
{uploadRate.value}
<em className="unit">{uploadRate.unit}</em>
</td>
</tr>
);
});
peerCount = peerList.length;
return (
<div className="torrent-details__peers torrent-details__section">
<table className="torrent-details__table table">
<thead className="torrent-details__table__heading">
<tr>
<th>
Peers
<span className="torrent-details__table__heading__count">
{peerCount}
</span>
</th>
<th>DL</th>
<th>UL</th>
</tr>
</thead>
<tbody>
{peerList}
</tbody>
</table>
</div>
)
}
}
getSidePanel() {
if (!this.state.isOpen) {
return null;
}
let selectedHash = UIStore.getTorrentDetailsHash();
let torrent = TorrentStore.getTorrent(selectedHash);
let added = new Date(torrent.added * 1000);
let addedString = (added.getMonth() + 1) + '/' + added.getDate() + '/' +
added.getFullYear();
let completed = format.data(torrent.bytesDone);
let downloadRate = format.data(torrent.downloadRate, '/s');
let downloadTotal = format.data(torrent.downloadTotal);
let eta = format.eta(torrent.eta);
let ratio = format.ratio(torrent.ratio);
let totalSize = format.data(torrent.sizeBytes);
let torrentDetails = this.state.torrentDetails || {};
let uploadRate = format.data(torrent.uploadRate, '/s');
let uploadTotal = format.data(torrent.uploadTotal);
return (
<div className="torrent-details" key={this.state.isOpen}>
{this.getHeading()}
<ul className="torrent-details__transfer-data torrent-details__section">
<li className="transfer-data transfer-data--download">
<Download />
{downloadRate.value}
<em className="unit">{downloadRate.unit}</em>
</li>
<li className="transfer-data transfer-data--upload">
<Upload />
{uploadRate.value}
<em className="unit">{uploadRate.unit}</em>
</li>
<li className="transfer-data transfer-data--ratio">
<Ratio />
{ratio}
</li>
<li className="transfer-data transfer-data--eta">
<ETA />
{eta}
</li>
</ul>
{this.getTrackerList(torrentDetails.trackers)}
<TorrentFiles files={torrentDetails.files} torrent={torrent} />
{this.getPeerList(torrentDetails.peers)}
</div>
);
}
getTrackerList(trackers = []) {
let trackerCount = trackers.length;
let trackerTypes = ['http', 'udp', 'dht'];
let trackerDetails = trackers.map((tracker, index) => {
return (
<tr key={index}>
<td>
{tracker.url}
</td>
<td>
{trackerTypes[tracker.type - 1]}
</td>
</tr>
);
});
return (
<div className="torrent-details__peers torrent-details__section">
<table className="torrent-details__table table">
<thead className="torrent-details__table__heading">
<tr>
<th>
Trackers
<span className="torrent-details__table__heading__count">
{trackerCount}
</span>
</th>
<th>Type</th>
</tr>
</thead>
<tbody>
{trackerDetails}
</tbody>
</table>
</div>
);
}
render() {
try {
return (
<CSSTransitionGroup
transitionEnterTimeout={500}
transitionLeaveTimeout={500}
transitionName="torrent-details">
{this.getSidePanel()}
</CSSTransitionGroup>
);
} catch (err) {
console.trace(err);
}
}
}
@@ -1,98 +0,0 @@
import React from 'react';
import FolderOpenSolid from '../icons/FolderOpenSolid';
import DirectoryTree from './DirectoryTree';
import File from '../icons/File';
const METHODS_TO_BIND = ['handleParentDirectoryClick'];
export default class TorrentFiles extends React.Component {
constructor() {
super();
this.state = {
expanded: true
};
METHODS_TO_BIND.forEach((method) => {
this[method] = this[method].bind(this);
});
}
constructDirectoryTree(tree = {}, directory, file, depth = 0) {
if (depth < file.pathComponents.length - 1) {
depth++;
tree[directory] = this.constructDirectoryTree(
tree[directory],
file.pathComponents[depth],
file,
depth
);
} else {
if (!tree.files) {
tree.files = [];
}
tree.files.push(file);
}
return tree;
}
getFileList(files) {
let tree = {};
files.forEach((file) => {
this.constructDirectoryTree(tree, file.pathComponents[0], file);
});
return <DirectoryTree tree={tree} depth={0} />;
}
getFileData(torrent, files) {
let parentDirectory = torrent.directory;
let filename = torrent.filename;
if (files) {
// We've received full file details from the client.
let fileList = null;
if (this.state.expanded) {
fileList = this.getFileList(files);
}
return (
<div className="directory-tree torrent-details__section">
<div className="directory-tree__node directory-tree__parent-directory"
onClick={this.handleParentDirectoryClick}>
<FolderOpenSolid />
{parentDirectory}
</div>
{fileList}
</div>
);
} else {
// We've only received the top-level file details from the torrent list.
return (
<div className="directory-tree torrent-details__section">
<div className="directory-tree__node directory-tree__parent-directory">
<FolderOpenSolid />
{parentDirectory}
</div>
<div className="directory-tree__node directory-tree__node--file">
<File />
{filename}
</div>
</div>
);
}
}
handleParentDirectoryClick() {
this.setState({
expanded: !this.state.expanded
});
}
render() {
return this.getFileData(this.props.torrent, this.props.files);
}
}
@@ -4,9 +4,8 @@ import React from 'react';
import ReactDOM from 'react-dom';
import EventTypes from '../../constants/EventTypes';
import LoadingIndicator from '../generic/LoadingIndicator';
import LoadingIndicator from '../ui/LoadingIndicator';
import Torrent from './Torrent';
import TorrentDetails from './TorrentDetails';
import TorrentFilterStore from '../../stores/TorrentFilterStore';
import TorrentStore from '../../stores/TorrentStore';
import UIActions from '../../actions/UIActions';
@@ -1,6 +1,5 @@
import React from 'react';
import TorrentDetails from './TorrentDetails';
import TorrentList from './TorrentList';
export default class TorrentListContainer extends React.Component {
@@ -8,7 +7,6 @@ export default class TorrentListContainer extends React.Component {
return (
<div className="torrents">
<TorrentList />
<TorrentDetails />
</div>
);
}