mirror of
https://github.com/zoriya/flood.git
synced 2026-06-01 18:47:44 +00:00
Merge pull request #78 from jfurrow/performance/improve-torrent-list-rendering
Improve scrolling performance in torrent list
This commit is contained in:
@@ -22,6 +22,7 @@ $progress-bar--fill--selected: #fff;
|
||||
$progress-bar--fill--stopped: #e3e5e5;
|
||||
|
||||
.progress-bar {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
transition: opacity 0.25s;
|
||||
width: 100%;
|
||||
@@ -66,15 +67,13 @@ $progress-bar--fill--stopped: #e3e5e5;
|
||||
}
|
||||
|
||||
&__fill {
|
||||
align-items: center;
|
||||
background: $progress-bar--fill;
|
||||
bottom: 0;
|
||||
display: block;
|
||||
height: $progress-bar--height;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
transform-origin: 0 50%;
|
||||
transition: background 0.25s, width 0.25s;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
|
||||
.is-seeding & {
|
||||
@@ -101,6 +100,7 @@ $progress-bar--fill--stopped: #e3e5e5;
|
||||
&__wrapper {
|
||||
flex: 1 1 auto;
|
||||
position: relative;
|
||||
height: 3px;
|
||||
|
||||
&:after {
|
||||
background: $progress-bar--fill;
|
||||
@@ -110,8 +110,7 @@ $progress-bar--fill--stopped: #e3e5e5;
|
||||
opacity: 0.5;
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
top: 1px;
|
||||
transition: background 0.25s, opacity 0.25s;
|
||||
width: 100%;
|
||||
|
||||
|
||||
@@ -197,8 +197,8 @@ $more-info--border: $textbox-repeater--button--border;
|
||||
&__details {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-flow: row wrap;
|
||||
flex: 1 1 auto;
|
||||
flex-flow: row;
|
||||
list-style: none;
|
||||
|
||||
&--primary,
|
||||
|
||||
@@ -2,18 +2,30 @@ import classnames from 'classnames';
|
||||
import React from 'react';
|
||||
import {Scrollbars} from 'react-custom-scrollbars';
|
||||
|
||||
const METHODS_TO_BIND = ['getHorizontalThumb', 'getVerticalThumb'];
|
||||
|
||||
export default class CustomScrollbar extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
METHODS_TO_BIND.forEach((method) => {
|
||||
this[method] = this[method].bind(this);
|
||||
});
|
||||
}
|
||||
|
||||
getHorizontalThumb(props) {
|
||||
return (
|
||||
<div {...props}
|
||||
className="scrollbars__thumb scrollbars__thumb--horizontal"/>
|
||||
className="scrollbars__thumb scrollbars__thumb--horizontal"
|
||||
onMouseUp={this.props.onThumbMouseUp} />
|
||||
);
|
||||
}
|
||||
|
||||
getVerticalThumb(props) {
|
||||
return (
|
||||
<div {...props}
|
||||
className="scrollbars__thumb scrollbars__thumb--vertical"/>
|
||||
className="scrollbars__thumb scrollbars__thumb--vertical"
|
||||
onMouseUp={this.props.onThumbMouseUp} />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -39,6 +51,8 @@ export default class CustomScrollbar extends React.Component {
|
||||
renderThumbHorizontal={this.getHorizontalThumb}
|
||||
renderThumbVertical={this.getVerticalThumb}
|
||||
onScroll={this.props.nativeScrollHandler}
|
||||
onScrollStart={this.props.onScrollStart}
|
||||
onScrollStop={this.props.onScrollStop}
|
||||
onScrollFrame={this.props.scrollHandler}>
|
||||
{this.props.children}
|
||||
</Scrollbars>
|
||||
|
||||
@@ -1,16 +1,34 @@
|
||||
import React from 'react';
|
||||
|
||||
let cachedProgressBars = {};
|
||||
|
||||
export default class ProgressBar extends React.Component {
|
||||
render() {
|
||||
let {percent} = this.props;
|
||||
let progressBar;
|
||||
|
||||
if (cachedProgressBars[percent] != null) {
|
||||
progressBar = cachedProgressBars[percent];
|
||||
} else {
|
||||
let style = {};
|
||||
|
||||
if (percent !== 100) {
|
||||
style = {transform: `scaleX(${percent / 100})`};
|
||||
}
|
||||
|
||||
progressBar = (
|
||||
<div className="progress-bar__fill__wrapper">
|
||||
<div className="progress-bar__fill" style={style} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="progress-bar">
|
||||
<div className="progress-bar__icon">
|
||||
{this.props.icon}
|
||||
</div>
|
||||
<div className="progress-bar__fill__wrapper">
|
||||
<div className="progress-bar__fill"
|
||||
style={{width: `${this.props.percent}%`}} />
|
||||
</div>
|
||||
{progressBar}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,11 +16,37 @@ import {torrentStatusIcons} from '../../util/torrentStatusIcons';
|
||||
import {torrentStatusClasses} from '../../util/torrentStatusClasses';
|
||||
import UploadThickIcon from '../Icons/UploadThickIcon';
|
||||
|
||||
const ICONS = {
|
||||
clock: <ClockIcon />,
|
||||
disk: <DiskIcon />,
|
||||
downloadThick: <DownloadThickIcon />,
|
||||
information: <InformationIcon />,
|
||||
peers: <PeersIcon />,
|
||||
ratio: <RatioIcon />,
|
||||
seeds: <SeedsIcon />,
|
||||
uploadThick: <UploadThickIcon />
|
||||
};
|
||||
|
||||
const METHODS_TO_BIND = [
|
||||
'handleClick',
|
||||
'handleRightClick'
|
||||
];
|
||||
|
||||
const TORRENT_PRIMITIVES_TO_OBSERVE = [
|
||||
'bytesDone',
|
||||
'downloadRate',
|
||||
'status',
|
||||
'tags',
|
||||
'totalPeers',
|
||||
'totalSeeds',
|
||||
'uploadRate'
|
||||
];
|
||||
|
||||
const TORRENT_ARRAYS_TO_OBSERVE = [
|
||||
'status',
|
||||
'tags'
|
||||
];
|
||||
|
||||
export default class Torrent extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
@@ -30,6 +56,33 @@ export default class Torrent extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
if (nextProps.selected !== this.props.selected) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let nextTorrent = nextProps.torrent;
|
||||
let {torrent} = this.props;
|
||||
|
||||
let shouldUpdate = TORRENT_ARRAYS_TO_OBSERVE.some((key) => {
|
||||
let nextArr = nextTorrent[key];
|
||||
let currentArr = this.props.torrent[key];
|
||||
|
||||
return nextArr.length !== currentArr.length ||
|
||||
nextArr.some((nextValue, index) => {
|
||||
return nextValue !== currentArr[index];
|
||||
});
|
||||
});
|
||||
|
||||
if (!shouldUpdate) {
|
||||
return TORRENT_PRIMITIVES_TO_OBSERVE.some((key) => {
|
||||
return nextTorrent[key] !== torrent[key];
|
||||
});
|
||||
}
|
||||
|
||||
return shouldUpdate;
|
||||
}
|
||||
|
||||
getTags(tags) {
|
||||
return tags.map((tag, index) => {
|
||||
return (
|
||||
@@ -65,7 +118,6 @@ export default class Torrent extends React.Component {
|
||||
let uploadTotal = format.data(torrent.uploadTotal);
|
||||
|
||||
let torrentClasses = torrentStatusClasses(torrent, this.props.selected ? 'is-selected' : null, 'torrent');
|
||||
let torrentStatusIcon = torrentStatusIcons(torrent.status);
|
||||
|
||||
let isActive = downloadRate.value > 0 || uploadRate.value > 0;
|
||||
let isDownloading = downloadRate.value > 0;
|
||||
@@ -73,13 +125,13 @@ export default class Torrent extends React.Component {
|
||||
let secondaryDetails = [
|
||||
<li className="torrent__details--secondary torrent__details--speed
|
||||
torrent__details--speed--download" key="download-rate">
|
||||
<span className="torrent__details__icon"><DownloadThickIcon /></span>
|
||||
<span className="torrent__details__icon">{ICONS.downloadThick}</span>
|
||||
{downloadRate.value}
|
||||
<em className="unit">{downloadRate.unit}</em>
|
||||
</li>,
|
||||
<li className="torrent__details--secondary torrent__details--speed
|
||||
torrent__details--speed--upload" key="upload-rate">
|
||||
<span className="torrent__details__icon"><UploadThickIcon /></span>
|
||||
<span className="torrent__details__icon">{ICONS.uploadThick}</span>
|
||||
{uploadRate.value}
|
||||
<em className="unit">{uploadRate.unit}</em>
|
||||
</li>
|
||||
@@ -89,7 +141,7 @@ export default class Torrent extends React.Component {
|
||||
secondaryDetails.unshift(
|
||||
<li className="torrent__details--secondary torrent__details--eta"
|
||||
key="eta">
|
||||
<span className="torrent__details__icon"><ClockIcon /></span>
|
||||
<span className="torrent__details__icon">{ICONS.clock}</span>
|
||||
{eta}
|
||||
</li>
|
||||
);
|
||||
@@ -107,7 +159,7 @@ export default class Torrent extends React.Component {
|
||||
<div className="torrent__details torrent__details--tertiary">
|
||||
<ul className="torrent__details torrent__details--tertiary--stats">
|
||||
<li className="torrent__details--completed">
|
||||
<span className="torrent__details__icon"><DownloadThickIcon /></span>
|
||||
<span className="torrent__details__icon">{ICONS.downloadThick}</span>
|
||||
{torrent.percentComplete}
|
||||
<em className="unit">%</em>
|
||||
—
|
||||
@@ -115,29 +167,29 @@ export default class Torrent extends React.Component {
|
||||
<em className="unit">{completed.unit}</em>
|
||||
</li>
|
||||
<li className="torrent__details--uploaded">
|
||||
<span className="torrent__details__icon"><UploadThickIcon /></span>
|
||||
<span className="torrent__details__icon">{ICONS.uploadThick}</span>
|
||||
{uploadTotal.value}
|
||||
<em className="unit">{uploadTotal.unit}</em>
|
||||
</li>
|
||||
<li className="torrent__details--ratio">
|
||||
<span className="torrent__details__icon"><RatioIcon /></span>
|
||||
<span className="torrent__details__icon">{ICONS.ratio}</span>
|
||||
{ratio}
|
||||
</li>
|
||||
<li className="torrent__details--size">
|
||||
<span className="torrent__details__icon"><DiskIcon /></span>
|
||||
<span className="torrent__details__icon">{ICONS.disk}</span>
|
||||
{totalSize.value}
|
||||
<em className="unit">{totalSize.unit}</em>
|
||||
</li>
|
||||
<li className="torrent__details--added">
|
||||
<span className="torrent__details__icon"><CalendarIcon /></span>
|
||||
<span className="torrent__details__icon">{ICONS.calendar}</span>
|
||||
{addedString}
|
||||
</li>
|
||||
<li className="torrent__details--peers">
|
||||
<span className="torrent__details__icon"><PeersIcon /></span>
|
||||
<span className="torrent__details__icon">{ICONS.peers}</span>
|
||||
{torrent.connectedPeers} <em className="unit">of</em> {torrent.totalPeers}
|
||||
</li>
|
||||
<li className="torrent__details--seeds">
|
||||
<span className="torrent__details__icon"><SeedsIcon /></span>
|
||||
<span className="torrent__details__icon">{ICONS.seeds}</span>
|
||||
{torrent.connectedSeeds} <em className="unit">of</em> {torrent.totalSeeds}
|
||||
</li>
|
||||
</ul>
|
||||
@@ -145,11 +197,11 @@ export default class Torrent extends React.Component {
|
||||
{this.getTags(torrent.tags)}
|
||||
</ul>
|
||||
</div>
|
||||
<ProgressBar percent={torrent.percentComplete} icon={torrentStatusIcon} />
|
||||
<ProgressBar percent={torrent.percentComplete} icon={torrentStatusIcons(torrent.status)} />
|
||||
<button className="torrent__more-info floating-action__button"
|
||||
onClick={this.props.handleDetailsClick.bind(this, torrent)}
|
||||
tabIndex="-1">
|
||||
<InformationIcon />
|
||||
{ICONS.information}
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
|
||||
@@ -25,20 +25,26 @@ const METHODS_TO_BIND = [
|
||||
'handleContextMenuItemClick',
|
||||
'handleDetailsClick',
|
||||
'handleRightClick',
|
||||
'handleScrollStop',
|
||||
'handleTorrentClick',
|
||||
'onContextMenuChange',
|
||||
'onReceiveTorrentsError',
|
||||
'onReceiveTorrentsSuccess',
|
||||
'onTorrentFilterChange',
|
||||
'onTorrentSelectionChange',
|
||||
'postponeRerender',
|
||||
'setScrollPosition',
|
||||
'setViewportHeight'
|
||||
];
|
||||
|
||||
let cachedTorrentList = null;
|
||||
|
||||
class TorrentListContainer extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.lastScrollPosition = 0;
|
||||
this.postponedRerender = false;
|
||||
this.state = {
|
||||
emptyTorrentList: false,
|
||||
handleTorrentPriorityChange: null,
|
||||
@@ -47,7 +53,7 @@ class TorrentListContainer extends React.Component {
|
||||
minTorrentIndex: 0,
|
||||
scrollPosition: 0,
|
||||
torrentCount: 0,
|
||||
torrentHeight: 72,
|
||||
torrentHeight: 71,
|
||||
torrents: [],
|
||||
torrentRequestError: false,
|
||||
torrentRequestSuccess: false,
|
||||
@@ -58,13 +64,9 @@ class TorrentListContainer extends React.Component {
|
||||
this[method] = this[method].bind(this);
|
||||
});
|
||||
|
||||
this.setScrollPosition = _.throttle(this.setScrollPosition, 100, {
|
||||
leading: true,
|
||||
trailing: true
|
||||
});
|
||||
|
||||
this.handleWindowResize = _.throttle(this.setViewportHeight, 350, {
|
||||
leading: true,
|
||||
this.handleWindowResize = _.debounce(this.setViewportHeight, 250);
|
||||
this.postponeRerender = _.debounce(this.postponeRerender, 500);
|
||||
this.setScrollPosition = _.throttle(this.setScrollPosition, 250, {
|
||||
trailing: true
|
||||
});
|
||||
}
|
||||
@@ -316,7 +318,12 @@ class TorrentListContainer extends React.Component {
|
||||
// Calculate the number of items that should be rendered based on the height
|
||||
// of the viewport. We offset this to render a few more outide of the
|
||||
// container's dimensions, which looks nicer when the user scrolls.
|
||||
let offset = 10;
|
||||
let offset = 0;
|
||||
|
||||
if (this.postponedRerender === false
|
||||
&& !this.refs.torrentList.refs.scrollbar.dragging) {
|
||||
offset = 20;
|
||||
}
|
||||
|
||||
// The number of elements in view is the height of the viewport divided
|
||||
// by the height of the elements.
|
||||
@@ -339,8 +346,25 @@ class TorrentListContainer extends React.Component {
|
||||
TorrentFilterStore.clearAllFilters();
|
||||
}
|
||||
|
||||
handleScrollStop() {
|
||||
// Force update as soon as scrolling stops.
|
||||
this.postponedRerender = false;
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
postponeRerender() {
|
||||
global.requestAnimationFrame(() => {
|
||||
this.postponedRerender = false;
|
||||
this.forceUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
setScrollPosition(scrollValues) {
|
||||
this.setState({scrollPosition: scrollValues.scrollTop});
|
||||
global.requestAnimationFrame(() => {
|
||||
let {scrollTop} = scrollValues;
|
||||
this.setState({scrollPosition: scrollTop});
|
||||
this.lastScrollPosition = scrollTop;
|
||||
});
|
||||
}
|
||||
|
||||
setViewportHeight() {
|
||||
@@ -357,64 +381,71 @@ class TorrentListContainer extends React.Component {
|
||||
if (this.state.emptyTorrentList || this.state.torrents.length === 0) {
|
||||
content = this.getEmptyTorrentListNotification();
|
||||
} else if (this.state.torrentRequestSuccess) {
|
||||
let contextMenu = null;
|
||||
let selectedTorrents = TorrentStore.getSelectedTorrents();
|
||||
let torrents = this.state.torrents;
|
||||
let viewportLimits = this.getViewportLimits();
|
||||
let scrollDelta = Math.abs(this.state.scrollPosition -
|
||||
this.lastScrollPosition);
|
||||
|
||||
let listPadding = this.getListPadding(
|
||||
viewportLimits.minTorrentIndex,
|
||||
viewportLimits.maxTorrentIndex,
|
||||
torrents.length
|
||||
);
|
||||
// If the torrent list is cached and the user is scrolling a large amount,
|
||||
// or the user is dragging the scroll handle, then we postpone the list's
|
||||
// rerender for better FPS.
|
||||
if ((cachedTorrentList != null && scrollDelta > 1000)
|
||||
|| this.refs.torrentList.refs.scrollbar.dragging === true) {
|
||||
this.postponedRerender = true;
|
||||
content = cachedTorrentList;
|
||||
|
||||
let maxTorrentIndex = viewportLimits.maxTorrentIndex;
|
||||
let minTorrentIndex = viewportLimits.minTorrentIndex;
|
||||
global.requestAnimationFrame(() => {
|
||||
this.postponeRerender();
|
||||
});
|
||||
} else {
|
||||
let contextMenu = null;
|
||||
let selectedTorrents = TorrentStore.getSelectedTorrents();
|
||||
let {minTorrentIndex, maxTorrentIndex} = this.getViewportLimits();
|
||||
let {torrents} = this.state;
|
||||
let listPadding = this.getListPadding(minTorrentIndex, maxTorrentIndex,
|
||||
torrents.length);
|
||||
|
||||
if (minTorrentIndex < 0) {
|
||||
minTorrentIndex = 0;
|
||||
}
|
||||
|
||||
if (this.state.contextMenu != null) {
|
||||
contextMenu = (
|
||||
<ContextMenu clickPosition={this.state.contextMenu.clickPosition}
|
||||
items={this.state.contextMenu.items} />
|
||||
);
|
||||
}
|
||||
|
||||
let visibleTorrents = torrents.slice(minTorrentIndex, maxTorrentIndex);
|
||||
|
||||
let torrentList = visibleTorrents.map((torrent, index) => {
|
||||
let isSelected = false;
|
||||
let hash = torrent.hash;
|
||||
|
||||
if (selectedTorrents.indexOf(hash) > -1) {
|
||||
isSelected = true;
|
||||
if (minTorrentIndex < 0) {
|
||||
minTorrentIndex = 0;
|
||||
}
|
||||
|
||||
return (
|
||||
<Torrent key={hash} torrent={torrent} selected={isSelected}
|
||||
handleClick={this.handleTorrentClick}
|
||||
handleRightClick={this.handleRightClick}
|
||||
handleDetailsClick={this.handleDetailsClick} />
|
||||
);
|
||||
});
|
||||
if (this.state.contextMenu != null) {
|
||||
contextMenu = (
|
||||
<ContextMenu clickPosition={this.state.contextMenu.clickPosition}
|
||||
items={this.state.contextMenu.items} />
|
||||
);
|
||||
}
|
||||
|
||||
content = (
|
||||
<ul className="torrent__list" key="torrent__list">
|
||||
<CSSTransitionGroup
|
||||
transitionName="menu"
|
||||
transitionEnterTimeout={250}
|
||||
transitionLeaveTimeout={250}>
|
||||
{contextMenu}
|
||||
</CSSTransitionGroup>
|
||||
<li className="torrent__spacer torrent__spacer--top"
|
||||
style={{height: `${listPadding.top}px`}}></li>
|
||||
{torrentList}
|
||||
<li className="torrent__spacer torrent__spacer--bottom"
|
||||
style={{height: `${listPadding.bottom}px`}}></li>
|
||||
</ul>
|
||||
);
|
||||
let torrentList = torrents.slice(minTorrentIndex, maxTorrentIndex).map(
|
||||
(torrent, index) => {
|
||||
let {hash} = torrent;
|
||||
|
||||
return (
|
||||
<Torrent key={hash} torrent={torrent}
|
||||
selected={selectedTorrents.includes(hash)}
|
||||
handleClick={this.handleTorrentClick}
|
||||
handleRightClick={this.handleRightClick}
|
||||
handleDetailsClick={this.handleDetailsClick} />
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
content = (
|
||||
<ul className="torrent__list" key="torrent__list">
|
||||
<CSSTransitionGroup
|
||||
transitionName="menu"
|
||||
transitionEnterTimeout={250}
|
||||
transitionLeaveTimeout={250}>
|
||||
{contextMenu}
|
||||
</CSSTransitionGroup>
|
||||
<li className="torrent__spacer torrent__spacer--top"
|
||||
style={{height: `${listPadding.top}px`}}></li>
|
||||
{torrentList}
|
||||
<li className="torrent__spacer torrent__spacer--bottom"
|
||||
style={{height: `${listPadding.bottom}px`}}></li>
|
||||
</ul>
|
||||
);
|
||||
|
||||
cachedTorrentList = content;
|
||||
}
|
||||
} else {
|
||||
content = this.getLoadingIndicator();
|
||||
}
|
||||
@@ -422,6 +453,7 @@ class TorrentListContainer extends React.Component {
|
||||
return (
|
||||
<div className="torrent__list__wrapper">
|
||||
<CustomScrollbars className="torrent__list__wrapper--custom-scroll"
|
||||
onScrollStop={this.handleScrollStop}
|
||||
ref="torrentList" scrollHandler={this.setScrollPosition}>
|
||||
{content}
|
||||
</CustomScrollbars>
|
||||
|
||||
Reference in New Issue
Block a user