mirror of
https://github.com/zoriya/flood.git
synced 2026-06-06 03:56:42 +00:00
client: migrate file tree componenets to FC
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
import classnames from 'classnames';
|
||||
import {Component, ReactNode, ReactText} from 'react';
|
||||
import {FC, ReactText, useRef, useState} from 'react';
|
||||
|
||||
import {Checkbox} from '@client/ui';
|
||||
import {Checkmark, Clipboard, File as FileIcon} from '@client/ui/icons';
|
||||
import ConfigStore from '@client/stores/ConfigStore';
|
||||
import TorrentActions from '@client/actions/TorrentActions';
|
||||
|
||||
import type {TorrentContent, TorrentContentSelection, TorrentContentSelectionTree} from '@shared/types/TorrentContent';
|
||||
import type {TorrentContentSelection, TorrentContentSelectionTree} from '@shared/types/TorrentContent';
|
||||
import type {TorrentProperties} from '@shared/types/Torrent';
|
||||
|
||||
import PriorityMeter from '../PriorityMeter';
|
||||
@@ -20,177 +20,127 @@ interface DirectoryFilesProps {
|
||||
onItemSelect: (selection: TorrentContentSelection) => void;
|
||||
}
|
||||
|
||||
interface DirectoryFilesStates {
|
||||
copiedToClipboard: number | null;
|
||||
}
|
||||
const DirectoryFiles: FC<DirectoryFilesProps> = ({depth, items, hash, path, onItemSelect}: DirectoryFilesProps) => {
|
||||
const [copiedToClipboard, setCopiedToClipboard] = useState<number | null>(null);
|
||||
const contentPermalinks = useRef<Record<number, string | null>>({});
|
||||
|
||||
class DirectoryFiles extends Component<DirectoryFilesProps, DirectoryFilesStates> {
|
||||
contentPermalinks: Record<number, string | null> = {};
|
||||
|
||||
static defaultProps = {
|
||||
path: [],
|
||||
items: {},
|
||||
};
|
||||
|
||||
constructor(props: DirectoryFilesProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
copiedToClipboard: null,
|
||||
};
|
||||
if (items == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
getCurrentPath(file: TorrentContent): string[] {
|
||||
const {path} = this.props;
|
||||
const files = Object.values(items)
|
||||
.sort((a, b) => a.filename.localeCompare(b.filename))
|
||||
.map((file) => {
|
||||
const isSelected = (items && items[file.filename] && items[file.filename].isSelected) || false;
|
||||
const classes = classnames(
|
||||
'directory-tree__node file',
|
||||
'directory-tree__node--file directory-tree__node--selectable',
|
||||
{
|
||||
'directory-tree__node--selected': isSelected,
|
||||
},
|
||||
);
|
||||
|
||||
return [...path, file.filename];
|
||||
}
|
||||
|
||||
getIcon(file: TorrentContent, isSelected: boolean): ReactNode {
|
||||
return (
|
||||
<div className="file__checkbox directory-tree__checkbox">
|
||||
<div
|
||||
className="directory-tree__checkbox__item
|
||||
directory-tree__checkbox__item--checkbox">
|
||||
<Checkbox checked={isSelected} id={`${file.index}`} onClick={() => this.handleFileSelect(file, isSelected)} />
|
||||
</div>
|
||||
<div
|
||||
className="directory-tree__checkbox__item
|
||||
directory-tree__checkbox__item--icon">
|
||||
<FileIcon />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
handlePriorityChange = (fileIndex: ReactText, priorityLevel: number): void => {
|
||||
const {hash} = this.props;
|
||||
|
||||
TorrentActions.setFilePriority(hash, {
|
||||
indices: [Number(fileIndex)],
|
||||
priority: priorityLevel,
|
||||
});
|
||||
};
|
||||
|
||||
handleFileSelect = (file: TorrentContent, isSelected: boolean): void => {
|
||||
const {depth, onItemSelect} = this.props;
|
||||
|
||||
onItemSelect({
|
||||
type: 'file',
|
||||
depth,
|
||||
path: this.getCurrentPath(file),
|
||||
select: !isSelected,
|
||||
});
|
||||
};
|
||||
|
||||
render(): ReactNode {
|
||||
const {items, hash} = this.props;
|
||||
|
||||
if (items == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const files = Object.values(items)
|
||||
.sort((a, b) => a.filename.localeCompare(b.filename))
|
||||
.map((file) => {
|
||||
const {copiedToClipboard} = this.state;
|
||||
|
||||
const isSelected = (items && items[file.filename] && items[file.filename].isSelected) || false;
|
||||
const classes = classnames(
|
||||
'directory-tree__node file',
|
||||
'directory-tree__node--file directory-tree__node--selectable',
|
||||
{
|
||||
'directory-tree__node--selected': isSelected,
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={classes} key={file.filename} title={file.filename}>
|
||||
<div className="file__label file__detail">
|
||||
{this.getIcon(file, isSelected)}
|
||||
<div className="file__name">
|
||||
{/* TODO: Add a WebAssembly decoding player if the feature is popular */}
|
||||
<a
|
||||
href={`${ConfigStore.baseURI}api/torrents/${hash}/contents/${file.index}/data`}
|
||||
style={{textDecoration: 'none'}}
|
||||
target="_blank"
|
||||
rel="noreferrer">
|
||||
{file.filename}
|
||||
</a>
|
||||
return (
|
||||
<div className={classes} key={file.filename} title={file.filename}>
|
||||
<div className="file__label file__detail">
|
||||
<div className="file__checkbox directory-tree__checkbox">
|
||||
<div className="directory-tree__checkbox__item directory-tree__checkbox__item--checkbox">
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
id={`${file.index}`}
|
||||
onClick={() =>
|
||||
onItemSelect({
|
||||
type: 'file',
|
||||
depth,
|
||||
path: [...path, file.filename],
|
||||
select: !isSelected,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="directory-tree__checkbox__item directory-tree__checkbox__item--icon">
|
||||
<FileIcon />
|
||||
</div>
|
||||
</div>
|
||||
<div className="file__detail file__detail--secondary">
|
||||
<Size value={file.sizeBytes} precision={1} />
|
||||
<div className="file__name">
|
||||
{/* TODO: Add a WebAssembly decoding player if the feature is popular */}
|
||||
<a
|
||||
href={`${ConfigStore.baseURI}api/torrents/${hash}/contents/${file.index}/data`}
|
||||
style={{textDecoration: 'none'}}
|
||||
target="_blank"
|
||||
rel="noreferrer">
|
||||
{file.filename}
|
||||
</a>
|
||||
</div>
|
||||
<div className="file__detail file__detail--secondary">{Math.trunc(file.percentComplete)}%</div>
|
||||
<div
|
||||
className="file__detail file__detail--secondary
|
||||
file__detail--priority">
|
||||
<PriorityMeter
|
||||
key={`${file.index}-${file.filename}`}
|
||||
level={file.priority}
|
||||
id={file.index}
|
||||
maxLevel={2}
|
||||
onChange={this.handlePriorityChange}
|
||||
priorityType="file"
|
||||
/>
|
||||
</div>
|
||||
{typeof navigator.clipboard?.writeText === 'function' && (
|
||||
<button
|
||||
className="file__detail file__detail--secondary file__detail--clipboard"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const copy = (link: string): void => {
|
||||
if (link !== '') {
|
||||
if (typeof navigator.share === 'function') {
|
||||
navigator
|
||||
.share({
|
||||
title: file.filename,
|
||||
url: link,
|
||||
})
|
||||
.then(() => {
|
||||
this.setState({
|
||||
copiedToClipboard: file.index,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
navigator.clipboard.writeText(link).then(() => {
|
||||
this.setState({
|
||||
copiedToClipboard: file.index,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
// Safari does not support async operations inside "user gesture" handler.
|
||||
// Otherwise the write to clipboard will be rejected for "security reasons".
|
||||
// As such, we cache the token, so next click can be synchronous. Incompatible
|
||||
// morons make everyone's life hard.
|
||||
const link = this.contentPermalinks[file.index];
|
||||
if (link != null) {
|
||||
copy(link);
|
||||
} else {
|
||||
this.contentPermalinks[file.index] = '';
|
||||
TorrentActions.getTorrentContentsDataPermalink(hash, [file.index]).then(
|
||||
(url) => {
|
||||
this.contentPermalinks[file.index] = url;
|
||||
copy(url);
|
||||
},
|
||||
() => {
|
||||
this.contentPermalinks[file.index] = null;
|
||||
},
|
||||
);
|
||||
}
|
||||
}}>
|
||||
{copiedToClipboard === file.index ? <Checkmark /> : <Clipboard />}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
<div className="file__detail file__detail--secondary">
|
||||
<Size value={file.sizeBytes} precision={1} />
|
||||
</div>
|
||||
<div className="file__detail file__detail--secondary">{Math.trunc(file.percentComplete)}%</div>
|
||||
<div
|
||||
className="file__detail file__detail--secondary
|
||||
file__detail--priority">
|
||||
<PriorityMeter
|
||||
key={`${file.index}-${file.filename}`}
|
||||
level={file.priority}
|
||||
id={file.index}
|
||||
maxLevel={2}
|
||||
onChange={(fileIndex: ReactText, priorityLevel: number) =>
|
||||
TorrentActions.setFilePriority(hash, {
|
||||
indices: [Number(fileIndex)],
|
||||
priority: priorityLevel,
|
||||
})
|
||||
}
|
||||
priorityType="file"
|
||||
/>
|
||||
</div>
|
||||
{typeof navigator.clipboard?.writeText === 'function' && (
|
||||
<button
|
||||
className="file__detail file__detail--secondary file__detail--clipboard"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const copy = (link: string): void => {
|
||||
if (link !== '') {
|
||||
if (typeof navigator.share === 'function') {
|
||||
navigator
|
||||
.share({
|
||||
title: file.filename,
|
||||
url: link,
|
||||
})
|
||||
.then(() => setCopiedToClipboard(file.index));
|
||||
} else {
|
||||
navigator.clipboard.writeText(link).then(() => setCopiedToClipboard(file.index));
|
||||
}
|
||||
}
|
||||
};
|
||||
// Safari does not support async operations inside "user gesture" handler.
|
||||
// Otherwise the write to clipboard will be rejected for "security reasons".
|
||||
// As such, we cache the token, so next click can be synchronous. Incompatible
|
||||
// morons make everyone's life hard.
|
||||
const link = contentPermalinks.current[file.index];
|
||||
if (link != null) {
|
||||
copy(link);
|
||||
} else {
|
||||
contentPermalinks.current[file.index] = '';
|
||||
TorrentActions.getTorrentContentsDataPermalink(hash, [file.index]).then(
|
||||
(url) => {
|
||||
contentPermalinks.current[file.index] = url;
|
||||
copy(url);
|
||||
},
|
||||
() => {
|
||||
contentPermalinks.current[file.index] = null;
|
||||
},
|
||||
);
|
||||
}
|
||||
}}>
|
||||
{copiedToClipboard === file.index ? <Checkmark /> : <Clipboard />}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
return <div className="directory-tree__node directory-tree__node--file-list">{files}</div>;
|
||||
}
|
||||
}
|
||||
return <div className="directory-tree__node directory-tree__node--file-list">{files}</div>;
|
||||
};
|
||||
|
||||
export default DirectoryFiles;
|
||||
|
||||
@@ -9,8 +9,8 @@ import DirectoryFileList from './DirectoryFileList';
|
||||
import DirectoryTreeNode from './DirectoryTreeNode';
|
||||
|
||||
interface DirectoryTreeProps {
|
||||
depth?: number;
|
||||
path?: Array<string>;
|
||||
depth: number;
|
||||
path: Array<string>;
|
||||
hash: TorrentProperties['hash'];
|
||||
itemsTree: TorrentContentSelectionTree;
|
||||
onItemSelect: (selection: TorrentContentSelection) => void;
|
||||
@@ -68,9 +68,4 @@ const DirectoryTree: FC<DirectoryTreeProps> = (props: DirectoryTreeProps) => {
|
||||
return <div className="directory-tree__tree">{directoryNodes.concat(fileList)}</div>;
|
||||
};
|
||||
|
||||
DirectoryTree.defaultProps = {
|
||||
depth: 0,
|
||||
path: [],
|
||||
};
|
||||
|
||||
export default DirectoryTree;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import classnames from 'classnames';
|
||||
import {Component, ReactNode} from 'react';
|
||||
import {FC, useState} from 'react';
|
||||
|
||||
import {Checkbox} from '@client/ui';
|
||||
import {FolderClosedSolid, FolderOpenSolid} from '@client/ui/icons';
|
||||
@@ -22,138 +22,87 @@ interface DirectoryTreeNodeProps {
|
||||
onItemSelect: (selection: TorrentContentSelection) => void;
|
||||
}
|
||||
|
||||
interface DirectoryTreeNodeStates {
|
||||
expanded: boolean;
|
||||
}
|
||||
const DirectoryTreeNode: FC<DirectoryTreeNodeProps> = ({
|
||||
depth,
|
||||
directoryName,
|
||||
id,
|
||||
itemsTree,
|
||||
hash,
|
||||
path,
|
||||
isSelected,
|
||||
onItemSelect,
|
||||
}: DirectoryTreeNodeProps) => {
|
||||
const [expanded, setExpanded] = useState<boolean>(false);
|
||||
const currentPath = [...path, directoryName];
|
||||
|
||||
class DirectoryTreeNode extends Component<DirectoryTreeNodeProps, DirectoryTreeNodeStates> {
|
||||
static defaultProps = {
|
||||
path: [],
|
||||
selectedItems: {},
|
||||
};
|
||||
|
||||
constructor(props: DirectoryTreeNodeProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
expanded: false,
|
||||
};
|
||||
}
|
||||
|
||||
getCurrentPath(): string[] {
|
||||
const {path, directoryName} = this.props;
|
||||
|
||||
return [...path, directoryName];
|
||||
}
|
||||
|
||||
getIcon(): ReactNode {
|
||||
const {id, isSelected} = this.props;
|
||||
const {expanded} = this.state;
|
||||
|
||||
let icon = null;
|
||||
if (expanded) {
|
||||
icon = <FolderOpenSolid />;
|
||||
} else {
|
||||
icon = <FolderClosedSolid />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="file__checkbox directory-tree__checkbox">
|
||||
<div
|
||||
className="directory-tree__checkbox__item
|
||||
return (
|
||||
<div
|
||||
className={classnames('directory-tree__branch', `directory-tree__branch--depth-${depth}`, {
|
||||
'directory-tree__node--selected': isSelected,
|
||||
})}>
|
||||
<button
|
||||
className={classnames(
|
||||
'directory-tree__node',
|
||||
'directory-tree__node--selectable directory-tree__node--directory',
|
||||
{
|
||||
'is-expanded': expanded,
|
||||
},
|
||||
)}
|
||||
css={{
|
||||
width: '100%',
|
||||
textAlign: 'left',
|
||||
':focus': {
|
||||
outline: 'none',
|
||||
WebkitTapHighlightColor: 'transparent',
|
||||
},
|
||||
':focus-visible': {
|
||||
outline: 'dashed',
|
||||
},
|
||||
}}
|
||||
type="button"
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
title={directoryName}>
|
||||
<div className="file__label">
|
||||
<div className="file__checkbox directory-tree__checkbox">
|
||||
<div
|
||||
className="directory-tree__checkbox__item
|
||||
directory-tree__checkbox__item--checkbox">
|
||||
<Checkbox checked={isSelected} id={id} onClick={this.handleDirectorySelection} />
|
||||
</div>
|
||||
<div
|
||||
className="directory-tree__checkbox__item
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
id={id}
|
||||
onClick={() =>
|
||||
onItemSelect({
|
||||
type: 'directory',
|
||||
depth,
|
||||
path: currentPath,
|
||||
select: !isSelected,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="directory-tree__checkbox__item
|
||||
directory-tree__checkbox__item--icon">
|
||||
{icon}
|
||||
{expanded ? <FolderOpenSolid /> : <FolderClosedSolid />}
|
||||
</div>
|
||||
</div>
|
||||
<div className="file__name">{directoryName}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
getSubTree(): ReactNode {
|
||||
const {depth, itemsTree, hash, onItemSelect} = this.props;
|
||||
const {expanded} = this.state;
|
||||
|
||||
if (expanded) {
|
||||
return (
|
||||
</button>
|
||||
{expanded ? (
|
||||
<div className="directory-tree__node directory-tree__node--group">
|
||||
<DirectoryTree
|
||||
depth={depth}
|
||||
hash={hash}
|
||||
key={`${expanded}-${depth}`}
|
||||
onItemSelect={onItemSelect}
|
||||
path={this.getCurrentPath()}
|
||||
path={currentPath}
|
||||
itemsTree={itemsTree}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
handleDirectoryClick = (): void => {
|
||||
this.setState((state) => ({
|
||||
expanded: !state.expanded,
|
||||
}));
|
||||
};
|
||||
|
||||
handleDirectorySelection = (): void => {
|
||||
const {depth, isSelected, onItemSelect} = this.props;
|
||||
|
||||
onItemSelect({
|
||||
type: 'directory',
|
||||
depth,
|
||||
path: this.getCurrentPath(),
|
||||
select: !isSelected,
|
||||
});
|
||||
};
|
||||
|
||||
render(): ReactNode {
|
||||
const {depth, directoryName, isSelected} = this.props;
|
||||
const {expanded} = this.state;
|
||||
|
||||
const branchClasses = classnames('directory-tree__branch', `directory-tree__branch--depth-${depth}`, {
|
||||
'directory-tree__node--selected': isSelected,
|
||||
});
|
||||
const directoryClasses = classnames(
|
||||
'directory-tree__node',
|
||||
'directory-tree__node--selectable directory-tree__node--directory',
|
||||
{
|
||||
'is-expanded': expanded,
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={branchClasses}>
|
||||
<button
|
||||
className={directoryClasses}
|
||||
css={{
|
||||
width: '100%',
|
||||
textAlign: 'left',
|
||||
':focus': {
|
||||
outline: 'none',
|
||||
WebkitTapHighlightColor: 'transparent',
|
||||
},
|
||||
':focus-visible': {
|
||||
outline: 'dashed',
|
||||
},
|
||||
}}
|
||||
type="button"
|
||||
onClick={this.handleDirectoryClick}
|
||||
title={directoryName}>
|
||||
<div className="file__label">
|
||||
{this.getIcon()}
|
||||
<div className="file__name">{directoryName}</div>
|
||||
</div>
|
||||
</button>
|
||||
{this.getSubTree()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DirectoryTreeNode;
|
||||
|
||||
@@ -79,6 +79,7 @@ const TorrentContents: FC = observer(() => {
|
||||
setSelectedIndices(selectionTree.getSelectedItems(newItemsTree));
|
||||
}}
|
||||
hash={hash}
|
||||
path={[]}
|
||||
itemsTree={itemsTree}
|
||||
/>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user