diff --git a/client/src/javascript/components/general/form-elements/FileDropzone.tsx b/client/src/javascript/components/general/form-elements/FileDropzone.tsx new file mode 100644 index 00000000..cf2da7cf --- /dev/null +++ b/client/src/javascript/components/general/form-elements/FileDropzone.tsx @@ -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 = ({onFilesChanged}: FileDropzoneProps) => { + const [files, setFiles] = useState([]); + + useEffect(() => { + onFilesChanged(files); + }, [files]); + + return ( + + + {files.length > 0 ? ( +
    { + event.stopPropagation(); + }}> + {files.map((file, index) => ( +
  • + + + + {file.name} + { + const newArray = files.slice(); + newArray.splice(index, 1); + setFiles(newArray); + }}> + + +
  • + ))} +
+ ) : null} + ) => { + 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}) => ( +
+ +
+
+ +
+ {' '} + + + + . +
+
+ )} +
+
+ ); +}; + +export default FileDropzone; diff --git a/client/src/javascript/components/modals/add-torrents-modal/AddTorrentsActions.tsx b/client/src/javascript/components/modals/add-torrents-modal/AddTorrentsActions.tsx index 25cde039..5c29c21d 100644 --- a/client/src/javascript/components/modals/add-torrents-modal/AddTorrentsActions.tsx +++ b/client/src/javascript/components/modals/add-torrents-modal/AddTorrentsActions.tsx @@ -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) => void; } -class AddTorrentsActions extends React.PureComponent { - getActions(): Array { - 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 = ({ + isAddingTorrents, + onAddTorrentsClick, +}: AddTorrentsActionsProps) => { + const intl = useIntl(); + return ( + + ); +}; - render() { - return ; - } -} - -export default injectIntl(AddTorrentsActions); +export default AddTorrentsActions; diff --git a/client/src/javascript/components/modals/add-torrents-modal/AddTorrentsByCreation.tsx b/client/src/javascript/components/modals/add-torrents-modal/AddTorrentsByCreation.tsx index 3698d009..d0463902 100644 --- a/client/src/javascript/components/modals/add-torrents-modal/AddTorrentsByCreation.tsx +++ b/client/src/javascript/components/modals/add-torrents-modal/AddTorrentsByCreation.tsx @@ -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
(null); + const intl = useIntl(); + const [isCreatingTorrents, setIsCreatingTorrents] = useState(false); -class AddTorrentsByCreation extends Component { - 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; - 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 ( - { - this.formRef = ref; - }}> - + + + + - - - - - - - - - - - - - {intl.formatMessage({id: 'torrents.create.is.private.label'})} - - - - - - - - ); - } -} + + + + + + + + + + {intl.formatMessage({id: 'torrents.create.is.private.label'})} + + + + + + { + if (formRef.current == null) { + return; + } -export default injectIntl(AddTorrentsByCreation, {forwardRef: true}); + const formData = formRef.current.getFormData() as Partial; + 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} + /> + + ); +}; + +export default AddTorrentsByCreation; diff --git a/client/src/javascript/components/modals/add-torrents-modal/AddTorrentsByFile.tsx b/client/src/javascript/components/modals/add-torrents-modal/AddTorrentsByFile.tsx index d7868c59..c5c7ddd4 100644 --- a/client/src/javascript/components/modals/add-torrents-modal/AddTorrentsByFile.tsx +++ b/client/src/javascript/components/modals/add-torrents-modal/AddTorrentsByFile.tsx @@ -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; - files: Array<{ - name: string; - data: string; - }>; - isAddingTorrents: boolean; -} +const AddTorrentsByFile: FC = () => { + const filesRef = useRef([]); + const formRef = useRef
(null); + const intl = useIntl(); + const [isAddingTorrents, setIsAddingTorrents] = useState(false); -class AddTorrentsByFile extends Component { - 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 ? ( -
    { - event.stopPropagation(); - }}> - {files.map((file, index) => ( -
  • - - - - {file.name} - this.handleFileRemove(index)}> - - -
  • - ))} -
- ) : null; - - return ( - - - {fileContent} - - {({getRootProps, getInputProps, isDragActive}) => ( -
- -
-
- -
- {' '} - - - - . -
-
- )} -
-
- ); - } - - handleFileDrop = (files: Array) => { - 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; - - const filesData: Array = []; - 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 ( - { - this.formRef = ref; - }}> - {this.getFileDropzone()} - + + { + filesRef.current = files; + }} /> - - - - - - ); - } -} + + + + + + { + 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; + + const filesData: Array = []; + 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} + /> + + ); +}; + +export default AddTorrentsByFile; diff --git a/client/src/javascript/components/modals/add-torrents-modal/AddTorrentsByURL.tsx b/client/src/javascript/components/modals/add-torrents-modal/AddTorrentsByURL.tsx index 255dec84..7ec918e0 100644 --- a/client/src/javascript/components/modals/add-torrents-modal/AddTorrentsByURL.tsx +++ b/client/src/javascript/components/modals/add-torrents-modal/AddTorrentsByURL.tsx @@ -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
(null); + const intl = useIntl(); + const [isAddingTorrents, setIsAddingTorrents] = useState(false); -class AddTorrentsByURL extends Component { - 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; - 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 ( + + { - UIStore.dismissModal(); - }); - - saveAddTorrentsUserPreferences({start: formData.start, destination: formData.destination}); - }; - - render() { - return ( - { - this.formRef = ref; - }}> - - + + + + - - - - - - - ); - } -} + + { + if (formRef.current == null) { + return; + } -export default injectIntl(AddTorrentsByURL, {forwardRef: true}); + const formData = formRef.current.getFormData() as Partial; + 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} + /> + + ); +}; + +export default AddTorrentsByURL; diff --git a/client/src/javascript/components/modals/add-torrents-modal/AddTorrentsModal.tsx b/client/src/javascript/components/modals/add-torrents-modal/AddTorrentsModal.tsx index c9ae8464..19f48318 100644 --- a/client/src/javascript/components/modals/add-torrents-modal/AddTorrentsModal.tsx +++ b/client/src/javascript/components/modals/add-torrents-modal/AddTorrentsModal.tsx @@ -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 = {