client: fix up and migrate to native scrolling

Native scrolling is much simpler and more performant.
This change also fixes a bunch of related visual bugs.

Ditch the unmaintained react-custom-scrollbars.

A more stylish scrollbar can be added in a later change.
This commit is contained in:
Jesse Chan
2020-10-28 00:24:44 +08:00
parent 56306af3b2
commit 720ad3e17c
16 changed files with 125 additions and 715 deletions
@@ -1,92 +0,0 @@
import classnames from 'classnames';
import React from 'react';
import {positionValues, Scrollbars} from 'react-custom-scrollbars';
const horizontalThumb: React.StatelessComponent = (props) => {
return <div {...props} className="scrollbars__thumb scrollbars__thumb--horizontal" />;
};
const verticalThumb: React.StatelessComponent = (props) => {
return <div {...props} className="scrollbars__thumb scrollbars__thumb--vertical" />;
};
const renderView: React.StatelessComponent = (props) => {
return (
<div {...props} className="scrollbars__view">
{props.children}
</div>
);
};
interface CustomScrollbarsProps {
children: React.ReactNode;
className?: string;
style?: React.CSSProperties;
autoHeight?: boolean;
autoHeightMin?: number | string;
autoHeightMax?: number | string;
inverted?: boolean;
getHorizontalThumb?: React.StatelessComponent;
getVerticalThumb?: React.StatelessComponent;
nativeScrollHandler?: (event: React.UIEvent) => void;
scrollHandler?: (values: positionValues) => void;
onScrollStart?: () => void;
onScrollStop?: () => void;
}
const CustomScrollbars = React.forwardRef<Scrollbars, CustomScrollbarsProps>((props: CustomScrollbarsProps, ref) => {
const {
children,
className,
style,
autoHeight,
autoHeightMin,
autoHeightMax,
inverted,
getHorizontalThumb,
getVerticalThumb,
nativeScrollHandler,
scrollHandler,
onScrollStart,
onScrollStop,
} = props;
const classes = classnames('scrollbars', className, {
'is-inverted': inverted,
});
return (
<Scrollbars
className={classes}
style={style}
autoHeight={autoHeight}
autoHeightMin={autoHeightMin}
autoHeightMax={autoHeightMax}
ref={ref}
renderView={renderView}
renderThumbHorizontal={getHorizontalThumb}
renderThumbVertical={getVerticalThumb}
onScroll={nativeScrollHandler}
onScrollFrame={scrollHandler}
onScrollStart={onScrollStart}
onScrollStop={onScrollStop}>
{children}
</Scrollbars>
);
});
CustomScrollbars.defaultProps = {
className: '',
style: undefined,
autoHeight: undefined,
autoHeightMin: undefined,
autoHeightMax: undefined,
inverted: undefined,
getHorizontalThumb: horizontalThumb,
getVerticalThumb: verticalThumb,
nativeScrollHandler: undefined,
scrollHandler: undefined,
onScrollStart: undefined,
onScrollStop: undefined,
};
export default CustomScrollbars;
@@ -1,282 +1,37 @@
import debounce from 'lodash/debounce';
import React from 'react';
import {positionValues, Scrollbars} from 'react-custom-scrollbars';
import throttle from 'lodash/throttle';
import CustomScrollbars from './CustomScrollbars';
const METHODS_TO_BIND = [
'handleScroll',
'handleScrollStart',
'handleScrollStop',
'measureItemHeight',
'scrollToTop',
'setScrollPosition',
'setViewportHeight',
] as const;
interface ListViewportProps {
children?: React.ReactNode;
itemRenderer: (index: number) => React.ReactNode;
listClass: string;
listLength: number;
scrollContainerClass: string;
topSpacerClass?: string;
bottomSpacerClass?: string;
itemScrollOffset?: number;
getVerticalThumb?: React.StatelessComponent;
onScroll?: () => void;
}
interface ListViewportStates {
itemHeight: number | null;
listVerticalPadding: number | null;
scrollTop: number;
viewportHeight: number | null;
}
// TODO: Implement windowing or infinite scrolling
const ListViewport = React.forwardRef<HTMLDivElement, ListViewportProps>((props: ListViewportProps, ref) => {
const {children, listClass, listLength, itemRenderer, onScroll} = props;
class ListViewport extends React.Component<ListViewportProps, ListViewportStates> {
scrollbarRef: Scrollbars | null = null;
listRef: HTMLUListElement | null = null;
topSpacerRef: HTMLLIElement | null = null;
isScrolling = false;
lastScrollTop = 0;
const list = [];
static defaultProps = {
bottomSpacerClass: 'list__spacer list__spacer--bottom',
itemScrollOffset: 10,
topSpacerClass: 'list__spacer list__spacer--top',
};
constructor(props: ListViewportProps) {
super(props);
this.state = {
itemHeight: null,
listVerticalPadding: null,
scrollTop: 0,
viewportHeight: null,
};
METHODS_TO_BIND.forEach(<T extends typeof METHODS_TO_BIND[number]>(methodName: T) => {
this[methodName] = this[methodName].bind(this);
});
this.setViewportHeight = debounce(this.setViewportHeight, 250);
this.updateAfterScrolling = debounce(this.updateAfterScrolling, 500, {
leading: true,
trailing: true,
});
this.setScrollPosition = throttle(this.setScrollPosition, 100);
// For loops are fast, and performance matters here.
for (let index = 0; index < listLength; index += 1) {
list.push(itemRenderer(index));
}
componentDidMount() {
global.addEventListener('resize', this.setViewportHeight);
this.setViewportHeight();
}
const listContent = <ul className={listClass}>{list}</ul>;
shouldComponentUpdate(_nextProps: ListViewportProps, nextState: ListViewportStates) {
const {scrollTop} = this.state;
const scrollDelta = Math.abs(scrollTop - nextState.scrollTop);
return (
<div className="torrent__list__viewport" onScroll={onScroll} ref={ref}>
{children}
{listContent}
</div>
);
});
if (this.isScrolling && scrollDelta > 20) {
return false;
}
return true;
}
componentWillUnmount() {
global.removeEventListener('resize', this.setViewportHeight);
}
getViewportLimits(scrollDelta: number) {
const {itemScrollOffset, listLength} = this.props;
const {itemHeight, listVerticalPadding, scrollTop, viewportHeight} = this.state;
if (
itemHeight == null ||
itemHeight <= 0 ||
itemScrollOffset == null ||
viewportHeight == null ||
viewportHeight <= 0
) {
return {
minItemIndex: 0,
maxItemIndex: Math.min(50, listLength),
};
}
// Calculate the number of items that should be rendered based on the height
// of the viewport. We offset this to render a few more outside of the
// container's dimensions, which looks nicer when the user scrolls.
const offsetBottom = scrollDelta > 0 ? itemScrollOffset * 2 : itemScrollOffset / 2;
const offsetTop = scrollDelta < 0 ? itemScrollOffset * 2 : itemScrollOffset / 2;
let viewportHeightPadded = viewportHeight;
if (listVerticalPadding) {
viewportHeightPadded -= listVerticalPadding;
}
// The number of elements in view is the height of the viewport divided
// by the height of the elements.
const elementsInView = Math.ceil(viewportHeightPadded / itemHeight);
// The minimum item index to render is the number of items above the
// viewport's current scroll position, minus the offset.
const minItemIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - offsetTop);
// The maximum item index to render is the minimum item rendered, plus the
// number of items in view, plus double the offset.
const maxItemIndex = Math.min(listLength, minItemIndex + elementsInView + offsetBottom + offsetTop);
return {minItemIndex, maxItemIndex};
}
getListPadding(minItemIndex: number, maxItemIndex: number, itemCount: number) {
const {itemHeight} = this.state;
if (itemHeight == null) {
return {bottom: 0, top: 0};
}
const bottom = (itemCount - maxItemIndex) * itemHeight;
const top = minItemIndex * itemHeight;
return {bottom, top};
}
setScrollPosition(scrollValues: positionValues) {
const {scrollTop} = this.state;
this.lastScrollTop = scrollTop;
this.setState({scrollTop: scrollValues.scrollTop});
}
setViewportHeight() {
if (this.scrollbarRef) {
this.setState({
viewportHeight: this.scrollbarRef.getClientHeight(),
});
}
}
scrollToTop() {
const {scrollTop} = this.state;
if (scrollTop !== 0) {
if (this.scrollbarRef != null) {
this.scrollbarRef.scrollToTop();
}
this.lastScrollTop = 0;
this.setState({scrollTop: 0});
}
}
measureItemHeight() {
this.lastScrollTop = 0;
this.setState(
{
scrollTop: 0,
itemHeight: null,
},
() => {
if (this.scrollbarRef != null) {
this.scrollbarRef.scrollTop(0);
}
},
);
}
handleScroll(scrollValues: positionValues) {
this.setScrollPosition(scrollValues);
}
handleScrollStart() {
this.isScrolling = true;
}
handleScrollStop() {
this.isScrolling = false;
this.updateAfterScrolling();
}
updateAfterScrolling() {
this.forceUpdate();
}
render() {
const {
children,
listClass,
topSpacerClass,
bottomSpacerClass,
scrollContainerClass,
listLength,
getVerticalThumb,
itemRenderer,
} = this.props;
const {itemHeight, scrollTop, listVerticalPadding} = this.state;
const {minItemIndex, maxItemIndex} = this.getViewportLimits(scrollTop - this.lastScrollTop);
const listPadding = this.getListPadding(minItemIndex, maxItemIndex, listLength);
const list = [];
// For loops are fast, and performance matters here.
for (let index = minItemIndex; index < maxItemIndex; index += 1) {
list.push(itemRenderer(index));
}
const listContent = (
<ul
className={listClass}
ref={(ref) => {
this.listRef = ref;
if (listVerticalPadding == null && this.listRef != null) {
const listStyle = global.getComputedStyle(this.listRef);
const paddingBottom = Number(listStyle.getPropertyValue('padding-bottom').replace('px', ''));
const paddingTop = Number(listStyle.getPropertyValue('padding-top').replace('px', ''));
this.setState({
listVerticalPadding: paddingBottom + paddingTop,
});
}
}}>
<li
className={topSpacerClass}
ref={(ref) => {
this.topSpacerRef = ref;
if (itemHeight == null && this.topSpacerRef?.nextSibling != null) {
this.setState({
itemHeight: (this.topSpacerRef.nextSibling as HTMLLIElement).offsetHeight,
});
}
}}
style={{height: `${listPadding.top}px`}}
/>
{list}
<li className={bottomSpacerClass} style={{height: `${listPadding.bottom}px`}} />
</ul>
);
return (
<CustomScrollbars
className={scrollContainerClass}
getVerticalThumb={getVerticalThumb}
onScrollStart={this.handleScrollStart}
onScrollStop={this.handleScrollStop}
ref={(ref) => {
this.scrollbarRef = ref;
}}
scrollHandler={this.handleScroll}>
{children}
{listContent}
</CustomScrollbars>
);
}
}
ListViewport.defaultProps = {
children: undefined,
onScroll: undefined,
};
export default ListViewport;
@@ -2,7 +2,6 @@ import React from 'react';
import {defineMessages, WrappedComponentProps} from 'react-intl';
import ArrowIcon from '../../icons/ArrowIcon';
import CustomScrollbars from '../CustomScrollbars';
import File from '../../icons/File';
import FolderClosedSolid from '../../icons/FolderClosedSolid';
import FloodActions from '../../../actions/FloodActions';
@@ -28,7 +27,6 @@ const MESSAGES = defineMessages({
interface FilesystemBrowserProps extends WrappedComponentProps {
selectable?: 'files' | 'directories';
directory: string;
maxHeight?: number | string | null;
onItemSelection?: (newDestination: string, isDirectory?: boolean) => void;
}
@@ -112,7 +110,7 @@ class FilesystemBrowser extends React.PureComponent<FilesystemBrowserProps, File
};
render() {
const {intl, selectable, maxHeight} = this.props;
const {intl, selectable} = this.props;
const {directories, errorResponse, files} = this.state;
let errorMessage = null;
let listItems = null;
@@ -198,13 +196,11 @@ class FilesystemBrowser extends React.PureComponent<FilesystemBrowserProps, File
}
return (
<CustomScrollbars autoHeight autoHeightMin={0} autoHeightMax={maxHeight || undefined}>
<div className="filesystem__directory-list context-menu__items__padding-surrogate">
{parentDirectory}
{errorMessage}
{listItems}
</div>
</CustomScrollbars>
<div className="filesystem__directory-list context-menu__items__padding-surrogate">
{parentDirectory}
{errorMessage}
{listItems}
</div>
);
}
}
@@ -198,16 +198,10 @@ class FilesystemBrowserTextbox extends React.Component<FilesystemBrowserTextboxP
setRef={(ref) => {
this.contextMenuNodeRef = ref;
}}
scrolling={false}
triggerRef={this.textboxRef}>
<FilesystemBrowser
directory={destination}
intl={intl}
maxHeight={
this.contextMenuInstanceRef &&
this.contextMenuInstanceRef.dropdownStyle &&
this.contextMenuInstanceRef.dropdownStyle.maxHeight
}
selectable={selectable}
onItemSelection={this.handleItemSelection}
/>
@@ -9,7 +9,6 @@ import type {Notification} from '@shared/types/Notification';
import FloodActions from '../../actions/FloodActions';
import ChevronLeftIcon from '../icons/ChevronLeftIcon';
import ChevronRightIcon from '../icons/ChevronRightIcon';
import CustomScrollbars from '../general/CustomScrollbars';
import LoadingIndicatorDots from '../icons/LoadingIndicatorDots';
import NotificationIcon from '../icons/NotificationIcon';
import NotificationStore from '../../stores/NotificationStore';
@@ -221,11 +220,9 @@ class NotificationsButton extends React.Component<WrappedComponentProps, Notific
<div className={notificationsWrapperClasses}>
{this.getTopToolbar()}
<div className="notifications__loading-indicator">{loadingIndicatorIcon}</div>
<CustomScrollbars autoHeight autoHeightMin={0} autoHeightMax={300} inverted>
<ul className="notifications__list tooltip__content--padding-surrogate">
{notifications.map(this.getNotification)}
</ul>
</CustomScrollbars>
<ul className="notifications__list tooltip__content--padding-surrogate">
{notifications.map(this.getNotification)}
</ul>
{this.getBottomToolbar()}
</div>
);
@@ -1,6 +1,5 @@
import React from 'react';
import CustomScrollbars from '../general/CustomScrollbars';
import FeedsButton from './FeedsButton';
import LogoutButton from './LogoutButton';
import NotificationsButton from './NotificationsButton';
@@ -16,7 +15,7 @@ import DiskUsage from './DiskUsage';
const Sidebar = () => {
return (
<CustomScrollbars className="application__sidebar" inverted>
<div className="application__sidebar">
<SidebarActions>
<SpeedLimitDropdown />
<SettingsButton />
@@ -30,7 +29,7 @@ const Sidebar = () => {
<TagFilters />
<TrackerFilters />
<DiskUsage />
</CustomScrollbars>
</div>
);
};
@@ -13,9 +13,9 @@ const pointerDownStyles = `
`;
interface TableHeadingProps extends WrappedComponentProps {
scrollOffset: number;
onCellClick: (column: TorrentListColumn) => void;
onWidthsChange: (column: TorrentListColumn, width: number) => void;
setRef?: React.RefCallback<HTMLDivElement>;
}
@observer
@@ -26,7 +26,6 @@ class TableHeading extends React.PureComponent<TableHeadingProps> {
lastPointerX: number | null = null;
tableHeading: HTMLDivElement | null = null;
resizeLine: HTMLDivElement | null = null;
tableHeadingX = 0;
constructor(props: TableHeadingProps) {
super(props);
@@ -36,12 +35,6 @@ class TableHeading extends React.PureComponent<TableHeadingProps> {
this.handlePointerMove = this.handlePointerMove.bind(this);
}
componentDidMount() {
if (this.tableHeading != null) {
this.tableHeadingX = this.tableHeading.getBoundingClientRect().left;
}
}
getHeadingElements() {
const {intl, onCellClick} = this.props;
@@ -103,11 +96,10 @@ class TableHeading extends React.PureComponent<TableHeadingProps> {
if (nextCellWidth > 20) {
this.focusedCellWidth = nextCellWidth;
this.lastPointerX = event.clientX;
if (this.resizeLine != null) {
this.resizeLine.style.transform = `translateX(${Math.max(
0,
event.clientX - this.tableHeadingX + this.props.scrollOffset,
)}px)`;
if (this.resizeLine != null && this.tableHeading != null) {
this.resizeLine.style.transform = `translate(${Math.max(0, event.clientX)}px, ${
this.tableHeading.getBoundingClientRect().top
}px)`;
}
}
}
@@ -133,7 +125,7 @@ class TableHeading extends React.PureComponent<TableHeadingProps> {
}
handlePointerDown(event: React.PointerEvent, slug: TorrentListColumn, width: number) {
if (!this.isPointerDown && this.resizeLine != null) {
if (!this.isPointerDown && this.resizeLine != null && this.tableHeading != null) {
global.document.addEventListener('pointerup', this.handlePointerUp);
global.document.addEventListener('pointermove', this.handlePointerMove);
UIStore.addGlobalStyle(pointerDownStyles);
@@ -142,20 +134,24 @@ class TableHeading extends React.PureComponent<TableHeadingProps> {
this.focusedCellWidth = width;
this.isPointerDown = true;
this.lastPointerX = event.clientX;
this.resizeLine.style.transform = `translateX(${Math.max(
0,
event.clientX - this.tableHeadingX + this.props.scrollOffset,
)}px)`;
this.resizeLine.style.transform = `translate(${Math.max(0, event.clientX)}px, ${
this.tableHeading.getBoundingClientRect().top
}px)`;
this.resizeLine.style.opacity = '1';
}
}
render() {
const {setRef} = this.props;
return (
<div
className="table__row table__row--heading"
ref={(ref) => {
this.tableHeading = ref;
if (setRef != null) {
setRef(ref);
}
}}>
{this.getHeadingElements()}
<div className="table__cell table__heading table__heading--fill" />
@@ -1,9 +1,7 @@
import {FormattedMessage, injectIntl, WrappedComponentProps} from 'react-intl';
import debounce from 'lodash/debounce';
import Dropzone from 'react-dropzone';
import {observer} from 'mobx-react';
import {Scrollbars} from 'react-custom-scrollbars';
import {observable, reaction, runInAction} from 'mobx';
import {observable, reaction} from 'mobx';
import React from 'react';
import defaultFloodSettings from '@shared/constants/defaultFloodSettings';
@@ -13,7 +11,6 @@ import type {TorrentProperties} from '@shared/types/Torrent';
import {Button} from '../../ui';
import ClientStatusStore from '../../stores/ClientStatusStore';
import CustomScrollbars from '../general/CustomScrollbars';
import Files from '../icons/Files';
import GlobalContextMenuMountPoint from '../general/GlobalContextMenuMountPoint';
import ListViewport from '../general/ListViewport';
@@ -59,18 +56,11 @@ const getEmptyTorrentListNotification = (): React.ReactNode => {
const handleClick = (torrent: TorrentProperties, event: React.MouseEvent) =>
UIActions.handleTorrentClick({hash: torrent.hash, event});
const handleDoubleClick = (torrent: TorrentProperties) => TorrentListContextMenu.handleDetailsClick(torrent);
interface TorrentListStates {
tableScrollLeft: number;
}
@observer
class TorrentList extends React.Component<WrappedComponentProps, TorrentListStates> {
class TorrentList extends React.Component<WrappedComponentProps> {
listContainer: HTMLDivElement | null = null;
listViewportRef: ListViewport | null = null;
horizontalScrollRef: Scrollbars | null = null;
verticalScrollbarThumb: HTMLDivElement | null = null;
lastScrollLeft = 0;
listHeaderRef: HTMLDivElement | null = null;
listViewportRef = React.createRef<HTMLDivElement>();
torrentListViewportSize = observable.object<{width: number; height: number}>({
width: window.innerWidth,
@@ -80,48 +70,17 @@ class TorrentList extends React.Component<WrappedComponentProps, TorrentListStat
constructor(props: WrappedComponentProps) {
super(props);
reaction(
() => SettingStore.floodSettings.torrentListViewSize,
(currentTorrentListViewSize, prevTorrentListViewSize) => {
const isCondensed = currentTorrentListViewSize === 'condensed';
const wasCondensed = prevTorrentListViewSize === 'condensed';
if (this.verticalScrollbarThumb != null) {
if (!isCondensed && wasCondensed) {
this.updateVerticalThumbPosition(0);
} else if (isCondensed && this.listContainer != null) {
this.updateVerticalThumbPosition(
(SettingStore.totalCellWidth - this.listContainer.clientWidth) * -1 + this.lastScrollLeft,
);
}
}
if (currentTorrentListViewSize !== prevTorrentListViewSize && this.listViewportRef != null) {
this.listViewportRef.measureItemHeight();
}
},
);
reaction(() => TorrentFilterStore.filters, this.handleTorrentFilterChange);
this.state = {
tableScrollLeft: 0,
};
}
componentDidMount() {
window.addEventListener('resize', this.updateTorrentListViewSize);
}
handleColumnWidthChange = (column: TorrentListColumn, width: number) => {
const {torrentListColumnWidths = defaultFloodSettings.torrentListColumnWidths} = SettingStore.floodSettings;
componentDidUpdate() {
if (this.horizontalScrollRef != null && this.torrentListViewportSize.width === 0) {
this.updateTorrentListViewSize();
}
}
componentWillUnmount() {
window.removeEventListener('resize', this.updateTorrentListViewSize);
}
SettingActions.saveSetting('torrentListColumnWidths', {
...torrentListColumnWidths,
[column]: width,
});
};
handleContextMenuClick = (torrent: TorrentProperties, event: React.MouseEvent | React.TouchEvent) => {
if (event.cancelable === true) {
@@ -184,70 +143,14 @@ class TorrentList extends React.Component<WrappedComponentProps, TorrentListStat
};
handleTorrentFilterChange = () => {
if (this.listViewportRef != null) {
this.listViewportRef.scrollToTop();
if (this.listViewportRef.current != null) {
this.listViewportRef.current.scrollTop = 0;
}
};
getVerticalScrollbarThumb: React.StatelessComponent = (props) => {
return (
<div {...props}>
<div
className="scrollbars__thumb scrollbars__thumb--horizontal scrollbars__thumb--surrogate"
ref={(ref) => {
this.verticalScrollbarThumb = ref;
}}
role="button"
tabIndex={0}
/>
</div>
);
};
handleHorizontalScroll = (event: React.UIEvent) => {
if (this.verticalScrollbarThumb != null) {
const {clientWidth, scrollLeft, scrollWidth} = event.target as HTMLElement;
this.lastScrollLeft = scrollLeft;
this.updateVerticalThumbPosition((scrollWidth - clientWidth) * -1 + scrollLeft);
}
};
handleHorizontalScrollStop = () => {
this.setState({tableScrollLeft: this.lastScrollLeft});
};
handleColumnWidthChange = (column: TorrentListColumn, width: number) => {
const {torrentListColumnWidths = defaultFloodSettings.torrentListColumnWidths} = SettingStore.floodSettings;
SettingActions.saveSetting('torrentListColumnWidths', {
...torrentListColumnWidths,
[column]: width,
});
};
/* eslint-disable react/sort-comp */
updateTorrentListViewSize = debounce(
() => {
runInAction(() => {
this.torrentListViewportSize.height = this.horizontalScrollRef?.getClientHeight() || window.innerHeight;
this.torrentListViewportSize.width = this.horizontalScrollRef?.getClientWidth() || window.innerWidth;
});
if (SettingStore.floodSettings.torrentListViewSize === 'condensed') {
if (this.verticalScrollbarThumb != null && this.listContainer != null) {
this.updateVerticalThumbPosition(
(SettingStore.totalCellWidth - this.listContainer.clientWidth) * -1 + this.lastScrollLeft,
);
}
}
},
100,
{trailing: true},
);
/* eslint-enable react/sort-comp */
updateVerticalThumbPosition = (offset: number) => {
if (this.verticalScrollbarThumb != null) {
this.verticalScrollbarThumb.style.transform = `translateX(${offset}px)`;
handleViewportScroll = () => {
if (this.listHeaderRef != null && this.listViewportRef.current != null) {
this.listHeaderRef.scrollLeft = this.listViewportRef.current.scrollLeft;
}
};
@@ -285,23 +188,9 @@ class TorrentList extends React.Component<WrappedComponentProps, TorrentListStat
} else if (isListEmpty || torrents == null) {
content = getEmptyTorrentListNotification();
} else {
content = (
<ListViewport
getVerticalThumb={this.getVerticalScrollbarThumb}
itemRenderer={this.renderListItem}
listClass="torrent__list"
listLength={torrents.length}
ref={(ref) => {
this.listViewportRef = ref;
}}
scrollContainerClass="torrent__list__scrollbars--vertical"
/>
);
if (isCondensed) {
torrentListHeading = (
<TableHeading
scrollOffset={this.state.tableScrollLeft}
onCellClick={(property: TorrentListColumn) => {
const currentSort = SettingStore.floodSettings.sortTorrents;
@@ -319,12 +208,23 @@ class TorrentList extends React.Component<WrappedComponentProps, TorrentListStat
SettingActions.saveSetting('sortTorrents', sortBy);
}}
onWidthsChange={this.handleColumnWidthChange}
setRef={(ref) => {
this.listHeaderRef = ref;
}}
/>
);
}
}
const listViewportWidth = this.torrentListViewportSize.width;
content = (
<ListViewport
itemRenderer={this.renderListItem}
listClass="torrent__list"
listLength={torrents.length}
onScroll={this.handleViewportScroll}
ref={this.listViewportRef}
/>
);
}
return (
<Dropzone onDrop={this.handleFileDrop} noClick noKeyboard>
@@ -335,26 +235,11 @@ class TorrentList extends React.Component<WrappedComponentProps, TorrentListStat
ref={(ref) => {
this.listContainer = ref;
}}>
<CustomScrollbars
className="torrent__list__scrollbars--horizontal"
onScrollStop={this.handleHorizontalScrollStop}
nativeScrollHandler={this.handleHorizontalScroll}
ref={(ref) => {
this.horizontalScrollRef = ref;
}}>
<div
className="torrent__list__wrapper"
style={
isCondensed && !isListEmpty
? {width: `${Math.max(listViewportWidth, SettingStore.totalCellWidth)}px`}
: {}
}>
<GlobalContextMenuMountPoint id="torrent-list-item" />
{torrentListHeading}
{content}
</div>
</CustomScrollbars>
<div className="torrent__list__wrapper">
<GlobalContextMenuMountPoint id="torrent-list-item" />
{torrentListHeading}
{content}
</div>
<div className="dropzone__overlay">
<div className="dropzone__copy">
<div className="dropzone__icon">
+27
View File
@@ -5,3 +5,30 @@ body {
ul {
list-style: none;
}
* {
scrollbar-width: thin;
}
::-webkit-scrollbar {
height: 6px;
width: 6px;
}
::-webkit-scrollbar-corner {
background: none;
}
::-webkit-scrollbar-track {
opacity: 0;
}
::-webkit-scrollbar-thumb {
border-radius: 4px;
background: #8d8d8d;
}
::-webkit-scrollbar-thumb:hover {
border-radius: 4px;
background: lighten(#8d8d8d, 10%);
}
+3 -1
View File
@@ -6,7 +6,9 @@
flex: 1;
min-width: 240px;
max-width: 250px;
overflow: auto;
height: 100%;
overflow-x: hidden;
overflow-y: overlay;
position: relative;
z-index: 2;
transition: transform 0.2s;
+2 -1
View File
@@ -9,6 +9,7 @@ $table--heading--resize--indicator--width: 1px;
@include theme('color', 'table--heading--color');
display: flex;
height: 24px;
overflow: hidden;
font-size: 12px;
white-space: nowrap;
z-index: 1;
@@ -121,7 +122,7 @@ $table--heading--resize--indicator--width: 1px;
bottom: 0;
left: 0;
opacity: 0;
position: absolute;
position: fixed;
top: 0;
transition: opacity 0.125s;
will-change: opacity, transform;
+3 -1
View File
@@ -475,9 +475,11 @@ $more-info--border: $textbox-repeater--button--border;
@include theme('border-top', 'torrent--border');
display: flex;
height: 30px;
min-width: max-content;
max-width: 100%;
padding: 0;
&:nth-child(0n + 2) {
&:nth-child(0n + 1) {
border-top: none;
}
+9 -2
View File
@@ -2,8 +2,9 @@
@include theme('background', 'torrent-list--background');
@include theme('box-shadow', 'torrent-list--border');
display: flex;
flex: 1 1 auto;
flex: 1 1 0px;
flex-direction: column;
overflow: hidden;
position: relative;
.loading-indicator {
@@ -52,11 +53,18 @@
}
}
&__viewport {
overflow-x: auto;
overflow-y: overlay;
height: 100%;
}
&__wrapper {
display: flex;
flex: 1 1 auto;
flex-direction: column;
height: 100%;
width: 100%;
justify-content: center;
list-style: none;
opacity: 1;
@@ -101,7 +109,6 @@
box-shadow: -1px 0 $torrent-list--border;
display: flex;
flex-direction: column;
flex: 1;
flex: 0 1 100%;
}
}
+1
View File
@@ -477,6 +477,7 @@ declare const styles: {
readonly torrent__list: string;
readonly 'torrent__list__scrollbars--horizontal': string;
readonly 'torrent__list__scrollbars--vertical': string;
readonly torrent__list__viewport: string;
readonly torrent__list__wrapper: string;
readonly 'torrent__list--loading-enter': string;
readonly 'torrent__list--loading-enter-active': string;
-158
View File
@@ -48,7 +48,6 @@
"@types/passport": "^1.0.4",
"@types/passport-jwt": "^3.0.3",
"@types/react": "^16.9.52",
"@types/react-custom-scrollbars": "^4.0.7",
"@types/react-dnd-multi-backend": "^6.0.0",
"@types/react-dom": "^16.9.8",
"@types/react-measure": "^2.0.6",
@@ -128,7 +127,6 @@
"prettier": "^2.1.2",
"promise": "^8.1.0",
"react": "^16.14.0",
"react-custom-scrollbars": "^4.2.1",
"react-dev-utils": "^10.2.1",
"react-dnd": "^11.1.3",
"react-dnd-html5-backend": "^11.1.3",
@@ -3105,15 +3103,6 @@
"csstype": "^3.0.2"
}
},
"node_modules/@types/react-custom-scrollbars": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/@types/react-custom-scrollbars/-/react-custom-scrollbars-4.0.7.tgz",
"integrity": "sha512-4QPZdwd+wmzWq9TyNSA/4MZFYvlQn1GlEFFkpFx8VSs13gR/L+hQne0vFnbzwlQmGG7OksthkoVpYxWJjzz95w==",
"dev": true,
"dependencies": {
"@types/react": "*"
}
},
"node_modules/@types/react-dnd-multi-backend": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@types/react-dnd-multi-backend/-/react-dnd-multi-backend-6.0.0.tgz",
@@ -3806,12 +3795,6 @@
"node": ">=0.4.0"
}
},
"node_modules/add-px-to-style": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/add-px-to-style/-/add-px-to-style-1.0.0.tgz",
"integrity": "sha1-0ME1RB+oAUqBN5BFMQlvZ/KPJjo=",
"dev": true
},
"node_modules/address": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/address/-/address-1.1.2.tgz",
@@ -7933,17 +7916,6 @@
"utila": "~0.4"
}
},
"node_modules/dom-css": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/dom-css/-/dom-css-2.1.0.tgz",
"integrity": "sha1-/bwtWgFdCj4YcuEUcrvQ57nmogI=",
"dev": true,
"dependencies": {
"add-px-to-style": "1.0.0",
"prefix-style": "2.0.1",
"to-camel-case": "1.0.0"
}
},
"node_modules/dom-helpers": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.0.tgz",
@@ -20189,12 +20161,6 @@
"node": ">=0.10.0"
}
},
"node_modules/prefix-style": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/prefix-style/-/prefix-style-2.0.1.tgz",
"integrity": "sha1-ZrupqHDP2jCKXcIOhekSCTLJWgY=",
"dev": true
},
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -20470,15 +20436,6 @@
}
]
},
"node_modules/raf": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
"integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
"dev": true,
"dependencies": {
"performance-now": "^2.1.0"
}
},
"node_modules/randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@@ -20558,21 +20515,6 @@
"node": ">=0.10.0"
}
},
"node_modules/react-custom-scrollbars": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/react-custom-scrollbars/-/react-custom-scrollbars-4.2.1.tgz",
"integrity": "sha1-gw/ZUCkn6X6KeMIIaBOJmyqLZts=",
"dev": true,
"dependencies": {
"dom-css": "^2.0.0",
"prop-types": "^15.5.10",
"raf": "^3.1.0"
},
"peerDependencies": {
"react": "^0.14.0 || ^15.0.0 || ^16.0.0",
"react-dom": "^0.14.0 || ^15.0.0 || ^16.0.0"
}
},
"node_modules/react-dev-utils": {
"version": "10.2.1",
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-10.2.1.tgz",
@@ -25009,15 +24951,6 @@
"integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=",
"dev": true
},
"node_modules/to-camel-case": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/to-camel-case/-/to-camel-case-1.0.0.tgz",
"integrity": "sha1-GlYFSy+daWKYzmamCJcyK29CPkY=",
"dev": true,
"dependencies": {
"to-space-case": "^1.0.0"
}
},
"node_modules/to-fast-properties": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
@@ -25027,12 +24960,6 @@
"node": ">=4"
}
},
"node_modules/to-no-case": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/to-no-case/-/to-no-case-1.0.2.tgz",
"integrity": "sha1-xyKQcWTvaxeBMsjmmTAhLRtKoWo=",
"dev": true
},
"node_modules/to-object-path": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz",
@@ -25084,15 +25011,6 @@
"node": ">=8.0"
}
},
"node_modules/to-space-case": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/to-space-case/-/to-space-case-1.0.0.tgz",
"integrity": "sha1-sFLar7Gysp3HcM6gFj5ewOvJ/Bc=",
"dev": true,
"dependencies": {
"to-no-case": "^1.0.0"
}
},
"node_modules/toidentifier": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
@@ -30859,15 +30777,6 @@
"csstype": "^3.0.2"
}
},
"@types/react-custom-scrollbars": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/@types/react-custom-scrollbars/-/react-custom-scrollbars-4.0.7.tgz",
"integrity": "sha512-4QPZdwd+wmzWq9TyNSA/4MZFYvlQn1GlEFFkpFx8VSs13gR/L+hQne0vFnbzwlQmGG7OksthkoVpYxWJjzz95w==",
"dev": true,
"requires": {
"@types/react": "*"
}
},
"@types/react-dnd-multi-backend": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@types/react-dnd-multi-backend/-/react-dnd-multi-backend-6.0.0.tgz",
@@ -31456,12 +31365,6 @@
"integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==",
"dev": true
},
"add-px-to-style": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/add-px-to-style/-/add-px-to-style-1.0.0.tgz",
"integrity": "sha1-0ME1RB+oAUqBN5BFMQlvZ/KPJjo=",
"dev": true
},
"address": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/address/-/address-1.1.2.tgz",
@@ -34865,17 +34768,6 @@
"utila": "~0.4"
}
},
"dom-css": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/dom-css/-/dom-css-2.1.0.tgz",
"integrity": "sha1-/bwtWgFdCj4YcuEUcrvQ57nmogI=",
"dev": true,
"requires": {
"add-px-to-style": "1.0.0",
"prefix-style": "2.0.1",
"to-camel-case": "1.0.0"
}
},
"dom-helpers": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.0.tgz",
@@ -44676,12 +44568,6 @@
"integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==",
"dev": true
},
"prefix-style": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/prefix-style/-/prefix-style-2.0.1.tgz",
"integrity": "sha1-ZrupqHDP2jCKXcIOhekSCTLJWgY=",
"dev": true
},
"prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -44909,15 +44795,6 @@
"integrity": "sha512-J95OVUiS4b8qqmpqhCodN8yPpHG2mpZUPQ8tDGyIY0VhM+kBHszOuvsMJVGNQ1OH2BnTFbqz45i+2jGpDw9H0w==",
"dev": true
},
"raf": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz",
"integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==",
"dev": true,
"requires": {
"performance-now": "^2.1.0"
}
},
"randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@@ -44987,17 +44864,6 @@
"prop-types": "^15.6.2"
}
},
"react-custom-scrollbars": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/react-custom-scrollbars/-/react-custom-scrollbars-4.2.1.tgz",
"integrity": "sha1-gw/ZUCkn6X6KeMIIaBOJmyqLZts=",
"dev": true,
"requires": {
"dom-css": "^2.0.0",
"prop-types": "^15.5.10",
"raf": "^3.1.0"
}
},
"react-dev-utils": {
"version": "10.2.1",
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-10.2.1.tgz",
@@ -48618,27 +48484,12 @@
"integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=",
"dev": true
},
"to-camel-case": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/to-camel-case/-/to-camel-case-1.0.0.tgz",
"integrity": "sha1-GlYFSy+daWKYzmamCJcyK29CPkY=",
"dev": true,
"requires": {
"to-space-case": "^1.0.0"
}
},
"to-fast-properties": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
"integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=",
"dev": true
},
"to-no-case": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/to-no-case/-/to-no-case-1.0.2.tgz",
"integrity": "sha1-xyKQcWTvaxeBMsjmmTAhLRtKoWo=",
"dev": true
},
"to-object-path": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz",
@@ -48680,15 +48531,6 @@
"is-number": "^7.0.0"
}
},
"to-space-case": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/to-space-case/-/to-space-case-1.0.0.tgz",
"integrity": "sha1-sFLar7Gysp3HcM6gFj5ewOvJ/Bc=",
"dev": true,
"requires": {
"to-no-case": "^1.0.0"
}
},
"toidentifier": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
-2
View File
@@ -87,7 +87,6 @@
"@types/passport": "^1.0.4",
"@types/passport-jwt": "^3.0.3",
"@types/react": "^16.9.52",
"@types/react-custom-scrollbars": "^4.0.7",
"@types/react-dnd-multi-backend": "^6.0.0",
"@types/react-dom": "^16.9.8",
"@types/react-measure": "^2.0.6",
@@ -167,7 +166,6 @@
"prettier": "^2.1.2",
"promise": "^8.1.0",
"react": "^16.14.0",
"react-custom-scrollbars": "^4.2.1",
"react-dev-utils": "^10.2.1",
"react-dnd": "^11.1.3",
"react-dnd-html5-backend": "^11.1.3",