mirror of
https://github.com/zoriya/flood.git
synced 2026-06-07 20:30:42 +00:00
client: fix set tracker and add set multi trackers support
This commit is contained in:
@@ -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);
|
||||
+5
-24
@@ -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}
|
||||
|
||||
+14
-19
@@ -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": [
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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},
|
||||
|
||||
Reference in New Issue
Block a user