client: convert "add torrents" modals to Functional Component

This commit is contained in:
Jesse Chan
2020-11-13 02:10:15 +08:00
parent 80ffb24d8d
commit 7542ad51e1
6 changed files with 405 additions and 459 deletions
@@ -0,0 +1,92 @@
import Dropzone from 'react-dropzone';
import {FormattedMessage} from 'react-intl';
import {FC, useEffect, useState} from 'react';
import CloseIcon from '../../icons/Close';
import FileIcon from '../../icons/File';
import FilesIcon from '../../icons/Files';
import {FormRowItem} from '../../../ui';
export type ProcessedFiles = Array<{name: string; data: string}>;
interface FileDropzoneProps {
onFilesChanged: (files: ProcessedFiles) => void;
}
const FileDropzone: FC<FileDropzoneProps> = ({onFilesChanged}: FileDropzoneProps) => {
const [files, setFiles] = useState<ProcessedFiles>([]);
useEffect(() => {
onFilesChanged(files);
}, [files]);
return (
<FormRowItem>
<label className="form__element__label">
<FormattedMessage id="torrents.add.torrents.label" />
</label>
{files.length > 0 ? (
<ul
className="dropzone__selected-files interactive-list"
onClick={(event) => {
event.stopPropagation();
}}>
{files.map((file, index) => (
<li className="dropzone__selected-files__file interactive-list__item" key={file.name} title={file.name}>
<span className="interactive-list__icon">
<FileIcon />
</span>
<span className="interactive-list__label">{file.name}</span>
<span
className="interactive-list__icon interactive-list__icon--action interactive-list__icon--action--warning"
onClick={() => {
const newArray = files.slice();
newArray.splice(index, 1);
setFiles(newArray);
}}>
<CloseIcon />
</span>
</li>
))}
</ul>
) : null}
<Dropzone
onDrop={(addedFiles: Array<File>) => {
addedFiles.forEach((file) => {
const reader = new FileReader();
reader.onload = (e) => {
if (e.target?.result != null && typeof e.target.result === 'string') {
setFiles(
files.concat({
name: file.name,
data: e.target.result.split('base64,')[1],
}),
);
}
};
reader.readAsDataURL(file);
});
}}>
{({getRootProps, getInputProps, isDragActive}) => (
<div
{...getRootProps()}
className={`form__dropzone dropzone interactive-list ${isDragActive ? 'dropzone--is-dragging' : ''}`}>
<input {...getInputProps()} />
<div className="dropzone__copy">
<div className="dropzone__icon">
<FilesIcon />
</div>
<FormattedMessage id="torrents.add.tab.file.drop" />{' '}
<span className="dropzone__browse-button">
<FormattedMessage id="torrents.add.tab.file.browse" />
</span>
.
</div>
</div>
)}
</Dropzone>
</FormRowItem>
);
};
export default FileDropzone;
@@ -1,53 +1,53 @@
import {injectIntl, WrappedComponentProps} from 'react-intl';
import * as React from 'react';
import {FC} from 'react';
import {useIntl} from 'react-intl';
import ModalActions from '../ModalActions';
import SettingStore from '../../../stores/SettingStore';
import type {ModalAction} from '../../../stores/UIStore';
interface AddTorrentsActionsProps extends WrappedComponentProps {
interface AddTorrentsActionsProps {
isAddingTorrents: boolean;
onAddTorrentsClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
}
class AddTorrentsActions extends React.PureComponent<AddTorrentsActionsProps> {
getActions(): Array<ModalAction> {
return [
{
checked: Boolean(SettingStore.floodSettings.startTorrentsOnLoad),
clickHandler: null,
content: this.props.intl.formatMessage({
id: 'torrents.add.start.label',
}),
id: 'start',
triggerDismiss: false,
type: 'checkbox',
},
{
clickHandler: null,
content: this.props.intl.formatMessage({
id: 'button.cancel',
}),
triggerDismiss: true,
type: 'tertiary',
},
{
clickHandler: this.props.onAddTorrentsClick,
content: this.props.intl.formatMessage({
id: 'torrents.add.button.add',
}),
isLoading: this.props.isAddingTorrents,
submit: true,
triggerDismiss: false,
type: 'primary',
},
];
}
const AddTorrentsActions: FC<AddTorrentsActionsProps> = ({
isAddingTorrents,
onAddTorrentsClick,
}: AddTorrentsActionsProps) => {
const intl = useIntl();
return (
<ModalActions
actions={[
{
checked: Boolean(SettingStore.floodSettings.startTorrentsOnLoad),
clickHandler: null,
content: intl.formatMessage({
id: 'torrents.add.start.label',
}),
id: 'start',
triggerDismiss: false,
type: 'checkbox',
},
{
clickHandler: null,
content: intl.formatMessage({
id: 'button.cancel',
}),
triggerDismiss: true,
type: 'tertiary',
},
{
clickHandler: onAddTorrentsClick,
content: intl.formatMessage({
id: 'torrents.add.button.add',
}),
isLoading: isAddingTorrents,
submit: true,
triggerDismiss: false,
type: 'primary',
},
]}
/>
);
};
render() {
return <ModalActions actions={this.getActions()} />;
}
}
export default injectIntl(AddTorrentsActions);
export default AddTorrentsActions;
@@ -1,5 +1,5 @@
import {Component} from 'react';
import {injectIntl, WrappedComponentProps} from 'react-intl';
import {FC, useRef, useState} from 'react';
import {useIntl} from 'react-intl';
import AddTorrentsActions from './AddTorrentsActions';
import {Checkbox, Form, FormRow, Textbox} from '../../../ui';
@@ -22,130 +22,110 @@ type AddTorrentsByCreationFormData = {
tags: string;
};
interface AddTorrentsByCreationStates {
isCreatingTorrents: boolean;
trackerTextboxes: Array<{id: number; value: string}>;
}
const AddTorrentsByCreation: FC = () => {
const formRef = useRef<Form>(null);
const intl = useIntl();
const [isCreatingTorrents, setIsCreatingTorrents] = useState<boolean>(false);
class AddTorrentsByCreation extends Component<WrappedComponentProps, AddTorrentsByCreationStates> {
formRef: Form | null = null;
constructor(props: WrappedComponentProps) {
super(props);
this.state = {
isCreatingTorrents: false,
trackerTextboxes: [{id: 0, value: ''}],
};
}
handleAddTorrents = () => {
if (this.formRef == null) {
return;
}
const formData = this.formRef.getFormData() as Partial<AddTorrentsByCreationFormData>;
this.setState({isCreatingTorrents: true});
if (formData.sourcePath == null) {
return;
}
TorrentActions.createTorrent({
name: formData.name,
sourcePath: formData.sourcePath,
trackers: getTextArray(formData, 'trackers'),
comment: formData.comment,
infoSource: formData.infoSource,
isPrivate: formData.isPrivate || false,
start: formData.start || false,
tags: formData.tags != null ? formData.tags.split(',') : undefined,
}).then(() => {
UIStore.dismissModal();
});
saveAddTorrentsUserPreferences({start: formData.start, destination: formData.sourcePath});
};
render() {
const {intl} = this.props;
const {isCreatingTorrents, trackerTextboxes} = this.state;
return (
<Form
className="inverse"
ref={(ref) => {
this.formRef = ref;
}}>
<FilesystemBrowserTextbox
id="sourcePath"
return (
<Form className="inverse" ref={formRef}>
<FilesystemBrowserTextbox
id="sourcePath"
label={intl.formatMessage({
id: 'torrents.create.source.path.label',
})}
/>
<TextboxRepeater
id="trackers"
label={intl.formatMessage({
id: 'torrents.create.trackers.label',
})}
placeholder={intl.formatMessage({
id: 'torrents.create.tracker.input.placeholder',
})}
defaultValues={[{id: 0, value: ''}]}
/>
<FormRow>
<Textbox
id="name"
label={intl.formatMessage({
id: 'torrents.create.source.path.label',
})}
/>
<TextboxRepeater
id="trackers"
label={intl.formatMessage({
id: 'torrents.create.trackers.label',
id: 'torrents.create.base.name.label',
})}
placeholder={intl.formatMessage({
id: 'torrents.create.tracker.input.placeholder',
id: 'torrents.create.base.name.input.placeholder',
})}
defaultValues={trackerTextboxes}
/>
<FormRow>
<Textbox
id="name"
label={intl.formatMessage({
id: 'torrents.create.base.name.label',
})}
placeholder={intl.formatMessage({
id: 'torrents.create.base.name.input.placeholder',
})}
/>
</FormRow>
<FormRow>
<Textbox
id="comment"
label={intl.formatMessage({
id: 'torrents.create.comment.label',
})}
placeholder={intl.formatMessage({
id: 'torrents.create.comment.input.placeholder',
})}
/>
</FormRow>
<FormRow>
<Textbox
id="infoSource"
label={intl.formatMessage({
id: 'torrents.create.info.source.label',
})}
placeholder={intl.formatMessage({
id: 'torrents.create.info.source.input.placeholder',
})}
/>
</FormRow>
<FormRow>
<Checkbox grow={false} id="isPrivate">
{intl.formatMessage({id: 'torrents.create.is.private.label'})}
</Checkbox>
</FormRow>
<FormRow>
<TagSelect
id="tags"
label={intl.formatMessage({
id: 'torrents.add.tags',
})}
placeholder={intl.formatMessage({
id: 'torrents.create.tags.input.placeholder',
})}
/>
</FormRow>
<AddTorrentsActions onAddTorrentsClick={this.handleAddTorrents} isAddingTorrents={isCreatingTorrents} />
</Form>
);
}
}
</FormRow>
<FormRow>
<Textbox
id="comment"
label={intl.formatMessage({
id: 'torrents.create.comment.label',
})}
placeholder={intl.formatMessage({
id: 'torrents.create.comment.input.placeholder',
})}
/>
</FormRow>
<FormRow>
<Textbox
id="infoSource"
label={intl.formatMessage({
id: 'torrents.create.info.source.label',
})}
placeholder={intl.formatMessage({
id: 'torrents.create.info.source.input.placeholder',
})}
/>
</FormRow>
<FormRow>
<Checkbox grow={false} id="isPrivate">
{intl.formatMessage({id: 'torrents.create.is.private.label'})}
</Checkbox>
</FormRow>
<FormRow>
<TagSelect
id="tags"
label={intl.formatMessage({
id: 'torrents.add.tags',
})}
placeholder={intl.formatMessage({
id: 'torrents.create.tags.input.placeholder',
})}
/>
</FormRow>
<AddTorrentsActions
onAddTorrentsClick={() => {
if (formRef.current == null) {
return;
}
export default injectIntl(AddTorrentsByCreation, {forwardRef: true});
const formData = formRef.current.getFormData() as Partial<AddTorrentsByCreationFormData>;
setIsCreatingTorrents(true);
if (formData.sourcePath == null) {
return;
}
TorrentActions.createTorrent({
name: formData.name,
sourcePath: formData.sourcePath,
trackers: getTextArray(formData, 'trackers'),
comment: formData.comment,
infoSource: formData.infoSource,
isPrivate: formData.isPrivate || false,
start: formData.start || false,
tags: formData.tags != null ? formData.tags.split(',') : undefined,
}).then(() => {
UIStore.dismissModal();
});
saveAddTorrentsUserPreferences({start: formData.start, destination: formData.sourcePath});
}}
isAddingTorrents={isCreatingTorrents}
/>
</Form>
);
};
export default AddTorrentsByCreation;
@@ -1,18 +1,17 @@
import {Component} from 'react';
import Dropzone from 'react-dropzone';
import {FormattedMessage, injectIntl, WrappedComponentProps} from 'react-intl';
import {FC, useRef, useState} from 'react';
import {useIntl} from 'react-intl';
import AddTorrentsActions from './AddTorrentsActions';
import CloseIcon from '../../icons/Close';
import FileIcon from '../../icons/File';
import FilesIcon from '../../icons/Files';
import FileDropzone from '../../general/form-elements/FileDropzone';
import FilesystemBrowserTextbox from '../../general/filesystem/FilesystemBrowserTextbox';
import {Form, FormRow, FormRowItem} from '../../../ui';
import {Form, FormRow} from '../../../ui';
import {saveAddTorrentsUserPreferences} from '../../../util/userPreferences';
import TagSelect from '../../general/form-elements/TagSelect';
import TorrentActions from '../../../actions/TorrentActions';
import UIStore from '../../../stores/UIStore';
import type {ProcessedFiles} from '../../general/form-elements/FileDropzone';
interface AddTorrentsByFileFormData {
destination: string;
start: boolean;
@@ -21,181 +20,76 @@ interface AddTorrentsByFileFormData {
isCompleted: boolean;
}
interface AddTorrentsByFileStates {
errors: Record<string, unknown>;
files: Array<{
name: string;
data: string;
}>;
isAddingTorrents: boolean;
}
const AddTorrentsByFile: FC = () => {
const filesRef = useRef<ProcessedFiles>([]);
const formRef = useRef<Form>(null);
const intl = useIntl();
const [isAddingTorrents, setIsAddingTorrents] = useState<boolean>(false);
class AddTorrentsByFile extends Component<WrappedComponentProps, AddTorrentsByFileStates> {
formRef: Form | null = null;
constructor(props: WrappedComponentProps) {
super(props);
this.state = {
errors: {},
files: [],
isAddingTorrents: false,
};
}
getFileDropzone() {
const {files} = this.state;
const fileContent =
files.length > 0 ? (
<ul
className="dropzone__selected-files interactive-list"
onClick={(event) => {
event.stopPropagation();
}}>
{files.map((file, index) => (
<li className="dropzone__selected-files__file interactive-list__item" key={file.name} title={file.name}>
<span className="interactive-list__icon">
<FileIcon />
</span>
<span className="interactive-list__label">{file.name}</span>
<span
className="interactive-list__icon interactive-list__icon--action interactive-list__icon--action--warning"
onClick={() => this.handleFileRemove(index)}>
<CloseIcon />
</span>
</li>
))}
</ul>
) : null;
return (
<FormRowItem>
<label className="form__element__label">
<FormattedMessage id="torrents.add.torrents.label" />
</label>
{fileContent}
<Dropzone onDrop={this.handleFileDrop}>
{({getRootProps, getInputProps, isDragActive}) => (
<div
{...getRootProps()}
className={`form__dropzone dropzone interactive-list ${isDragActive ? 'dropzone--is-dragging' : ''}`}>
<input {...getInputProps()} />
<div className="dropzone__copy">
<div className="dropzone__icon">
<FilesIcon />
</div>
<FormattedMessage id="torrents.add.tab.file.drop" />{' '}
<span className="dropzone__browse-button">
<FormattedMessage id="torrents.add.tab.file.browse" />
</span>
.
</div>
</div>
)}
</Dropzone>
</FormRowItem>
);
}
handleFileDrop = (files: Array<File>) => {
const nextErrorsState = this.state.errors;
if (nextErrorsState.files != null) {
delete nextErrorsState.files;
}
files.forEach((file) => {
const reader = new FileReader();
reader.onload = (e) => {
this.setState((state) => {
if (e.target?.result != null && typeof e.target.result === 'string') {
return {
errors: nextErrorsState,
files: state.files.concat({
name: file.name,
data: e.target.result.split('base64,')[1],
}),
};
}
return {errors: nextErrorsState, files: state.files};
});
};
reader.readAsDataURL(file);
});
};
handleFileRemove = (fileIndex: number) => {
const {files} = this.state;
files.splice(fileIndex, 1);
this.setState({files});
};
handleAddTorrents = () => {
if (this.formRef == null) {
return;
}
const formData = this.formRef.getFormData();
this.setState({isAddingTorrents: true});
const {destination, start, tags, isBasePath, isCompleted} = formData as Partial<AddTorrentsByFileFormData>;
const filesData: Array<string> = [];
this.state.files.forEach((file) => {
filesData.push(file.data);
});
if (filesData[0] == null || destination == null) {
this.setState({isAddingTorrents: false});
return;
}
TorrentActions.addTorrentsByFiles({
files: filesData as [string, ...string[]],
destination,
tags: tags != null ? tags.split(',') : undefined,
isBasePath,
isCompleted,
start,
}).then(() => {
UIStore.dismissModal();
});
saveAddTorrentsUserPreferences({start, destination});
};
render() {
const {intl} = this.props;
const {isAddingTorrents} = this.state;
return (
<Form
className="inverse"
ref={(ref) => {
this.formRef = ref;
}}>
<FormRow>{this.getFileDropzone()}</FormRow>
<FilesystemBrowserTextbox
id="destination"
label={intl.formatMessage({
id: 'torrents.add.destination.label',
})}
selectable="directories"
showBasePathToggle
showCompletedToggle
return (
<Form className="inverse" ref={formRef}>
<FormRow>
<FileDropzone
onFilesChanged={(files) => {
filesRef.current = files;
}}
/>
<FormRow>
<TagSelect
label={intl.formatMessage({
id: 'torrents.add.tags',
})}
id="tags"
/>
</FormRow>
<AddTorrentsActions onAddTorrentsClick={this.handleAddTorrents} isAddingTorrents={isAddingTorrents} />
</Form>
);
}
}
</FormRow>
<FilesystemBrowserTextbox
id="destination"
label={intl.formatMessage({
id: 'torrents.add.destination.label',
})}
selectable="directories"
showBasePathToggle
showCompletedToggle
/>
<FormRow>
<TagSelect
label={intl.formatMessage({
id: 'torrents.add.tags',
})}
id="tags"
/>
</FormRow>
<AddTorrentsActions
onAddTorrentsClick={() => {
if (formRef.current == null) {
return;
}
export default injectIntl(AddTorrentsByFile, {forwardRef: true});
const formData = formRef.current?.getFormData();
setIsAddingTorrents(true);
const {destination, start, tags, isBasePath, isCompleted} = formData as Partial<AddTorrentsByFileFormData>;
const filesData: Array<string> = [];
filesRef.current.forEach((file) => {
filesData.push(file.data);
});
if (filesData.length === 0 || destination == null) {
setIsAddingTorrents(false);
return;
}
TorrentActions.addTorrentsByFiles({
files: filesData as [string, ...string[]],
destination,
tags: tags != null ? tags.split(',') : undefined,
isBasePath,
isCompleted,
start,
}).then(() => {
UIStore.dismissModal();
});
saveAddTorrentsUserPreferences({start, destination});
}}
isAddingTorrents={isAddingTorrents}
/>
</Form>
);
};
export default AddTorrentsByFile;
@@ -1,5 +1,5 @@
import {Component} from 'react';
import {injectIntl, WrappedComponentProps} from 'react-intl';
import {FC, useRef, useState} from 'react';
import {useIntl} from 'react-intl';
import AddTorrentsActions from './AddTorrentsActions';
import FilesystemBrowserTextbox from '../../general/filesystem/FilesystemBrowserTextbox';
@@ -22,115 +22,95 @@ type AddTorrentsByURLFormData = {
tags: string;
};
interface AddTorrentsByURLStates {
isAddingTorrents: boolean;
urlTextboxes: Array<{id: number; value: string}>;
}
const AddTorrentsByURL: FC = () => {
const formRef = useRef<Form>(null);
const intl = useIntl();
const [isAddingTorrents, setIsAddingTorrents] = useState<boolean>(false);
class AddTorrentsByURL extends Component<WrappedComponentProps, AddTorrentsByURLStates> {
formRef: Form | null = null;
constructor(props: WrappedComponentProps) {
super(props);
this.state = {
isAddingTorrents: false,
urlTextboxes: (UIStore.activeModal?.id === 'add-torrents' && UIStore.activeModal?.initialURLs) || [
{id: 0, value: ''},
],
};
}
handleAddTorrents = () => {
if (this.formRef == null) {
return;
}
const formData = this.formRef.getFormData() as Partial<AddTorrentsByURLFormData>;
this.setState({isAddingTorrents: true});
const urls = getTextArray(formData, 'urls').filter((url) => url !== '');
if (urls[0] == null || formData.destination == null) {
this.setState({isAddingTorrents: false});
return;
}
const cookies = getTextArray(formData, 'cookies');
// TODO: handle multiple domain names
const firstDomain = urls[0].startsWith('http') && urls[0].split('/')[2];
const processedCookies = firstDomain
? {
[firstDomain]: cookies,
return (
<Form className="inverse" ref={formRef}>
<TextboxRepeater
id="urls"
label={intl.formatMessage({
id: 'torrents.add.torrents.label',
})}
placeholder={intl.formatMessage({
id: 'torrents.add.tab.url.input.placeholder',
})}
defaultValues={
(UIStore.activeModal?.id === 'add-torrents' && UIStore.activeModal?.initialURLs) || [{id: 0, value: ''}]
}
: undefined;
TorrentActions.addTorrentsByUrls({
urls: urls as [string, ...string[]],
cookies: processedCookies,
destination: formData.destination,
isBasePath: formData.isBasePath,
isCompleted: formData.isCompleted,
start: formData.start,
tags: formData.tags != null ? formData.tags.split(',') : undefined,
}).then(() => {
UIStore.dismissModal();
});
saveAddTorrentsUserPreferences({start: formData.start, destination: formData.destination});
};
render() {
return (
<Form
className="inverse"
ref={(ref) => {
this.formRef = ref;
}}>
<TextboxRepeater
id="urls"
label={this.props.intl.formatMessage({
id: 'torrents.add.torrents.label',
})}
placeholder={this.props.intl.formatMessage({
id: 'torrents.add.tab.url.input.placeholder',
})}
defaultValues={this.state.urlTextboxes}
/>
<TextboxRepeater
id="cookies"
label={this.props.intl.formatMessage({
id: 'torrents.add.cookies.label',
})}
placeholder={this.props.intl.formatMessage({
id: 'torrents.add.cookies.input.placeholder',
/>
<TextboxRepeater
id="cookies"
label={intl.formatMessage({
id: 'torrents.add.cookies.label',
})}
placeholder={intl.formatMessage({
id: 'torrents.add.cookies.input.placeholder',
})}
/>
<FilesystemBrowserTextbox
id="destination"
label={intl.formatMessage({
id: 'torrents.add.destination.label',
})}
selectable="directories"
showBasePathToggle
showCompletedToggle
/>
<FormRow>
<TagSelect
id="tags"
label={intl.formatMessage({
id: 'torrents.add.tags',
})}
/>
<FilesystemBrowserTextbox
id="destination"
label={this.props.intl.formatMessage({
id: 'torrents.add.destination.label',
})}
selectable="directories"
showBasePathToggle
showCompletedToggle
/>
<FormRow>
<TagSelect
id="tags"
label={this.props.intl.formatMessage({
id: 'torrents.add.tags',
})}
/>
</FormRow>
<AddTorrentsActions
onAddTorrentsClick={this.handleAddTorrents}
isAddingTorrents={this.state.isAddingTorrents}
/>
</Form>
);
}
}
</FormRow>
<AddTorrentsActions
onAddTorrentsClick={() => {
if (formRef.current == null) {
return;
}
export default injectIntl(AddTorrentsByURL, {forwardRef: true});
const formData = formRef.current.getFormData() as Partial<AddTorrentsByURLFormData>;
setIsAddingTorrents(true);
const urls = getTextArray(formData, 'urls').filter((url) => url !== '');
if (urls.length === 0 || formData.destination == null) {
setIsAddingTorrents(false);
return;
}
const cookies = getTextArray(formData, 'cookies');
// TODO: handle multiple domain names
const firstDomain = urls[0].startsWith('http') && urls[0].split('/')[2];
const processedCookies = firstDomain
? {
[firstDomain]: cookies,
}
: undefined;
TorrentActions.addTorrentsByUrls({
urls: urls as [string, ...string[]],
cookies: processedCookies,
destination: formData.destination,
isBasePath: formData.isBasePath,
isCompleted: formData.isCompleted,
start: formData.start,
tags: formData.tags != null ? formData.tags.split(',') : undefined,
}).then(() => {
UIStore.dismissModal();
});
saveAddTorrentsUserPreferences({start: formData.start, destination: formData.destination});
}}
isAddingTorrents={isAddingTorrents}
/>
</Form>
);
};
export default AddTorrentsByURL;
@@ -1,12 +1,12 @@
import {FC} from 'react';
import {useIntl} from 'react-intl';
import * as React from 'react';
import AddTorrentsByFile from './AddTorrentsByFile';
import AddTorrentsByURL from './AddTorrentsByURL';
import Modal from '../Modal';
import AddTorrentsByCreation from './AddTorrentsByCreation';
const AddTorrentsModal: React.FC = () => {
const AddTorrentsModal: FC = () => {
const intl = useIntl();
const tabs = {