client: fix set tracker and add set multi trackers support

This commit is contained in:
Jesse Chan
2020-10-25 18:18:40 +08:00
parent becb09cdd5
commit f113835864
15 changed files with 186 additions and 185 deletions
@@ -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<TorrentProperties['hash']>, 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(
() => {
@@ -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 <RemoveTorrentsModal />;
case 'set-taxonomy':
return <SetTagsModal />;
case 'set-tracker':
return <SetTrackerModal />;
case 'set-trackers':
return <SetTrackersModal />;
case 'settings':
return <SettingsModal />;
case 'torrent-details':
@@ -56,7 +56,9 @@ class SetTagsModal extends React.Component<WrappedComponentProps, SetTagsModalSt
}}>
<FormRow>
<TagSelect
defaultValue={TorrentStore.selectedTorrents.map((hash: string) => 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',
@@ -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<WrappedComponentProps, SetTrackerModalStates> {
formRef: Form | null = null;
constructor(props: WrappedComponentProps) {
super(props);
this.state = {
isSettingTracker: false,
};
}
getActions(): Array<ModalAction> {
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 (
<div className="modal__content inverse">
<Form
ref={(ref) => {
this.formRef = ref;
}}>
<FormRow>
<Textbox
defaultValue={trackerValue}
id="tracker"
placeholder={this.props.intl.formatMessage({
id: 'torrents.set.tracker.enter.tracker',
})}
/>
</FormRow>
</Form>
</div>
);
}
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 (
<Modal
actions={this.getActions()}
content={this.getContent()}
heading={this.props.intl.formatMessage({
id: 'torrents.set.tracker.heading',
})}
/>
);
}
}
export default injectIntl(SetTrackerModal);
@@ -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<WrappedComponentProps, SetTrackersModalStates> {
trackerURLs = observable.array<string>([]);
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<ModalAction> {
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<string, string>;
const trackers = getTextArray(formData, 'trackers').filter((tracker) => tracker !== '');
TorrentActions.setTrackers({hashes: TorrentStore.selectedTorrents, trackers}).then(() => {
this.setState({isSettingTrackers: false});
UIStore.dismissModal();
});
};
render() {
return (
<Modal
actions={this.getActions()}
content={
<div className="modal__content inverse">
<Form
ref={(ref) => {
this.formRef = ref;
}}>
{this.state.isLoadingTrackers ? (
<FormRow>
<Textbox
id="loading"
placeholder={this.props.intl.formatMessage({
id: 'torrents.set.trackers.loading.trackers',
})}
/>
</FormRow>
) : (
<TextboxRepeater
id="trackers"
placeholder={this.props.intl.formatMessage({
id: 'torrents.set.trackers.enter.tracker',
})}
defaultValues={
this.trackerURLs.length === 0
? undefined
: this.trackerURLs.map((url, index) => ({id: index, value: url}))
}
/>
)}
</Form>
</div>
}
heading={this.props.intl.formatMessage({
id: 'torrents.set.trackers.heading',
})}
/>
);
}
}
export default injectIntl(SetTrackersModal);
@@ -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 = <FormattedMessage id={TorrentContextMenuActions[id].warning} />;
warning = (
<Tooltip
className="tooltip tooltip--is-error"
content={tooltipContent}
offset={-5}
ref={(ref) => {
this.tooltipRef = ref;
}}
width={200}
wrapperClassName="sortable-list__content sortable-list__content--secondary tooltip__wrapper"
wrapText>
<ErrorIcon />
</Tooltip>
);
}
const content = (
<div className="sortable-list__content sortable-list__content__wrapper">
{warning}
<span className="sortable-list__content sortable-list__content--primary">
<FormattedMessage id={TorrentContextMenuActions[id].id} />
</span>
@@ -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 (
<SortableList
id="torrent-context-menu-items"
className="sortable-list--torrent-context-menu-items"
items={torrentContextMenuActions.slice()}
items={torrentContextMenuActions}
lockedIDs={lockedIDs}
isDraggable={false}
onMouseDown={this.handleMouseDown}
@@ -6,10 +6,11 @@ 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 SortableList from '../../../general/SortableList';
import Tooltip from '../../../general/Tooltip';
import TorrentListColumns from '../../../../constants/TorrentListColumns';
import type {ListItem} from '../../../general/SortableList';
import type {TorrentListColumn} from '../../../../constants/TorrentListColumns';
interface TorrentListColumnsListProps {
@@ -121,31 +122,25 @@ class TorrentListColumnsList extends React.Component<TorrentListColumnsListProps
render(): React.ReactNode {
const lockedIDs = this.getLockedIDs();
let nextUnlockedIndex = lockedIDs.length;
const torrentListColumnItems =
this.props.torrentListViewSize === 'expanded'
? this.state.torrentListColumns
.reduce((accumulator: FloodSettings['torrentListColumns'], column) => {
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 (
<SortableList
id="torrent-details"
className="sortable-list--torrent-details"
items={torrentListColumnItems.slice()}
items={torrentListColumnItems.concat(newTorrentListColumnItems)}
lockedIDs={lockedIDs}
onMouseDown={this.handleMouseDown}
onDrop={this.handleMove}
@@ -20,7 +20,7 @@ class TorrentTrackers extends React.Component<unknown> {
TorrentActions.fetchTorrentTrackers(UIStore.activeModal?.hash).then((trackers) => {
if (trackers != null) {
runInAction(() => {
this.trackers.replace(trackers);
this.trackers.replace(trackers.filter((tracker) => tracker.isEnabled));
});
}
});
@@ -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,
},
{
@@ -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',
@@ -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": [
+5 -5
View File
@@ -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.",
+1 -1
View File
@@ -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';
-1
View File
@@ -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
+1 -1
View File
@@ -55,7 +55,7 @@ const defaultFloodSettings: Readonly<FloodSettings> = {
{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},