Rearrange app directory structure
3
.gitignore
vendored
@@ -2,5 +2,4 @@
|
||||
node_modules
|
||||
npm-debug.log
|
||||
bower_components
|
||||
public/stylesheets
|
||||
public/scripts
|
||||
server/assets
|
||||
|
||||
|
Before Width: | Height: | Size: 186 B After Width: | Height: | Size: 186 B |
|
Before Width: | Height: | Size: 158 B After Width: | Height: | Size: 158 B |
|
Before Width: | Height: | Size: 150 B After Width: | Height: | Size: 150 B |
|
Before Width: | Height: | Size: 147 B After Width: | Height: | Size: 147 B |
@@ -21,7 +21,7 @@ body {
|
||||
max-width: 240px;
|
||||
}
|
||||
|
||||
.main {
|
||||
.content {
|
||||
display: flex;
|
||||
flex: 5;
|
||||
flex-direction: column;
|
||||
@@ -43,6 +43,7 @@ body {
|
||||
|
||||
&__list {
|
||||
flex: 5;
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -63,8 +63,21 @@
|
||||
|
||||
&__group {
|
||||
display: inline-block;
|
||||
box-shadow: inset 1px 0 $action-bar--group--border;
|
||||
padding: 0 15px;
|
||||
|
||||
&--has-divider {
|
||||
position: relative;
|
||||
|
||||
&:before {
|
||||
background: $action-bar--group--border;
|
||||
content: '';
|
||||
position: absolute;
|
||||
height: 80%;
|
||||
left: 0;
|
||||
top: 10%;
|
||||
width: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,16 +90,7 @@
|
||||
max-width: 225px;
|
||||
|
||||
.dropdown {
|
||||
display: inline-block;
|
||||
|
||||
&__button {
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
padding: 15px 30px 11px 30px;
|
||||
text-align: left;
|
||||
width: auto;
|
||||
word-wrap: none;
|
||||
}
|
||||
margin: 5px 0 0 15px;
|
||||
|
||||
&__label {
|
||||
color: $dropdown--label;
|
||||
@@ -114,7 +118,6 @@
|
||||
}
|
||||
|
||||
&__content {
|
||||
left: 30px;
|
||||
min-width: 250px;
|
||||
}
|
||||
}
|
||||
3
client/source/sass/objects/_content.scss
Normal file
@@ -0,0 +1,3 @@
|
||||
.content {
|
||||
background: $main-content--background;
|
||||
}
|
||||
93
client/source/sass/objects/_dropdown.scss
Normal file
@@ -0,0 +1,93 @@
|
||||
.dropdown {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
|
||||
&__button {
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
padding: 12px 15px 7px 15px;
|
||||
text-align: left;
|
||||
width: auto;
|
||||
word-wrap: none;
|
||||
}
|
||||
|
||||
&__content {
|
||||
background: $dropdown--background;
|
||||
border-radius: 3px;
|
||||
box-shadow:
|
||||
0 0 0 1px $dropdown--container--border,
|
||||
0 0 35px $dropdown--container--shadow;
|
||||
color: $dropdown--foreground;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
text-align: left;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
&__header {
|
||||
position: relative;
|
||||
|
||||
&:after {
|
||||
background: $dropdown--header--border;
|
||||
bottom: 0;
|
||||
content: '';
|
||||
display: block;
|
||||
height: 1px;
|
||||
left: 5%;
|
||||
position: absolute;
|
||||
width: 90%;
|
||||
}
|
||||
}
|
||||
|
||||
&__items {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
&__item {
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
font-size: 0.9em;
|
||||
padding: 5px 15px;
|
||||
transition: background 0.25s, color 0.25s;
|
||||
|
||||
&:hover {
|
||||
background: $dropdown--item--background--hover;
|
||||
color: $dropdown--item--foreground--hover;
|
||||
}
|
||||
|
||||
&.is-selected {
|
||||
color: $dropdown--item--foreground--active;
|
||||
}
|
||||
}
|
||||
|
||||
&--align-right & {
|
||||
left: auto;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
|
||||
&__content {
|
||||
|
||||
&-enter {
|
||||
animation: fade-in 0.25s both;
|
||||
}
|
||||
|
||||
&-leave {
|
||||
animation: fade-out 0.25s both;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
|
||||
&__content {
|
||||
|
||||
&__container {
|
||||
padding: 25px 30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
.sidebar {
|
||||
box-shadow: inset -1px 0 $sidebar--border;
|
||||
color: $sidebar--foreground;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
|
||||
&__item {
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
&__list {
|
||||
background: $torrent-list--background;
|
||||
box-shadow: -1px -1px $torrent-list--border;
|
||||
border-radius: 4px 4px 0 0;
|
||||
box-shadow: 0 0 0 1px $torrent-list--border;
|
||||
list-style: none;
|
||||
opacity: 1;
|
||||
overflow: auto;
|
||||
25
client/source/sass/style.scss
Normal file
@@ -0,0 +1,25 @@
|
||||
@import "../../../node_modules/inuit-defaults/settings.defaults";
|
||||
@import "../../../node_modules/inuit-functions/tools.functions";
|
||||
@import "../../../node_modules/inuit-mixins/tools.mixins";
|
||||
@import "../../../node_modules/inuit-normalize/generic.normalize";
|
||||
@import "../../../node_modules/inuit-reset/generic.reset";
|
||||
@import "../../../node_modules/inuit-box-sizing/generic.box-sizing";
|
||||
@import "../../../node_modules/inuit-page/base.page";
|
||||
|
||||
@import "tools/variables";
|
||||
|
||||
@import "base/main";
|
||||
@import "base/layout";
|
||||
@import "base/typography";
|
||||
@import "base/animations";
|
||||
@import "base/form-elements";
|
||||
|
||||
@import "objects/action-bar";
|
||||
@import "objects/client-stats";
|
||||
@import "objects/content";
|
||||
@import "objects/dropdown";
|
||||
@import "objects/sidebar";
|
||||
@import "objects/modals";
|
||||
@import "objects/progress-bar";
|
||||
@import "objects/status-filter";
|
||||
@import "objects/torrents";
|
||||
@@ -1,8 +1,10 @@
|
||||
$blue: #258de5;
|
||||
$green: #39ce83;
|
||||
|
||||
$background: #e3e5e5;
|
||||
$foreground: #686469;
|
||||
$background: #1a2f3d;
|
||||
$foreground: #53718a;
|
||||
|
||||
$main-content--background: #e9eef2;
|
||||
|
||||
// form elements
|
||||
$form--label--foreground: #979999;
|
||||
@@ -20,7 +22,7 @@ $button--primary--foreground: #fff;
|
||||
$button--primary--background: $green;
|
||||
|
||||
// action bar
|
||||
$action-bar--background: rgba(#fff, 0.75);
|
||||
$action-bar--background: transparent;
|
||||
$action-bar--foreground: #1b1a1c;
|
||||
$action-bar--group--border: rgba(#7a8080, 0.15);
|
||||
|
||||
@@ -30,12 +32,8 @@ $action--background--hover: rgba(#333e4a, 0.05);
|
||||
$action--border--hover: rgba(#333e4a, 0.15);
|
||||
|
||||
// filter bar
|
||||
$sidebar--foreground: #626466;
|
||||
$sidebar--border: #161316;
|
||||
|
||||
$client-stats--primary: #333332;
|
||||
$client-stats--secondary: #999997;
|
||||
$client-stats--icon: #8c8c8a;
|
||||
$sidebar--foreground: #53718a;
|
||||
$sidebar--border: rgba(darken($sidebar--foreground, 40%), 0.3);
|
||||
|
||||
$client-stats--download--primary--foreground: #2bae6c;
|
||||
$client-stats--download--secondary--foreground: rgba($client-stats--download--primary--foreground, 0.75);
|
||||
@@ -49,30 +47,30 @@ $client-stats--upload--graph--stroke: rgba(#2387d9, 0.4);
|
||||
$client-stats--upload--graph--fill--top: rgba(#2387d9, 0.2);
|
||||
$client-stats--upload--graph--fill--bottom: rgba(#2387d9, 0);
|
||||
|
||||
$client-stats--limits--foreground: #999997;
|
||||
$client-stats--limits--foreground--hover: darken($client-stats--limits--foreground, 20%);
|
||||
$client-stats--limits--foreground: $foreground;
|
||||
$client-stats--limits--icon--hover: $blue;
|
||||
|
||||
$search-torrents--background: #d9ddde;
|
||||
$search-torrents--background--active: #39ce83;
|
||||
$search-torrents--border: rgba(#babfc2, 0.5);
|
||||
$search-torrents--border--active: #33b574;
|
||||
$search-torrents--foreground: #625e66;
|
||||
$search-torrents--base: #091824;
|
||||
$search-torrents--background: rgba($search-torrents--base, 0.3);
|
||||
$search-torrents--background--active: $green;
|
||||
$search-torrents--border: rgba($search-torrents--background, 0.4);
|
||||
$search-torrents--border--active: $search-torrents--background--active;
|
||||
$search-torrents--foreground: $sidebar--foreground;
|
||||
$search-torrents--foreground--active: #1e8954;
|
||||
$search-torrents--placeholder: rgba(#909799, 0.7);
|
||||
$search-torrents--placeholder: rgba($sidebar--foreground, 0.4);
|
||||
$search-torrents--placeholder--active: #2cad6d;
|
||||
|
||||
$search-torrents--icon--foreground: #8a9ca6;
|
||||
$search-torrents--icon--foreground: $sidebar--foreground;
|
||||
$search-torrents--icon--foreground--active: #2c9e65;
|
||||
|
||||
$status-filter--foreground: #626466;
|
||||
$status-filter--foreground--header: #b5b5b3;
|
||||
$status-filter--foreground: $sidebar--foreground;
|
||||
$status-filter--foreground--header: rgba($status-filter--foreground, 0.5);
|
||||
$status-filter--foreground--active: $blue;
|
||||
$status-filter--foreground--hover: darken($status-filter--foreground, 15%);
|
||||
$status-filter--foreground--hover: lighten($status-filter--foreground, 15%);
|
||||
|
||||
// torrents list
|
||||
$torrent-list--background: #fff;
|
||||
$torrent-list--border: rgba(#000, 0.1);
|
||||
$torrent-list--border: rgba($background, 0.1);
|
||||
|
||||
$torrent--primary--foreground: #333332;
|
||||
$torrent--primary--foreground--stopped: rgba(#333332, 0.5);
|
||||
@@ -93,23 +91,23 @@ $torrent--background--selected: $blue;
|
||||
$progress-bar--background: #e3e5e5;
|
||||
$progress-bar--background--selected: rgba(#fff, 0.5);
|
||||
$progress-bar--background--selected--stopped: rgba(#fff, 0.5);
|
||||
$progress-bar--fill: #39ce83;
|
||||
$progress-bar--fill: $green;
|
||||
|
||||
$progress-bar--fill--stopped: #e3e5e5;
|
||||
$progress-bar--fill--completed: $blue;
|
||||
$progress-bar--fill--selected: #fff;
|
||||
|
||||
// dropdown menu
|
||||
$dropdown--background: #2c2d2e;
|
||||
$dropdown--foreground: #979899;
|
||||
$dropdown--container--border: #d4d3d1;
|
||||
$dropdown--label: $status-filter--foreground--header;
|
||||
$dropdown--value: #807f7e;
|
||||
$dropdown--header--background: #282929;
|
||||
$dropdown--header--foreground: #e3e5e5;
|
||||
$dropdown--header--border: #383b3f;
|
||||
$dropdown--item--background--hover: #3d3f42;
|
||||
$dropdown--item--foreground--hover: #bdbebf;
|
||||
$dropdown--background: rgba(#fff, 0.98);;
|
||||
$dropdown--foreground: #95a2ad;
|
||||
$dropdown--container--border: rgba($background, 0.1);
|
||||
$dropdown--container--shadow: rgba($background, 0.3);
|
||||
$dropdown--label: #abbac7;
|
||||
$dropdown--value: #8899a8;
|
||||
$dropdown--header--border: rgba($background, 0.05);
|
||||
$dropdown--item--background--hover: rgba($main-content--background, 0.4);
|
||||
$dropdown--item--foreground--hover: darken($dropdown--foreground, 10%);
|
||||
$dropdown--item--foreground--active: $blue;
|
||||
|
||||
// modal windows
|
||||
$modal--background: #2f2c30;
|
||||
@@ -22,6 +22,26 @@ export function addTorrent(hashes) {
|
||||
}
|
||||
};
|
||||
|
||||
// CLIENT_RECEIVE_TRANSFER_DATA
|
||||
|
||||
export function fetchTransferData() {
|
||||
return function(dispatch) {
|
||||
return axios.get('/client/stats')
|
||||
.then((json = {}) => {
|
||||
return json.data;
|
||||
})
|
||||
.then(transferData => {
|
||||
dispatch({
|
||||
type: 'CLIENT_RECEIVE_TRANSFER_DATA',
|
||||
payload: transferData
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('error', error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function fetchTorrents() {
|
||||
return function(dispatch) {
|
||||
dispatch({
|
||||
@@ -34,7 +54,7 @@ export function fetchTorrents() {
|
||||
.then((json = {}) => {
|
||||
return json.data;
|
||||
})
|
||||
.then((torrents) => {
|
||||
.then(torrents => {
|
||||
dispatch({
|
||||
type: 'RECEIVE_TORRENTS',
|
||||
payload: {
|
||||
@@ -7,6 +7,7 @@ import UIActions from '../../actions/UIActions';
|
||||
const methodsToBind = [
|
||||
'componentDidMount',
|
||||
'componentWillUnmount',
|
||||
'getHeader',
|
||||
'getMenu',
|
||||
'onItemSelect',
|
||||
'onDropdownClick',
|
||||
@@ -19,7 +20,7 @@ export default class SortDropdown extends React.Component {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
isExpanded: false
|
||||
isExpanded: true
|
||||
};
|
||||
|
||||
methodsToBind.forEach((method) => {
|
||||
@@ -35,6 +36,15 @@ export default class SortDropdown extends React.Component {
|
||||
window.removeEventListener('click', this.onExternalClick);
|
||||
}
|
||||
|
||||
getHeader() {
|
||||
return (
|
||||
<a className="dropdown__button" onClick={this.onDropdownClick}>
|
||||
<label className="dropdown__label">Sort By</label>
|
||||
<span className="dropdown__value">{this.props.selectedItem.displayName}</span>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
getMenu() {
|
||||
let sortableProperties = [
|
||||
{
|
||||
@@ -80,18 +90,26 @@ export default class SortDropdown extends React.Component {
|
||||
];
|
||||
|
||||
let menuItems = sortableProperties.map(function(property, index) {
|
||||
let classes = classnames({
|
||||
'dropdown__item': true,
|
||||
'is-selected': this.props.selectedItem.property === property.property
|
||||
})
|
||||
return (
|
||||
<li className="dropdown__content__item" key={index} onClick={this.onItemSelect.bind(this, property)}>
|
||||
<li className={classes} key={index} onClick={this.onItemSelect.bind(this, property)}>
|
||||
{property.displayName}
|
||||
</li>
|
||||
);
|
||||
}, this);
|
||||
|
||||
return (
|
||||
<ul className="dropdown__content">
|
||||
<li className="dropdown__content__header">Sort Torrents</li>
|
||||
{menuItems}
|
||||
</ul>
|
||||
<div className="dropdown__content">
|
||||
<div className="dropdown__header">
|
||||
{this.getHeader()}
|
||||
</div>
|
||||
<ul className="dropdown__items">
|
||||
{menuItems}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -132,10 +150,7 @@ export default class SortDropdown extends React.Component {
|
||||
|
||||
return (
|
||||
<div className={classes}>
|
||||
<a className="dropdown__button" onClick={this.onDropdownClick}>
|
||||
<label className="dropdown__label">Sort By</label>
|
||||
<span className="dropdown__value">{this.props.selectedItem.displayName}</span>
|
||||
</a>
|
||||
{this.getHeader()}
|
||||
<CSSTransitionGroup
|
||||
transitionName="dropdown__content"
|
||||
transitionEnterTimeout={250}
|
||||
144
client/source/scripts/components/sidebar/ClientStats.js
Normal file
@@ -0,0 +1,144 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import format from '../../helpers/formatData';
|
||||
import Icon from '../icons/Icon';
|
||||
import LineChart from './LineChart';
|
||||
|
||||
const methodsToBind = [
|
||||
'componentDidMount',
|
||||
'componentWillReceiveProps',
|
||||
'shouldComponentUpdate'
|
||||
];
|
||||
|
||||
export default class ClientStats extends React.Component {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
sidebarWidth: 0,
|
||||
transferData: {
|
||||
download: [],
|
||||
upload: []
|
||||
}
|
||||
};
|
||||
|
||||
methodsToBind.forEach((method) => {
|
||||
this[method] = this[method].bind(this);
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.setState({
|
||||
sidebarWidth: ReactDOM.findDOMNode(this).offsetWidth
|
||||
});
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
// check that the transferData was actually updated since the last component
|
||||
// update. if it was updated, add the latest download & upload rates to the
|
||||
// end of the array and remove the first element in the array. if the arrays
|
||||
// are empty, fill in zeros for the first n entries.
|
||||
if (nextProps.transferData.updatedAt !== this.props.transferData.updatedAt) {
|
||||
let index = 0;
|
||||
let uploadRateHistory = Object.assign([], this.state.transferData.upload);
|
||||
let downloadRateHistory = Object.assign([], this.state.transferData.download);
|
||||
|
||||
if (uploadRateHistory.length === this.props.historyLength) {
|
||||
uploadRateHistory.shift();
|
||||
downloadRateHistory.shift();
|
||||
uploadRateHistory.push(parseInt(nextProps.transferData.upload.rate));
|
||||
downloadRateHistory.push(parseInt(nextProps.transferData.download.rate));
|
||||
} else {
|
||||
while (index < this.props.historyLength) {
|
||||
if (index < this.props.historyLength - 1) {
|
||||
uploadRateHistory[index] = 0;
|
||||
downloadRateHistory[index] = 0;
|
||||
} else {
|
||||
uploadRateHistory[index] = parseInt(nextProps.transferData.upload.rate);
|
||||
downloadRateHistory[index] = parseInt(nextProps.transferData.download.rate);
|
||||
}
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
transferData: {
|
||||
download: downloadRateHistory,
|
||||
upload: uploadRateHistory
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
if (nextProps.transferData.updatedAt !== this.props.transferData.updatedAt) {
|
||||
return true;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let uploadRate = format.data(this.props.transferData.upload.rate, '/s');
|
||||
let uploadTotal = format.data(this.props.transferData.upload.total);
|
||||
let downloadRate = format.data(this.props.transferData.download.rate, '/s');
|
||||
let downloadTotal = format.data(this.props.transferData.download.total);
|
||||
|
||||
return (
|
||||
<div className="client-stats sidebar__item">
|
||||
<div className="client-stat client-stat--download">
|
||||
<span className="client-stat__icon">
|
||||
<Icon icon="download" />
|
||||
</span>
|
||||
<div className="client-stat__data">
|
||||
<div className="client-stat__data--primary">
|
||||
{downloadRate.value}
|
||||
<em className="unit">{downloadRate.unit}</em>
|
||||
</div>
|
||||
<div className="client-stat__data--secondary">
|
||||
{downloadTotal.value}
|
||||
<em className="unit">{downloadTotal.unit}</em> Downloaded
|
||||
</div>
|
||||
</div>
|
||||
<LineChart
|
||||
data={this.state.transferData.download}
|
||||
height={100}
|
||||
id="graph--download"
|
||||
slug="graph--download"
|
||||
width={this.state.sidebarWidth} />
|
||||
</div>
|
||||
<div className="client-stat client-stat--upload">
|
||||
<span className="client-stat__icon">
|
||||
<Icon icon="upload" />
|
||||
</span>
|
||||
<div className="client-stat__data">
|
||||
<div className="client-stat__data--primary">
|
||||
{uploadRate.value}
|
||||
<em className="unit">{uploadRate.unit}</em>
|
||||
</div>
|
||||
<div className="client-stat__data--secondary">
|
||||
{uploadTotal.value}
|
||||
<em className="unit">{uploadTotal.unit}</em> Uploaded
|
||||
</div>
|
||||
</div>
|
||||
<LineChart
|
||||
data={this.state.transferData.upload}
|
||||
height={100}
|
||||
id="graph--upload"
|
||||
slug="graph--upload"
|
||||
width={this.state.sidebarWidth} />
|
||||
</div>
|
||||
<button className="client-stats client-stat--limits">
|
||||
<Icon icon="limits" /> Limits
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ClientStats.defaultProps = {
|
||||
historyLength: 20
|
||||
};
|
||||
@@ -7,7 +7,7 @@ export default class LineChart extends React.Component {
|
||||
super();
|
||||
}
|
||||
|
||||
componentWillUpdate() {
|
||||
componentDidUpdate() {
|
||||
let graph = d3.select('#' + this.props.id);
|
||||
let lineData = this.props.data;
|
||||
let margin = {
|
||||
@@ -22,11 +22,11 @@ export default class LineChart extends React.Component {
|
||||
.linear()
|
||||
.range([0, width])
|
||||
.domain([
|
||||
d3.min(lineData, function(d) {
|
||||
return d.x;
|
||||
d3.min(lineData, function(d,i) {
|
||||
return i;
|
||||
}),
|
||||
d3.max(lineData, function(d) {
|
||||
return d.x;
|
||||
d3.max(lineData, function(d,i) {
|
||||
return i;
|
||||
})
|
||||
]);
|
||||
|
||||
@@ -36,33 +36,33 @@ export default class LineChart extends React.Component {
|
||||
.range([height - margin.bottom - margin.top, 0])
|
||||
.domain([
|
||||
d3.min(lineData, function(d) {
|
||||
return d.y;
|
||||
return d;
|
||||
}),
|
||||
d3.max(lineData, function(d) {
|
||||
return d.y;
|
||||
return d;
|
||||
})
|
||||
]);
|
||||
|
||||
let lineFunc = d3
|
||||
.svg
|
||||
.line()
|
||||
.x(function(d) {
|
||||
return xRange(d.x);
|
||||
.x(function(d,i) {
|
||||
return xRange(i);
|
||||
})
|
||||
.y(function(d) {
|
||||
return yRange(d.y);
|
||||
return yRange(d);
|
||||
})
|
||||
.interpolate('basis');
|
||||
|
||||
let areaFunc = d3
|
||||
.svg
|
||||
.area()
|
||||
.x(function(d) {
|
||||
return xRange(d.x);
|
||||
.x(function(d,i) {
|
||||
return xRange(i);
|
||||
})
|
||||
.y0(height)
|
||||
.y1(function(d) {
|
||||
return yRange(d.y);
|
||||
return yRange(d);
|
||||
})
|
||||
.interpolate('basis');
|
||||
|
||||
@@ -57,7 +57,7 @@ export default class FilterBar extends React.Component {
|
||||
<Action label="Pause Torrent" slug="pause-torrent" icon="pause"
|
||||
clickHandler={this.handlePause} />
|
||||
</div>
|
||||
<div className="action-bar__group">
|
||||
<div className="action-bar__group action-bar__group--has-divider">
|
||||
<AddTorrent />
|
||||
</div>
|
||||
</div>
|
||||
@@ -2,8 +2,8 @@ import { connect } from 'react-redux';
|
||||
import React from 'react';
|
||||
|
||||
import ActionBar from '../containers/ActionBar';
|
||||
import { fetchTorrents } from '../actions/ClientActions';
|
||||
import FilterBar from './FilterBar';
|
||||
import { fetchTorrents, fetchTransferData } from '../actions/ClientActions';
|
||||
import Sidebar from './Sidebar';
|
||||
import rootSelector from '../selectors/rootSelector';
|
||||
import TorrentList from '../containers/TorrentList';
|
||||
import TorrentListHeader from '../components/torrent-list/TorrentListHeader';
|
||||
@@ -11,7 +11,8 @@ import TorrentListHeader from '../components/torrent-list/TorrentListHeader';
|
||||
const methodsToBind = [
|
||||
'componentWillMount',
|
||||
'componentWillUnmount',
|
||||
'getClientData'
|
||||
'getTransferData',
|
||||
'getTorrents'
|
||||
];
|
||||
|
||||
class FloodApp extends React.Component {
|
||||
@@ -20,8 +21,9 @@ class FloodApp extends React.Component {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
clientDataFetchInterval: null,
|
||||
count: 0,
|
||||
dataFetchInterval: null
|
||||
torrentFetchInterval: null
|
||||
};
|
||||
|
||||
methodsToBind.forEach((method) => {
|
||||
@@ -30,33 +32,45 @@ class FloodApp extends React.Component {
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
let getClientData = this.getClientData;
|
||||
let getTorrents = this.getTorrents;
|
||||
let getTransferData = this.getTransferData;
|
||||
|
||||
this.state.dataFetchInterval = setInterval(function() {
|
||||
getClientData();
|
||||
this.state.torrentFetchInterval = setInterval(function() {
|
||||
getTorrents();
|
||||
}, 5000);
|
||||
|
||||
getClientData();
|
||||
this.state.clientDataFetchInterval = setInterval(function() {
|
||||
getTransferData();
|
||||
}, 5000);
|
||||
|
||||
this.getTorrents();
|
||||
this.getTransferData();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
clearInterval(this.state.dataFetchInterval);
|
||||
clearInterval(this.state.torrentFetchInterval);
|
||||
clearInterval(this.state.clientDataFetchInterval);
|
||||
}
|
||||
|
||||
getClientData() {
|
||||
getTransferData() {
|
||||
this.props.dispatch(fetchTransferData());
|
||||
}
|
||||
|
||||
getTorrents() {
|
||||
this.props.dispatch(fetchTorrents());
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="flood">
|
||||
<FilterBar dispatch={this.props.dispatch} uiStore={this.props.ui} />
|
||||
<main className="main">
|
||||
<Sidebar dispatch={this.props.dispatch}
|
||||
filterBy={this.props.ui.torrentList.filterBy}
|
||||
transferData={this.props.client.transfers}/>
|
||||
<main className="content">
|
||||
<ActionBar dispatch={this.props.dispatch} uiStore={this.props.ui} />
|
||||
<TorrentList dispatch={this.props.dispatch}
|
||||
selectedTorrents={this.props.ui.torrentList.selected}
|
||||
torrents={this.props.torrents}
|
||||
uiStore={this.props.ui}
|
||||
isFetching={this.props.ui.fetchingData} />
|
||||
</main>
|
||||
</div>
|
||||
@@ -31,12 +31,12 @@ export default class Sidebar extends React.Component {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<nav className="sidebar">
|
||||
<ClientStats />
|
||||
<aside className="sidebar">
|
||||
<ClientStats transferData={this.props.transferData} />
|
||||
<SearchBox handleSearchChange={this.handleSearchChange} />
|
||||
<StatusFilters handleFilterChange={this.handleFilterChange}
|
||||
activeFilter={this.props.filterBy} />
|
||||
</nav>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
42
client/source/scripts/reducers/clientReducer.js
Normal file
@@ -0,0 +1,42 @@
|
||||
const initialState = {
|
||||
transfers: {
|
||||
updatedAt: 0,
|
||||
download: {
|
||||
rate: 0,
|
||||
total: 0
|
||||
},
|
||||
upload: {
|
||||
rate: 0,
|
||||
total: 0
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default function clientReducer(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
|
||||
case 'CLIENT_RECEIVE_TRANSFER_DATA':
|
||||
return Object.assign(
|
||||
{},
|
||||
state,
|
||||
{
|
||||
...state,
|
||||
transfers: {
|
||||
...state.transfers,
|
||||
updatedAt: Date.now(),
|
||||
download: {
|
||||
rate: action.payload.downloadRate,
|
||||
total: action.payload.downloadTotal
|
||||
},
|
||||
upload: {
|
||||
rate: action.payload.uploadRate,
|
||||
total: action.payload.uploadTotal
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
1436
dist/public/scripts/app.js
vendored
1
dist/public/scripts/app.js.map
vendored
1197
dist/public/stylesheets/style.css
vendored
1
dist/public/stylesheets/style.css.map
vendored
8
dist/views/layout.jade
vendored
@@ -1,8 +0,0 @@
|
||||
doctype html
|
||||
html
|
||||
head
|
||||
title= title
|
||||
link(rel='stylesheet' href='/stylesheets/style.css')
|
||||
body
|
||||
block content
|
||||
script(src='/scripts/app.js')
|
||||
21
gulpfile.js
@@ -17,12 +17,12 @@ var packageInfo = require('./package');
|
||||
var development = process.env.NODE_ENV === 'development';
|
||||
|
||||
var dirs = {
|
||||
src: 'source',
|
||||
dist: 'dist',
|
||||
src: 'client/source',
|
||||
dist: 'server/assets',
|
||||
js: 'scripts',
|
||||
jsDist: 'public/scripts',
|
||||
jsDist: '',
|
||||
styles: 'sass',
|
||||
stylesDist: 'public/stylesheets',
|
||||
stylesDist: '',
|
||||
img: 'images',
|
||||
imgDist: 'images'
|
||||
};
|
||||
@@ -92,7 +92,7 @@ function eslintFn () {
|
||||
gulp.task('eslint', eslintFn);
|
||||
|
||||
gulp.task('images', function () {
|
||||
return gulp.src([dirs.img + '/**/*.*', '!' + dirs.img + '/**/_exports/**/*.*'])
|
||||
return gulp.src(dirs.src + '/' + dirs.img + '/**/*.*')
|
||||
.pipe(imagemin({
|
||||
progressive: true,
|
||||
svgoPlugins: [{removeViewBox: false}]
|
||||
@@ -103,7 +103,15 @@ gulp.task('images', function () {
|
||||
gulp.task('sass', function () {
|
||||
return gulp.src(dirs.src + '/' + dirs.styles + '/' + files.mainStyles + '.scss')
|
||||
.pipe(gulpif(development, sourcemaps.init()))
|
||||
.pipe(sass())
|
||||
.pipe(sass().on('error', function(error) {
|
||||
gutil.log(
|
||||
gutil.colors.green('Sass Error!\n'),
|
||||
'\n',
|
||||
error.messageFormatted,
|
||||
'\n'
|
||||
);
|
||||
this.emit('end');
|
||||
}))
|
||||
.pipe(autoprefixer())
|
||||
.pipe(gulpif(development, sourcemaps.write('.')))
|
||||
.pipe(gulp.dest(dirs.dist + '/' + dirs.stylesDist))
|
||||
@@ -160,6 +168,7 @@ gulp.task('webpack', function (callback) {
|
||||
// This runs after webpack's internal watch rebuild.
|
||||
// eslintFn();
|
||||
if (development) {
|
||||
console.log('reloading from webpack');
|
||||
browserSync.reload();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"livereload": "NODE_ENV='development' ./node_modules/.bin/gulp livereload",
|
||||
"start": "node ./dist/bin/www"
|
||||
"start": "node ./server/bin/www"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.7.0",
|
||||
|
||||
@@ -12,6 +12,8 @@ var client = require('./routes/client');
|
||||
var app = express();
|
||||
|
||||
// view engine setup
|
||||
console.log(__dirname);
|
||||
|
||||
app.set('views', path.join(__dirname, 'views'));
|
||||
app.set('view engine', 'jade');
|
||||
|
||||
@@ -21,7 +23,7 @@ app.use(logger('dev'));
|
||||
app.use(bodyParser.json());
|
||||
app.use(bodyParser.urlencoded({ extended: false }));
|
||||
app.use(cookieParser());
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
app.use(express.static(path.join(__dirname, 'assets')));
|
||||
|
||||
app.use(function(req, res, next) {
|
||||
req.socket.on("error", function(err) {
|
||||
@@ -78,6 +78,7 @@ client.prototype.getTorrentList = function(callback) {
|
||||
|
||||
callback(null, torrents);
|
||||
}, function(error) {
|
||||
console.log(error);
|
||||
callback(error, null)
|
||||
});
|
||||
};
|
||||
@@ -7,11 +7,18 @@ var Serializer = require('./serializer');
|
||||
var rtorrent = {};
|
||||
|
||||
rtorrent.get = function(api, array) {
|
||||
var stream = net.connect(5000, 'localhost');
|
||||
var stream = net.connect({
|
||||
port: 5000,
|
||||
host: 'localhost'
|
||||
});
|
||||
var deferred = Q.defer();
|
||||
var xml;
|
||||
var length = 0;
|
||||
|
||||
stream.on('error', function(error) {
|
||||
console.log(error);
|
||||
});
|
||||
|
||||
stream.setEncoding('UTF8');
|
||||
|
||||
try {
|
||||
@@ -9,9 +9,9 @@ router.get('/', function(req, res, next) {
|
||||
|
||||
router.get('/stats', function(req, res, next) {
|
||||
|
||||
clientStats.getStats(function(error, results) {
|
||||
res.json(results);
|
||||
});
|
||||
clientStats.getStats(function(error, results) {
|
||||
res.json(results);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
8
server/views/layout.jade
Normal file
@@ -0,0 +1,8 @@
|
||||
doctype html
|
||||
html
|
||||
head
|
||||
title= title
|
||||
link(rel='stylesheet' href='/style.css')
|
||||
body
|
||||
block content
|
||||
script(src='/app.js')
|
||||
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 59.8"><path fill-rule="evenodd" clip-rule="evenodd" fill="#010101" d="M53.7 25.3h-19v-19h-9.4v19h-19v9.4h19v19h9.4v-19h19"/></svg>
|
||||
|
Before Width: | Height: | Size: 186 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60"><path d="M13.5 51h11V9h-11v42zm22-42v42h11V9h-11z" fill-rule="evenodd" clip-rule="evenodd"/></svg>
|
||||
|
Before Width: | Height: | Size: 158 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60"><path d="M13.1 9.5L46.9 30 13.1 50.5v-41z" fill-rule="evenodd" clip-rule="evenodd"/></svg>
|
||||
|
Before Width: | Height: | Size: 150 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60"><path d="M11.9 11.9H48v36.2H11.9V11.9z" fill-rule="evenodd" clip-rule="evenodd"/></svg>
|
||||
|
Before Width: | Height: | Size: 147 B |
@@ -1,85 +0,0 @@
|
||||
.dropdown {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
|
||||
&__content {
|
||||
background: $dropdown--background;
|
||||
color: $dropdown--foreground;
|
||||
left: 0;
|
||||
margin-top: -5px;
|
||||
position: absolute;
|
||||
text-align: left;
|
||||
top: 100%;
|
||||
z-index: 2;
|
||||
|
||||
&__header {
|
||||
background: $dropdown--header--background;
|
||||
border-bottom: 1px solid $dropdown--header--border;
|
||||
font-size: 1.15em;
|
||||
margin-bottom: 5px;
|
||||
padding: 18px 30px 15px 30px;
|
||||
}
|
||||
|
||||
&:after {
|
||||
border-bottom: 6px solid $dropdown--header--background;
|
||||
border-left: 6px solid transparent;
|
||||
border-right: 6px solid transparent;
|
||||
content: '';
|
||||
display: block;
|
||||
left: 20px;
|
||||
position: absolute;
|
||||
top: -6px;
|
||||
|
||||
.dropdown--align-right & {
|
||||
left: auto;
|
||||
right: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown--align-right & {
|
||||
left: auto;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
|
||||
&__content {
|
||||
|
||||
&-enter {
|
||||
animation: fade-in 0.25s both;
|
||||
}
|
||||
|
||||
&-leave {
|
||||
animation: fade-out 0.25s both;
|
||||
}
|
||||
|
||||
&__item {
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
font-size: 0.9em;
|
||||
padding: 5px 30px;
|
||||
transition: background 0.25s, color 0.25s;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: $dropdown--item--background--hover;
|
||||
color: $dropdown--item--foreground--hover;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
|
||||
&__content {
|
||||
|
||||
&__container {
|
||||
padding: 25px 30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
@import "../../node_modules/inuit-defaults/settings.defaults";
|
||||
@import "../../node_modules/inuit-functions/tools.functions";
|
||||
@import "../../node_modules/inuit-mixins/tools.mixins";
|
||||
@import "../../node_modules/inuit-normalize/generic.normalize";
|
||||
@import "../../node_modules/inuit-reset/generic.reset";
|
||||
@import "../../node_modules/inuit-box-sizing/generic.box-sizing";
|
||||
@import "../../node_modules/inuit-page/base.page";
|
||||
|
||||
@import "tools/variables";
|
||||
|
||||
@import "base/main";
|
||||
@import "base/layout";
|
||||
@import "base/typography";
|
||||
@import "base/animations";
|
||||
@import "base/form-elements";
|
||||
|
||||
@import "objects/action-bar";
|
||||
@import "objects/client-stats";
|
||||
@import "objects/dropdown";
|
||||
@import "objects/sidebar";
|
||||
@import "objects/modals";
|
||||
@import "objects/progress-bar";
|
||||
@import "objects/status-filter";
|
||||
@import "objects/torrents";
|
||||
@@ -1,108 +0,0 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import format from '../../helpers/formatData';
|
||||
import Icon from '../icons/Icon';
|
||||
import LineChart from './LineChart';
|
||||
|
||||
const methodsToBind = [
|
||||
'componentDidMount',
|
||||
'_onChange'
|
||||
];
|
||||
|
||||
export default class ClientStats extends React.Component {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
clientStats: {
|
||||
currentSpeed: {
|
||||
upload: 0,
|
||||
download: 0
|
||||
},
|
||||
historicalSpeed: {
|
||||
download: [],
|
||||
upload: []
|
||||
},
|
||||
transferred: {
|
||||
upload: 0,
|
||||
download: 0
|
||||
}
|
||||
},
|
||||
sidebarWidth: 0
|
||||
};
|
||||
|
||||
methodsToBind.forEach((method) => {
|
||||
this[method] = this[method].bind(this);
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.setState({
|
||||
sidebarWidth: ReactDOM.findDOMNode(this).offsetWidth
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
let uploadSpeed = format.data(this.state.clientStats.currentSpeed.upload, '/s');
|
||||
let uploadTotal = format.data(this.state.clientStats.transferred.upload);
|
||||
let downloadSpeed = format.data(this.state.clientStats.currentSpeed.download, '/s');
|
||||
let downloadTotal = format.data(this.state.clientStats.transferred.download);
|
||||
|
||||
return (
|
||||
<div className="client-stats sidebar__item">
|
||||
<div className="client-stat client-stat--download">
|
||||
<span className="client-stat__icon">
|
||||
<Icon icon="download" />
|
||||
</span>
|
||||
<div className="client-stat__data">
|
||||
<div className="client-stat__data--primary">
|
||||
{downloadSpeed.value}
|
||||
<em className="unit">{downloadSpeed.unit}</em>
|
||||
</div>
|
||||
<div className="client-stat__data--secondary">
|
||||
{downloadTotal.value}
|
||||
<em className="unit">{downloadTotal.unit}</em> Downloaded
|
||||
</div>
|
||||
</div>
|
||||
<LineChart
|
||||
data={this.state.clientStats.historicalSpeed.download}
|
||||
height={100}
|
||||
id="graph--download"
|
||||
slug="graph--download"
|
||||
width={this.state.sidebarWidth} />
|
||||
</div>
|
||||
<div className="client-stat client-stat--upload">
|
||||
<span className="client-stat__icon">
|
||||
<Icon icon="upload" />
|
||||
</span>
|
||||
<div className="client-stat__data">
|
||||
<div className="client-stat__data--primary">
|
||||
{uploadSpeed.value}
|
||||
<em className="unit">{uploadSpeed.unit}</em>
|
||||
</div>
|
||||
<div className="client-stat__data--secondary">
|
||||
{uploadTotal.value}
|
||||
<em className="unit">{uploadTotal.unit}</em> Uploaded
|
||||
</div>
|
||||
</div>
|
||||
<LineChart
|
||||
data={this.state.clientStats.historicalSpeed.upload}
|
||||
height={100}
|
||||
id="graph--upload"
|
||||
slug="graph--upload"
|
||||
width={this.state.sidebarWidth} />
|
||||
</div>
|
||||
<button className="client-stats client-stat--limits">
|
||||
<Icon icon="limits" /> Limits
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
_onChange() {
|
||||
this.setState(getClientStats);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
export default function client(state = {}, action) {
|
||||
switch (action.type) {
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||