Torrent details overhaul

This commit is contained in:
John Furrow
2016-01-25 22:14:20 -08:00
parent 978db6f67f
commit 6e21a3a96f
12 changed files with 415 additions and 162 deletions

View File

@@ -55,10 +55,10 @@
} }
&__tree { &__tree {
padding-left: 1px; padding-left: 3px;
.directory-tree__tree { .directory-tree__tree {
padding-left: 3px; padding-left: 6px;
} }
} }
} }

View File

@@ -12,11 +12,12 @@
background: $torrent-details--background; background: $torrent-details--background;
bottom: 0; bottom: 0;
box-shadow: -1px 0 0 $torrent-details--border; box-shadow: -1px 0 0 $torrent-details--border;
display: flex;
flex-direction: column;
font-size: 0.8em; font-size: 0.8em;
left: 0; left: 0;
min-width: 400px; min-width: 400px;
overflow: auto; overflow: auto;
padding: $spacing-unit;
position: absolute; position: absolute;
right: 0; right: 0;
top: 0; top: 0;
@@ -24,6 +25,8 @@
z-index: 2; z-index: 2;
&__heading { &__heading {
box-shadow: 0 1px 0 rgba(#040d13, 0.3);
flex: 0 0 auto;
.torrent { .torrent {
@@ -66,10 +69,6 @@
} }
} }
&__section {
margin-bottom: $spacing-unit;
}
&__table { &__table {
color: $torrent-details--table--foreground; color: $torrent-details--table--foreground;
width: 100%; width: 100%;
@@ -92,7 +91,27 @@
} }
} }
&__heading,
&__content,
&__navigation {
padding: $spacing-unit * 1/2 $spacing-unit;
}
&__content {
flex: 1;
overflow: auto;
&__wrapper {
display: flex;
flex: 1;
overflow: hidden;
}
}
&__navigation {
box-shadow: 1px 0 0 rgba(#040d13, 0.5);
background: rgba(#0c1b26, 0.5);
}
} }
.torrent-details-enter { .torrent-details-enter {

View File

@@ -4,26 +4,22 @@ import React from 'react';
import CSSTransitionGroup from 'react-addons-css-transition-group'; import CSSTransitionGroup from 'react-addons-css-transition-group';
import ApplicationPanel from '../layout/ApplicationPanel'; import ApplicationPanel from '../layout/ApplicationPanel';
import Download from '../icons/Download';
import EventTypes from '../../constants/EventTypes'; import EventTypes from '../../constants/EventTypes';
import ETA from '../icons/ETA';
import format from '../../util/formatData'; import format from '../../util/formatData';
import ProgressBar from '../ui/ProgressBar'; import NavigationList from '../ui/NavigationList';
import Ratio from '../../components/icons/Ratio';
import TorrentActions from '../../actions/TorrentActions'; import TorrentActions from '../../actions/TorrentActions';
import TorrentFiles from '../torrent-details/TorrentFiles'; import TorrentFiles from '../torrent-details/TorrentFiles';
import TorrentHeading from '../torrent-details/TorrentHeading';
import TorrentPeers from '../torrent-details/TorrentPeers'; import TorrentPeers from '../torrent-details/TorrentPeers';
import TorrentStore from '../../stores/TorrentStore'; import TorrentStore from '../../stores/TorrentStore';
import TorrentTrackers from '../torrent-details/TorrentTrackers'; import TorrentTrackers from '../torrent-details/TorrentTrackers';
import UIStore from '../../stores/UIStore'; import UIStore from '../../stores/UIStore';
import Upload from '../icons/Upload';
const METHODS_TO_BIND = [ const METHODS_TO_BIND = [
'handleNavChange',
'onTorrentDetailsHashChange', 'onTorrentDetailsHashChange',
'onOpenChange', 'onOpenChange',
'onTorrentDetailsChange', 'onTorrentDetailsChange'
'getHeading',
'getTorrentDetailsView'
]; ];
export default class TorrentDetails extends React.Component { export default class TorrentDetails extends React.Component {
@@ -36,7 +32,8 @@ export default class TorrentDetails extends React.Component {
torrentDetailsError: false, torrentDetailsError: false,
selectedTorrent: {}, selectedTorrent: {},
selectedTorrentHash: null, selectedTorrentHash: null,
torrentDetails: {} torrentDetails: {},
torrentDetailsPane: 'files'
}; };
METHODS_TO_BIND.forEach((method) => { METHODS_TO_BIND.forEach((method) => {
@@ -81,111 +78,76 @@ export default class TorrentDetails extends React.Component {
}); });
} }
getHeading(torrent) { getNavigationItem(item) {
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 uploadRate = format.data(torrent.uploadRate, '/s');
let uploadTotal = format.data(torrent.uploadTotal);
let classes = classNames('torrent-details__heading torrent-details__section', {
'is-selected': this.props.selected,
'is-stopped': torrent.status.indexOf('is-stopped') > -1,
'is-paused': torrent.status.indexOf('is-paused') > -1,
'is-actively-downloading': downloadRate.value > 0,
'is-downloading': torrent.status.indexOf('is-downloading') > -1,
'is-seeding': torrent.status.indexOf('is-seeding') > -1,
'is-completed': torrent.status.indexOf('is-completed') > -1,
'is-checking': torrent.status.indexOf('is-checking') > -1,
'is-active': torrent.status.indexOf('is-active') > -1,
'is-inactive': torrent.status.indexOf('is-inactive') > -1
});
return (
<div className={classes}>
<h1 className="torrent__details--name">{torrent.name}</h1>
<ul className="torrent__details torrent__details--tertiary">
<li className="torrent__details--download transfer-data--download">
<Download />
{downloadRate.value}
<em className="unit">{downloadRate.unit}</em>
</li>
<li className="torrent__details--upload transfer-data--upload">
<Upload />
{uploadRate.value}
<em className="unit">{uploadRate.unit}</em>
</li>
<li className="torrent__details--ratio">
<Ratio />
{ratio}
</li>
<li className="torrent__details--eta">
<ETA />
{eta}
</li>
<li className="torrent__details--completed">
<span className="torrent__details__label">Downloaded</span>
{torrent.percentComplete}
<em className="unit">%</em>
&nbsp;&mdash;&nbsp;
{completed.value}
<em className="unit">{completed.unit}</em>
</li>
<li className="torrent__details--uploaded">
<span className="torrent__details__label">Uploaded</span>
{uploadTotal.value}
<em className="unit">{uploadTotal.unit}</em>
</li>
<li className="torrent__details--size">
<span className="torrent__details__label">Size</span>
{totalSize.value}
<em className="unit">{totalSize.unit}</em>
</li>
<li className="torrent__details--added">
<span className="torrent__details__label">Added</span>
{addedString}
</li>
</ul>
<ProgressBar percent={torrent.percentComplete} />
</div>
);
}
getTorrentDetailsView() {
let torrentDetailsContent = null;
if (this.state.isOpen) {
let selectedHash = UIStore.getTorrentDetailsHash(); let selectedHash = UIStore.getTorrentDetailsHash();
let torrent = TorrentStore.getTorrent(selectedHash); let torrent = TorrentStore.getTorrent(selectedHash);
let torrentDetails = this.state.torrentDetails || {}; let torrentDetails = this.state.torrentDetails || {};
torrentDetailsContent = ( switch (item) {
case 'files':
return <TorrentFiles files={torrentDetails.files} torrent={torrent} />;
break;
case 'trackers':
return <TorrentTrackers trackers={torrentDetails.trackers} />;
break;
case 'peers':
return <TorrentPeers peers={torrentDetails.peers} />;
break;
}
return null;
}
getNavigationItems() {
return [
{
slug: 'files',
label: 'Files'
},
{
slug: 'peers',
label: 'Peers'
},
{
slug: 'trackers',
label: 'Trackers'
}
];
}
handleNavChange(item) {
this.setState({torrentDetailsPane: item.slug});
}
render() {
let detailContent = null;
if (this.state.isOpen) {
let selectedHash = UIStore.getTorrentDetailsHash();
let torrent = TorrentStore.getTorrent(selectedHash);
detailContent = (
<div className="torrent-details"> <div className="torrent-details">
{this.getHeading(torrent)} <TorrentHeading torrent={torrent} />
<TorrentTrackers trackers={torrentDetails.trackers} /> <div className="torrent-details__content__wrapper">
<TorrentFiles files={torrentDetails.files} torrent={torrent} /> <NavigationList defaultItem={this.state.torrentDetailsPane}
<TorrentPeers peers={torrentDetails.peers} /> items={this.getNavigationItems()} onChange={this.handleNavChange}
uniqueClassName="torrent-details__navigation" />
<div className="torrent-details__content">
{this.getNavigationItem(this.state.torrentDetailsPane)}
</div>
</div>
</div> </div>
); );
} }
return torrentDetailsContent;
}
render() {
return ( return (
<ApplicationPanel modifier="torrent-details"> <ApplicationPanel modifier="torrent-details">
<CSSTransitionGroup <CSSTransitionGroup
transitionEnterTimeout={1000} transitionEnterTimeout={1000}
transitionLeaveTimeout={1000} transitionLeaveTimeout={1000}
transitionName="torrent-details"> transitionName="torrent-details">
{this.getTorrentDetailsView()} {detailContent}
</CSSTransitionGroup> </CSSTransitionGroup>
</ApplicationPanel> </ApplicationPanel>
); );

View File

@@ -0,0 +1,59 @@
import classnames from 'classnames';
import React from 'react';
class NavigationList extends React.Component {
constructor() {
constructor();
this.state = {
selectedItem: null
};
}
getNavigationItems(items) {
return items.map((item, index) => {
let classes = classnames(this.props.itemClassName, {
this.props.selectedClassName: item.slug === this.state.selectedItem
});
return (
<li className={classes} key={index}
onClick={this.handleItemClick.bind(this, item)}>
{item.label}
</li>
);
});
}
handleItemClick(item) {
this.setState({
selectedItem: item.slug
});
this.props.onChange(item);
}
render() {
return (
<ul className={this.props.listClassName}>
{this.getNavigationItems(this.props.items)}
</ul>
);
}
}
NavigationList.defaultProps = {
itemClassName: 'navigation__item',
listClassName: 'navigation'
};
NavigationList.propTypes = {
defaultItem: React.PropTypes.string,
itemClassName: React.PropTypes.string,
items: React.PropTypes.array,
listClassName: React.PropTypes.string,
onChange: React.PropTypes.func,
selectedClassName: React.PropTypes.string
};
export default NavigationList;

View File

@@ -48,10 +48,27 @@ export default class TorrentFiles extends React.Component {
} }
getFileData(torrent, files) { getFileData(torrent, files) {
let parentDirectory = torrent.directory;
let filename = torrent.filename;
if (files) { }
handleParentDirectoryClick() {
this.setState({
expanded: !this.state.expanded
});
}
isLoaded() {
if (this.props.files) {
return true;
}
return false;
}
render() {
let {files, torrent} = this.props;
if (this.isLoaded()) {
// We've received full file details from the client. // We've received full file details from the client.
let fileList = null; let fileList = null;
@@ -64,7 +81,7 @@ export default class TorrentFiles extends React.Component {
<div className="directory-tree__node directory-tree__parent-directory" <div className="directory-tree__node directory-tree__parent-directory"
onClick={this.handleParentDirectoryClick}> onClick={this.handleParentDirectoryClick}>
<FolderOpenSolid /> <FolderOpenSolid />
{parentDirectory} {torrent.directory}
</div> </div>
{fileList} {fileList}
</div> </div>
@@ -73,26 +90,17 @@ export default class TorrentFiles extends React.Component {
// We've only received the top-level file details from the torrent list. // We've only received the top-level file details from the torrent list.
return ( return (
<div className="directory-tree torrent-details__section"> <div className="directory-tree torrent-details__section">
<div className="directory-tree__node directory-tree__parent-directory"> <div className="directory-tree__node
directory-tree__parent-directory">
<FolderOpenSolid /> <FolderOpenSolid />
{parentDirectory} {torrent.directory}
</div> </div>
<div className="directory-tree__node directory-tree__node--file"> <div className="directory-tree__node directory-tree__node--file">
<File /> <File />
{filename} {torrent.filename}
</div> </div>
</div> </div>
); );
} }
} }
handleParentDirectoryClick() {
this.setState({
expanded: !this.state.expanded
});
}
render() {
return this.getFileData(this.props.torrent, this.props.files);
}
} }

View File

@@ -0,0 +1,91 @@
import classNames from 'classnames';
import React from 'react';
import FolderOpenSolid from '../icons/FolderOpenSolid';
import DirectoryTree from '../filesystem/DirectoryTree';
import Download from '../icons/Download';
import ETA from '../icons/ETA';
import File from '../icons/File';
import format from '../../util/formatData';
import ProgressBar from '../ui/ProgressBar';
import Ratio from '../../components/icons/Ratio';
import Upload from '../icons/Upload';
export default class TorrentHeading extends React.Component {
render() {
let torrent = this.props.torrent;
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 uploadRate = format.data(torrent.uploadRate, '/s');
let uploadTotal = format.data(torrent.uploadTotal);
let classes = classNames('torrent-details__heading', {
'is-selected': this.props.selected,
'is-stopped': torrent.status.indexOf('is-stopped') > -1,
'is-paused': torrent.status.indexOf('is-paused') > -1,
'is-actively-downloading': downloadRate.value > 0,
'is-downloading': torrent.status.indexOf('is-downloading') > -1,
'is-seeding': torrent.status.indexOf('is-seeding') > -1,
'is-completed': torrent.status.indexOf('is-completed') > -1,
'is-checking': torrent.status.indexOf('is-checking') > -1,
'is-active': torrent.status.indexOf('is-active') > -1,
'is-inactive': torrent.status.indexOf('is-inactive') > -1
});
return (
<div className={classes}>
<h1 className="torrent__details--name">{torrent.name}</h1>
<ul className="torrent__details torrent__details--tertiary">
<li className="torrent__details--download transfer-data--download">
<Download />
{downloadRate.value}
<em className="unit">{downloadRate.unit}</em>
</li>
<li className="torrent__details--upload transfer-data--upload">
<Upload />
{uploadRate.value}
<em className="unit">{uploadRate.unit}</em>
</li>
<li className="torrent__details--ratio">
<Ratio />
{ratio}
</li>
<li className="torrent__details--eta">
<ETA />
{eta}
</li>
<li className="torrent__details--completed">
<span className="torrent__details__label">Downloaded</span>
{torrent.percentComplete}
<em className="unit">%</em>
&nbsp;&mdash;&nbsp;
{completed.value}
<em className="unit">{completed.unit}</em>
</li>
<li className="torrent__details--uploaded">
<span className="torrent__details__label">Uploaded</span>
{uploadTotal.value}
<em className="unit">{uploadTotal.unit}</em>
</li>
<li className="torrent__details--size">
<span className="torrent__details__label">Size</span>
{totalSize.value}
<em className="unit">{totalSize.unit}</em>
</li>
<li className="torrent__details--added">
<span className="torrent__details__label">Added</span>
{addedString}
</li>
</ul>
<ProgressBar percent={torrent.percentComplete} />
</div>
);
}
}

View File

@@ -6,7 +6,9 @@ import File from '../icons/File';
import format from '../../util/formatData'; import format from '../../util/formatData';
export default class TorrentPeers extends React.Component { export default class TorrentPeers extends React.Component {
getPeerList(peers) { render() {
let peers = this.props.peers;
if (peers) { if (peers) {
let peerList = null; let peerList = null;
let peerCount = 0; let peerCount = 0;
@@ -55,8 +57,4 @@ export default class TorrentPeers extends React.Component {
return null; return null;
} }
} }
render() {
return this.getPeerList(this.props.peers);
}
} }

View File

@@ -1,7 +1,9 @@
import React from 'react'; import React from 'react';
export default class TorrentTrackrs extends React.Component { export default class TorrentTrackrs extends React.Component {
getTrackerList(trackers = []) { render() {
let trackers = this.props.trackers || [];
let trackerCount = trackers.length; let trackerCount = trackers.length;
let trackerTypes = ['http', 'udp', 'dht']; let trackerTypes = ['http', 'udp', 'dht'];
@@ -39,8 +41,4 @@ export default class TorrentTrackrs extends React.Component {
</div> </div>
); );
} }
render() {
return this.getTrackerList(this.props.trackers);
}
} }

View File

@@ -0,0 +1,73 @@
import classnames from 'classnames';
import React from 'react';
class NavigationList extends React.Component {
constructor() {
super();
this.state = {
selectedItem: null
};
}
getNavigationItems(items) {
return items.map((item, index) => {
let selectedSlug = null;
if (this.state.selectedItem) {
selectedSlug = this.state.selectedItem;
} else {
selectedSlug = this.props.defaultItem;
}
let classes = classnames(this.props.itemClassName, {
[this.props.selectedClassName]: item.slug === selectedSlug
});
return (
<li className={classes} key={index}
onClick={this.handleItemClick.bind(this, item)}>
{item.label}
</li>
);
});
}
handleItemClick(item) {
this.setState({
selectedItem: item.slug
});
this.props.onChange(item);
}
render() {
let classes = classnames(this.props.listClassName, {
[this.props.uniqueClassName]: this.props.uniqueClassName
});
return (
<ul className={classes}>
{this.getNavigationItems(this.props.items)}
</ul>
);
}
}
NavigationList.defaultProps = {
itemClassName: 'navigation__item',
listClassName: 'navigation',
selectedClassName: 'is-selected'
};
NavigationList.propTypes = {
defaultItem: React.PropTypes.string,
itemClassName: React.PropTypes.string,
items: React.PropTypes.array,
listClassName: React.PropTypes.string,
onChange: React.PropTypes.func,
selectedClassName: React.PropTypes.string,
uniqueClassName: React.PropTypes.string
};
export default NavigationList;

File diff suppressed because one or more lines are too long

View File

@@ -915,9 +915,9 @@ body {
opacity: 0.75; } opacity: 0.75; }
.directory-tree__tree { .directory-tree__tree {
padding-left: 1px; }
.directory-tree__tree .directory-tree__tree {
padding-left: 3px; } padding-left: 3px; }
.directory-tree__tree .directory-tree__tree {
padding-left: 6px; }
.dropdown { .dropdown {
display: inline-block; display: inline-block;
@@ -1288,17 +1288,31 @@ body {
background: #162835; background: #162835;
bottom: 0; bottom: 0;
box-shadow: -1px 0 0 rgba(26, 47, 61, 0.1); box-shadow: -1px 0 0 rgba(26, 47, 61, 0.1);
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
font-size: 0.8em; font-size: 0.8em;
left: 0; left: 0;
min-width: 400px; min-width: 400px;
overflow: auto; overflow: auto;
padding: 25px;
position: absolute; position: absolute;
right: 0; right: 0;
top: 0; top: 0;
-webkit-transition: opacity 1s; -webkit-transition: opacity 1s;
transition: opacity 1s; transition: opacity 1s;
z-index: 2; } z-index: 2; }
.torrent-details__heading {
box-shadow: 0 1px 0 rgba(4, 13, 19, 0.3);
-webkit-box-flex: 0;
-webkit-flex: 0 0 auto;
-ms-flex: 0 0 auto;
flex: 0 0 auto; }
.torrent-details__heading .torrent__details--name { .torrent-details__heading .torrent__details--name {
color: #97bbd5; color: #97bbd5;
font-size: 1.7em; font-size: 1.7em;
@@ -1314,8 +1328,6 @@ body {
visibility: visible; } visibility: visible; }
.torrent-details__heading .progress-bar:after { .torrent-details__heading .progress-bar:after {
background: #2a3e4c; } background: #2a3e4c; }
.torrent-details__section {
margin-bottom: 25px; }
.torrent-details__table { .torrent-details__table {
color: #567285; color: #567285;
width: 100%; } width: 100%; }
@@ -1330,6 +1342,27 @@ body {
color: #778c9e; color: #778c9e;
display: inline-block; display: inline-block;
margin-left: 5px; } margin-left: 5px; }
.torrent-details__heading, .torrent-details__content, .torrent-details__navigation {
padding: 12.5px 25px; }
.torrent-details__content {
-webkit-box-flex: 1;
-webkit-flex: 1;
-ms-flex: 1;
flex: 1;
overflow: auto; }
.torrent-details__content__wrapper {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-box-flex: 1;
-webkit-flex: 1;
-ms-flex: 1;
flex: 1;
overflow: hidden; }
.torrent-details__navigation {
box-shadow: 1px 0 0 rgba(4, 13, 19, 0.5);
background: rgba(12, 27, 38, 0.5); }
.torrent-details-enter { .torrent-details-enter {
opacity: 0; } opacity: 0; }

File diff suppressed because one or more lines are too long