diff --git a/client/src/javascript/actions/TorrentActions.ts b/client/src/javascript/actions/TorrentActions.ts index d779b518..bb0b84d7 100644 --- a/client/src/javascript/actions/TorrentActions.ts +++ b/client/src/javascript/actions/TorrentActions.ts @@ -11,6 +11,7 @@ import type { SetTorrentContentsPropertiesOptions, SetTorrentsPriorityOptions, SetTorrentsTagsOptions, + SetTorrentsTrackersOptions, StartTorrentsOptions, StopTorrentsOptions, } from '@shared/types/api/torrents'; @@ -244,13 +245,9 @@ const TorrentActions = { }, ), - setTracker: (hashes: Array, tracker: string, options = {}) => + setTrackers: (options: SetTorrentsTrackersOptions) => axios - .patch(`${baseURI}api/torrents/tracker`, { - hashes, - tracker, - options, - }) + .patch(`${baseURI}api/torrents/trackers`, options) .then((json) => json.data) .then( () => { diff --git a/client/src/javascript/components/modals/Modals.tsx b/client/src/javascript/components/modals/Modals.tsx index 36c84434..d6f5e078 100644 --- a/client/src/javascript/components/modals/Modals.tsx +++ b/client/src/javascript/components/modals/Modals.tsx @@ -8,7 +8,7 @@ import FeedsModal from './feeds-modal/FeedsModal'; import MoveTorrentsModal from './move-torrents-modal/MoveTorrentsModal'; import RemoveTorrentsModal from './remove-torrents-modal/RemoveTorrentsModal'; import SetTagsModal from './set-tags-modal/SetTagsModal'; -import SetTrackerModal from './set-tracker-modal/SetTrackerModal'; +import SetTrackersModal from './set-trackers-modal/SetTrackersModal'; import SettingsModal from './settings-modal/SettingsModal'; import TorrentDetailsModal from './torrent-details-modal/TorrentDetailsModal'; import UIActions from '../../actions/UIActions'; @@ -30,8 +30,8 @@ const createModal = (id: Modal['id']): React.ReactNode => { return ; case 'set-taxonomy': return ; - case 'set-tracker': - return ; + case 'set-trackers': + return ; case 'settings': return ; case 'torrent-details': diff --git a/client/src/javascript/components/modals/set-tags-modal/SetTagsModal.tsx b/client/src/javascript/components/modals/set-tags-modal/SetTagsModal.tsx index ba34b478..1450d1b0 100644 --- a/client/src/javascript/components/modals/set-tags-modal/SetTagsModal.tsx +++ b/client/src/javascript/components/modals/set-tags-modal/SetTagsModal.tsx @@ -56,7 +56,9 @@ class SetTagsModal extends React.Component TorrentStore.torrents[hash].tags)[0]} + defaultValue={TorrentStore.selectedTorrents + .map((hash: string) => TorrentStore.torrents[hash].tags)[0] + .slice()} id="tags" placeholder={this.props.intl.formatMessage({ id: 'torrents.set.tags.enter.tags', diff --git a/client/src/javascript/components/modals/set-tracker-modal/SetTrackerModal.tsx b/client/src/javascript/components/modals/set-tracker-modal/SetTrackerModal.tsx deleted file mode 100644 index db8c4801..00000000 --- a/client/src/javascript/components/modals/set-tracker-modal/SetTrackerModal.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import {injectIntl, WrappedComponentProps} from 'react-intl'; -import React from 'react'; - -import {Form, FormRow, Textbox} from '../../../ui'; -import Modal from '../Modal'; -import TorrentActions from '../../../actions/TorrentActions'; -import TorrentStore from '../../../stores/TorrentStore'; -import UIStore from '../../../stores/UIStore'; - -import type {ModalAction} from '../../../stores/UIStore'; - -interface SetTrackerModalStates { - isSettingTracker: boolean; -} - -class SetTrackerModal extends React.Component { - formRef: Form | null = null; - - constructor(props: WrappedComponentProps) { - super(props); - this.state = { - isSettingTracker: false, - }; - } - - getActions(): Array { - const primaryButtonText = this.props.intl.formatMessage({ - id: 'torrents.set.tracker.button.set', - }); - - return [ - { - clickHandler: null, - content: this.props.intl.formatMessage({ - id: 'button.cancel', - }), - triggerDismiss: true, - type: 'tertiary', - }, - { - clickHandler: this.handleSetTrackerClick, - content: primaryButtonText, - isLoading: this.state.isSettingTracker, - triggerDismiss: false, - type: 'primary', - }, - ]; - } - - getContent(): React.ReactNode { - const trackerValue = TorrentStore.selectedTorrents - .map((hash) => TorrentStore.torrents[hash].trackerURIs)[0] - .join(', '); - - return ( -
-
{ - this.formRef = ref; - }}> - - - -
-
- ); - } - - handleSetTrackerClick = (): void => { - if (this.formRef == null) { - return; - } - - const formData = this.formRef.getFormData() as {tracker: string}; - const {tracker} = formData; - - this.setState({isSettingTracker: true}, () => - TorrentActions.setTracker(TorrentStore.selectedTorrents, tracker).then(() => { - UIStore.dismissModal(); - }), - ); - }; - - render() { - return ( - - ); - } -} - -export default injectIntl(SetTrackerModal); diff --git a/client/src/javascript/components/modals/set-trackers-modal/SetTrackersModal.tsx b/client/src/javascript/components/modals/set-trackers-modal/SetTrackersModal.tsx new file mode 100644 index 00000000..26d34f22 --- /dev/null +++ b/client/src/javascript/components/modals/set-trackers-modal/SetTrackersModal.tsx @@ -0,0 +1,131 @@ +import {injectIntl, WrappedComponentProps} from 'react-intl'; +import {observable, runInAction} from 'mobx'; +import {observer} from 'mobx-react'; +import React from 'react'; + +import {Form, FormRow, Textbox} from '../../../ui'; +import Modal from '../Modal'; +import TextboxRepeater, {getTextArray} from '../../general/form-elements/TextboxRepeater'; +import TorrentActions from '../../../actions/TorrentActions'; +import TorrentStore from '../../../stores/TorrentStore'; +import UIStore from '../../../stores/UIStore'; + +import type {ModalAction} from '../../../stores/UIStore'; + +interface SetTrackersModalStates { + isLoadingTrackers: boolean; + isSettingTrackers: boolean; +} + +@observer +class SetTrackersModal extends React.Component { + trackerURLs = observable.array([]); + formRef: Form | null = null; + + constructor(props: WrappedComponentProps) { + super(props); + + TorrentActions.fetchTorrentTrackers(TorrentStore.selectedTorrents[0]).then((trackers) => { + if (trackers != null) { + runInAction(() => { + this.trackerURLs.replace( + trackers + .filter((tracker) => tracker.isEnabled) + .map((tracker) => tracker.url) + .filter((url) => url.startsWith('http') || url.startsWith('udp')), + ); + this.setState({isLoadingTrackers: false}); + }); + } + }); + + this.state = { + isSettingTrackers: false, + isLoadingTrackers: true, + }; + } + + getActions(): Array { + const primaryButtonText = this.props.intl.formatMessage({ + id: 'torrents.set.trackers.button.set', + }); + + return [ + { + clickHandler: null, + content: this.props.intl.formatMessage({ + id: 'button.cancel', + }), + triggerDismiss: true, + type: 'tertiary', + }, + { + clickHandler: this.handleSetTrackersClick, + content: primaryButtonText, + isLoading: this.state.isSettingTrackers || this.state.isLoadingTrackers, + triggerDismiss: false, + type: 'primary', + }, + ]; + } + + handleSetTrackersClick = (): void => { + if (this.formRef == null || this.state.isSettingTrackers || this.state.isLoadingTrackers) { + return; + } + + this.setState({isSettingTrackers: true}); + + const formData = this.formRef.getFormData() as Record; + const trackers = getTextArray(formData, 'trackers').filter((tracker) => tracker !== ''); + + TorrentActions.setTrackers({hashes: TorrentStore.selectedTorrents, trackers}).then(() => { + this.setState({isSettingTrackers: false}); + UIStore.dismissModal(); + }); + }; + + render() { + return ( + +
{ + this.formRef = ref; + }}> + {this.state.isLoadingTrackers ? ( + + + + ) : ( + ({id: index, value: url})) + } + /> + )} + + + } + heading={this.props.intl.formatMessage({ + id: 'torrents.set.trackers.heading', + })} + /> + ); + } +} + +export default injectIntl(SetTrackersModal); diff --git a/client/src/javascript/components/modals/settings-modal/lists/TorrentContextMenuActionsList.tsx b/client/src/javascript/components/modals/settings-modal/lists/TorrentContextMenuActionsList.tsx index 6dd8f831..48fa5c37 100644 --- a/client/src/javascript/components/modals/settings-modal/lists/TorrentContextMenuActionsList.tsx +++ b/client/src/javascript/components/modals/settings-modal/lists/TorrentContextMenuActionsList.tsx @@ -4,7 +4,6 @@ import {FormattedMessage} from 'react-intl'; import type {FloodSettings} from '@shared/types/FloodSettings'; import {Checkbox} from '../../../../ui'; -import ErrorIcon from '../../../icons/ErrorIcon'; import SettingStore from '../../../../stores/SettingStore'; import SortableList, {ListItem} from '../../../general/SortableList'; import Tooltip from '../../../general/Tooltip'; @@ -67,7 +66,6 @@ class TorrentContextMenuActionsList extends React.Component< renderItem = (item: ListItem) => { const {id, visible} = item as FloodSettings['torrentContextMenuActions'][number]; let checkbox = null; - let warning = null; if (!lockedIDs.includes(id)) { checkbox = ( @@ -81,28 +79,8 @@ class TorrentContextMenuActionsList extends React.Component< ); } - if (id === 'setTracker') { - const tooltipContent = ; - - warning = ( - { - this.tooltipRef = ref; - }} - width={200} - wrapperClassName="sortable-list__content sortable-list__content--secondary tooltip__wrapper" - wrapText> - - - ); - } - const content = (
- {warning} @@ -114,13 +92,16 @@ class TorrentContextMenuActionsList extends React.Component< }; render() { - const {torrentContextMenuActions} = this.state; + const torrentContextMenuActions = Object.keys(TorrentContextMenuActions).map((key) => ({ + id: key, + visible: this.state.torrentContextMenuActions.some((setting) => setting.id === key && setting.visible), + })); return ( { - const lockedIDIndex = lockedIDs.indexOf(column.id); + const torrentListColumnItems: ListItem[] = this.state.torrentListColumns + .filter((column) => TorrentListColumns[column.id] != null) + .slice(); - if (lockedIDIndex > -1) { - accumulator[lockedIDIndex] = column; - } else { - accumulator[nextUnlockedIndex] = column; - nextUnlockedIndex += 1; - } - - return accumulator; - }, []) - .filter((column) => column != null) - : this.state.torrentListColumns; + const newTorrentListColumnItems: ListItem[] = Object.keys(TorrentListColumns) + .filter((key) => this.state.torrentListColumns.every((column) => column.id !== key)) + .map((newColumn) => { + return { + id: newColumn, + visible: false, + }; + }); return ( { TorrentActions.fetchTorrentTrackers(UIStore.activeModal?.hash).then((trackers) => { if (trackers != null) { runInAction(() => { - this.trackers.replace(trackers); + this.trackers.replace(trackers.filter((tracker) => tracker.isEnabled)); }); } }); diff --git a/client/src/javascript/components/torrent-list/TorrentListContextMenu.tsx b/client/src/javascript/components/torrent-list/TorrentListContextMenu.tsx index c250278b..39beb64e 100644 --- a/client/src/javascript/components/torrent-list/TorrentListContextMenu.tsx +++ b/client/src/javascript/components/torrent-list/TorrentListContextMenu.tsx @@ -46,8 +46,8 @@ const handleItemClick = (action: TorrentContextMenuAction, event: React.MouseEve case 'setTaxonomy': UIActions.displayModal({id: 'set-taxonomy'}); break; - case 'setTracker': - UIActions.displayModal({id: 'set-tracker'}); + case 'setTrackers': + UIActions.displayModal({id: 'set-trackers'}); break; case 'start': TorrentActions.startTorrents({ @@ -130,8 +130,8 @@ const getContextMenuItems = (intl: IntlShape, torrent: TorrentProperties): Array }, { type: 'action', - action: 'setTracker', - label: intl.formatMessage(TorrentContextMenuActions.setTracker), + action: 'setTrackers', + label: intl.formatMessage(TorrentContextMenuActions.setTrackers), clickHandler, }, { diff --git a/client/src/javascript/constants/TorrentContextMenuActions.ts b/client/src/javascript/constants/TorrentContextMenuActions.ts index 3bded894..8f69bddb 100644 --- a/client/src/javascript/constants/TorrentContextMenuActions.ts +++ b/client/src/javascript/constants/TorrentContextMenuActions.ts @@ -17,9 +17,8 @@ const TorrentContextMenuActions = { move: { id: 'torrents.list.context.move', }, - setTracker: { - id: 'torrents.list.context.set.tracker', - warning: 'settings.warning.set.tracker', + setTrackers: { + id: 'torrents.list.context.set.trackers', }, torrentDetails: { id: 'torrents.list.context.details', diff --git a/client/src/javascript/i18n/strings.compiled.json b/client/src/javascript/i18n/strings.compiled.json index 8a041908..61071260 100644 --- a/client/src/javascript/i18n/strings.compiled.json +++ b/client/src/javascript/i18n/strings.compiled.json @@ -1429,12 +1429,6 @@ "value": "Expanded View" } ], - "settings.warning.set.tracker": [ - { - "type": 0, - "value": "Replaces main tracker. Ideal for single-tracker private torrents. Not recommended for multi-tracker torrents." - } - ], "sidebar.button.feeds": [ { "type": 0, @@ -2071,10 +2065,10 @@ "value": "Set Tags" } ], - "torrents.list.context.set.tracker": [ + "torrents.list.context.set.trackers": [ { "type": 0, - "value": "Set Tracker" + "value": "Set Trackers" } ], "torrents.list.context.start": [ @@ -2339,22 +2333,28 @@ "value": "Set Tags" } ], - "torrents.set.tracker.button.set": [ + "torrents.set.trackers.button.set": [ { "type": 0, - "value": "Set Tracker" + "value": "Set Trackers" } ], - "torrents.set.tracker.enter.tracker": [ + "torrents.set.trackers.enter.tracker": [ { "type": 0, "value": "Enter a tracker" } ], - "torrents.set.tracker.heading": [ + "torrents.set.trackers.heading": [ { "type": 0, - "value": "Set Tracker" + "value": "Set Trackers" + } + ], + "torrents.set.trackers.loading.trackers": [ + { + "type": 0, + "value": "Loading trackers..." } ], "torrents.sort.title": [ diff --git a/client/src/javascript/i18n/strings.json b/client/src/javascript/i18n/strings.json index 627c67e2..0a29f623 100644 --- a/client/src/javascript/i18n/strings.json +++ b/client/src/javascript/i18n/strings.json @@ -187,7 +187,6 @@ "settings.diskusage.show": "Show", "settings.diskusage.mount.points": "Disk Usage Mount Points", "settings.about.flood": "About Flood", - "settings.warning.set.tracker": "Replaces main tracker. Ideal for single-tracker private torrents. Not recommended for multi-tracker torrents.", "sidebar.button.feeds": "Feeds", "sidebar.button.notifications": "Notifications", "sidebar.button.settings": "Settings", @@ -288,7 +287,7 @@ "torrents.list.context.priority": "Priority", "torrents.list.context.remove": "Remove", "torrents.list.context.set.tags": "Set Tags", - "torrents.list.context.set.tracker": "Set Tracker", + "torrents.list.context.set.trackers": "Set Trackers", "torrents.list.context.start": "Start", "torrents.list.context.stop": "Stop", "torrents.list.no.torrents": "No torrents to display.", @@ -330,9 +329,10 @@ "torrents.set.tags.button.set": "Set Tags", "torrents.set.tags.heading": "Set Tags", "torrents.set.tags.enter.tags": "Enter tags", - "torrents.set.tracker.button.set": "Set Tracker", - "torrents.set.tracker.heading": "Set Tracker", - "torrents.set.tracker.enter.tracker": "Enter a tracker", + "torrents.set.trackers.button.set": "Set Trackers", + "torrents.set.trackers.heading": "Set Trackers", + "torrents.set.trackers.enter.tracker": "Enter a tracker", + "torrents.set.trackers.loading.trackers": "Loading trackers...", "torrents.sort.title": "Sort By", "connection-interruption.heading": "Cannot connect to the client", "connection-interruption.verify-settings-prompt": "Let's verify your connection settings.", diff --git a/client/src/javascript/stores/UIStore.ts b/client/src/javascript/stores/UIStore.ts index 8a88631f..6b1eb24a 100644 --- a/client/src/javascript/stores/UIStore.ts +++ b/client/src/javascript/stores/UIStore.ts @@ -56,7 +56,7 @@ export type ModalAction = CheckboxModalAction | ButtonModalAction; export type Modal = | { - id: 'feeds' | 'move-torrents' | 'remove-torrents' | 'set-taxonomy' | 'set-tracker' | 'settings'; + id: 'feeds' | 'move-torrents' | 'remove-torrents' | 'set-taxonomy' | 'set-trackers' | 'settings'; } | { id: 'add-torrents'; diff --git a/server/routes/api/torrents.ts b/server/routes/api/torrents.ts index 76abbd4e..fd791b1f 100644 --- a/server/routes/api/torrents.ts +++ b/server/routes/api/torrents.ts @@ -462,7 +462,6 @@ router.get('/:hash/contents/:indices/data', (req, res) => { }); /** - * TODO: Split to /peers, /trackers and /contents endpoints * GET /api/torrents/{hash}/details * @summary Gets details of a torrent. * @tags Torrent diff --git a/shared/constants/defaultFloodSettings.ts b/shared/constants/defaultFloodSettings.ts index 4dfb00a3..122dec29 100644 --- a/shared/constants/defaultFloodSettings.ts +++ b/shared/constants/defaultFloodSettings.ts @@ -55,7 +55,7 @@ const defaultFloodSettings: Readonly = { {id: 'checkHash', visible: true}, {id: 'setTaxonomy', visible: true}, {id: 'move', visible: true}, - {id: 'setTracker', visible: false}, + {id: 'setTrackers', visible: false}, {id: 'torrentDetails', visible: true}, {id: 'torrentDownload', visible: true}, {id: 'setPriority', visible: false},